diff --git a/src/sonic-framework/rebootbackend/redis_utils.cpp b/src/sonic-framework/rebootbackend/redis_utils.cpp deleted file mode 100644 index 4010e99d423a..000000000000 --- a/src/sonic-framework/rebootbackend/redis_utils.cpp +++ /dev/null @@ -1,80 +0,0 @@ -#include "redis_utils.h" - -#include -#include -#include -#include - -#include "dbconnector.h" -#include "notificationproducer.h" -//#include "stateverification.h" -#include "table.h" -#include "timestamp.h" -#include "warm_restart.h" - -namespace rebootbackend { - -using WarmStartState = ::swss::WarmStart::WarmStartState; - - -void init_warm_reboot_states(const swss::DBConnector &db) { - swss::Table table(&db, STATE_WARM_RESTART_TABLE_NAME); - std::vector keys; - - table.getKeys(keys); - for (auto &key : keys) { - table.hdel(key, "state"); - table.hdel(key, "timestamp"); - } -} - -void set_warm_restart_enable(const swss::DBConnector &db, bool enabled) { - swss::Table table(&db, STATE_WARM_RESTART_ENABLE_TABLE_NAME); - table.hset("system", "enable", enabled ? "true" : "false"); -} - -bool is_valid_key(const std::string &key, const std::string &separator) { - if (separator.empty()) { - return false; - } - - size_t pos = key.find(separator); - // The separator must exist in the string, and cannot be the first or last - // character. - return !(pos == std::string::npos || pos == 0 || pos == key.size() - 1); -} - -bool get_docker_app_from_key(const std::string &key, - const std::string &separator, std::string &docker, - std::string &app) { - SWSS_LOG_ENTER(); - - size_t pos = key.find(separator); - - if (separator.empty()) { - SWSS_LOG_ERROR("separator [%s] shouldn't be empty", separator.c_str()); - return false; - } - - if (pos == std::string::npos) { - SWSS_LOG_ERROR("key [%s] should contain separator [%s]", key.c_str(), - separator.c_str()); - return false; - } - - docker = key.substr(0, pos); - app = key.substr(pos + separator.length(), std::string::npos); - - if (docker.empty()) { - SWSS_LOG_ERROR("docker name shouldn't be empty, key = %s", key.c_str()); - return false; - } - - if (app.empty()) { - SWSS_LOG_ERROR("app name shouldn't be empty, key = %s", key.c_str()); - return false; - } - return true; -} - -} // namespace rebootbackend diff --git a/src/sonic-framework/rebootbackend/redis_utils.h b/src/sonic-framework/rebootbackend/redis_utils.h deleted file mode 100644 index 05d87c2aef1c..000000000000 --- a/src/sonic-framework/rebootbackend/redis_utils.h +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once -#include -#include -#include - -#include "dbconnector.h" -#include "notificationconsumer.h" -#include "notificationproducer.h" -#include "selectableevent.h" -#include "status_code_util.h" -#include "warm_restart.h" - -namespace rebootbackend { - -// Return string corresponding to state -std::string get_warm_start_state_name( - const swss::WarmStart::WarmStartState state); - -void init_warm_reboot_states(const swss::DBConnector &db); - -// Set the system warm start state to a new enabled/disabled state. -// STATE_WARM_RESTART_TABLE_NAME -// key = system, field = enable, value = "true"/"false" -void set_warm_restart_enable(const swss::DBConnector &db, bool enabled); - -// Returns true if key is in the formm "texttext", and false -// otherwise. -bool is_valid_key(const std::string &key, const std::string &separator); - -// Helper function: given key of form "docker|app" -// extract docker and app. (separator = | in this case) -// return false if docker or app are empty or separator -// isn't present, else true. -// key and separator are inputs -// docker and app are outputs -bool get_docker_app_from_key(const std::string &key, - const std::string &separator, std::string &docker, - std::string &app); - -} // namespace rebootbackend diff --git a/src/sonic-framework/tests/init_thread_test.cpp b/src/sonic-framework/tests/init_thread_test.cpp new file mode 100644 index 000000000000..208fecee9550 --- /dev/null +++ b/src/sonic-framework/tests/init_thread_test.cpp @@ -0,0 +1,154 @@ +#include "init_thread.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include "mock_reboot_interfaces.h" +#include "reboot_interfaces.h" +#include "redis_utils.h" +#include "select.h" +#include "selectableevent.h" +#include "stateverification.h" +#include "status_code_util.h" +#include "table.h" +#include "test_utils_common.h" +#include "timestamp.h" + +namespace rebootbackend { + +using ::testing::_; +using ::testing::AtLeast; +using ::testing::ExplainMatchResult; +using ::testing::IsEmpty; +using ::testing::Not; +using ::testing::Return; +using ::testing::StrEq; +using ::testing::StrictMock; + +constexpr int kSelectTimeoutSeconds = 5; +constexpr int kShortSelectTimeoutSeconds = 1; + +MATCHER(IsDoneStatus, "") { + const InitThreadStatus::DetailedStatus &status = arg; + if (status.thread_state.active()) { + *result_listener << "Status was active, expected inactive"; + return false; + } + if (status.detailed_thread_status != InitThreadStatus::ThreadStatus::DONE) { + *result_listener << "Status was not DONE: " + << status.detailed_thread_status; + return false; + } + if (status.thread_state.status().status() != + gnoi::system::RebootStatus_Status::RebootStatus_Status_STATUS_SUCCESS) { + *result_listener << "Proto status was not SUCCESS: " + << status.thread_state.status().status(); + return false; + } + return true; +} + +MATCHER_P(IsActiveStatus, state_matcher, "") { + const InitThreadStatus::DetailedStatus &status = arg; + if (!status.thread_state.active()) { + *result_listener << "Status was inactive, expected active"; + return false; + } + if (status.thread_state.status().status() != + gnoi::system::RebootStatus_Status::RebootStatus_Status_STATUS_UNKNOWN) { + *result_listener << "Proto status was not UNKNOWN: " + << status.thread_state.status().status(); + return false; + } + return ExplainMatchResult(state_matcher, status.detailed_thread_status, + result_listener); +} + +MATCHER_P(IsErrorStatus, error_condition_matcher, "") { + const InitThreadStatus::DetailedStatus &status = arg; + if (status.thread_state.active()) { + *result_listener << "Status was active, expected inactive"; + return false; + } + if (status.detailed_thread_status != InitThreadStatus::ThreadStatus::ERROR) { + *result_listener << "Status was not ERROR: " + << status.detailed_thread_status; + return false; + } + if (status.thread_state.status().status() != + gnoi::system::RebootStatus_Status::RebootStatus_Status_STATUS_FAILURE) { + *result_listener << "Proto status was not FAILURE: " + << status.thread_state.status().status(); + return false; + } + return ExplainMatchResult(error_condition_matcher, + status.detailed_thread_error_condition, + result_listener); +} + +class InitThreadTest : public ::testing::Test { + public: + InitThreadTest() + : m_db("STATE_DB", 0), + m_config_db("CONFIG_DB", 0), + m_critical_interface(), + m_init_thread(m_critical_interface, m_telemetry, m_finished, + m_stack_unfrozen) { + // sigterm_requested and the Redis tables have global state that is + // maintained across tests. + sigterm_requested = false; + TestUtils::clear_tables(m_db); + init_redis_defaults(); + } + + void populate_default_init_table() { + initTable.hset("docker1|app1", "timestamp", ""); + initTable.hset("docker2|app2", "timestamp", ""); + initTable.hset("docker3|app3", "timestamp", ""); + // The invalid entry should not end up in the list of apps. + initTable.hset("invalid", "timestamp", ""); + } + + void advance_through_registration() { + populate_default_init_table(); + TestUtils::populate_registration_table(m_db, "docker1|app1", false, false, + false, true); + TestUtils::populate_registration_table(m_db, "docker2|app2", true, true, + true, false); + TestUtils::populate_registration_table(m_db, "docker3|app3", false, false, + true, false); + } + + void set_apps_to_state(std::string state) { + TestUtils::populate_restart_table_state(m_db, "app1", state); + TestUtils::populate_restart_table_state(m_db, "app2", state); + TestUtils::populate_restart_table_state(m_db, "app3", state); + } + + protected: + swss::DBConnector m_db; + swss::DBConnector m_config_db; + StrictMock m_critical_interface; + StrictMock m_telemetry; + swss::NotificationConsumer m_nsf_channel; + swss::SelectableEvent m_finished; + swss::SelectableEvent m_stack_unfrozen; + InitThread m_init_thread; +}; + +TEST_F(InitThreadTest, TestJoinWithoutStart) { + EXPECT_FALSE(m_init_thread.Join()); +} + +class InitThreadTestWithSvResult + : public InitThreadTest, + public testing::WithParamInterface {}; + + +} // namespace rebootbackend diff --git a/src/sonic-framework/tests/mock_reboot_interfaces.h b/src/sonic-framework/tests/mock_reboot_interfaces.h new file mode 100644 index 000000000000..37b7492db371 --- /dev/null +++ b/src/sonic-framework/tests/mock_reboot_interfaces.h @@ -0,0 +1,35 @@ +#pragma once +#include + +#include "reboot_interfaces.h" +#include "selectableevent.h" +#include "system/system.pb.h" + +namespace rebootbackend { + +class MockDbusInterface : public DbusInterface { + public: + MOCK_METHOD(DbusInterface::DbusResponse, Reboot, (const std::string &), + (override)); + MOCK_METHOD(DbusInterface::DbusResponse, RebootStatus, (const std::string &), + (override)); +}; + +/*class MockInitThread : public InitThread { + public: + MockInitThread() + : InitThread(m_unused_event, + m_unused_event) {} + + MOCK_METHOD(swss::StatusCode, Start, (), (override)); + MOCK_METHOD(void, Stop, (), (override)); + MOCK_METHOD(bool, Join, (), (override)); + MOCK_METHOD(gnoi::system::RebootStatusResponse, GetResponse, (), (override)); + MOCK_METHOD(InitThreadStatus::DetailedStatus, GetDetailedStatus, (), + (override)); + + private: + swss::SelectableEvent m_unused_event; +};*/ + +} // namespace rebootbackend diff --git a/src/sonic-framework/tests/reboot_common_test.cpp b/src/sonic-framework/tests/reboot_common_test.cpp new file mode 100644 index 000000000000..72f0cd9dc7ca --- /dev/null +++ b/src/sonic-framework/tests/reboot_common_test.cpp @@ -0,0 +1,27 @@ +#include "reboot_common.h" + +#include +#include +#include + +namespace rebootbackend { + +using ::testing::_; +using ::testing::AtLeast; +using ::testing::ExplainMatchResult; +using ::testing::StrEq; + +MATCHER_P2(CheckTimespec, secs, nsecs, "") { + return (arg.tv_sec == secs && arg.tv_nsec == nsecs); +} + +TEST(RebootCommon, MillisecToTimespec) { + timespec l_timespec = milliseconds_to_timespec(0); + EXPECT_THAT(l_timespec, CheckTimespec(0, 0)); + l_timespec = milliseconds_to_timespec(200); + EXPECT_THAT(l_timespec, CheckTimespec(0, 200 * 1000 * 1000)); + l_timespec = milliseconds_to_timespec(1800); + EXPECT_THAT(l_timespec, CheckTimespec(1, 800 * 1000 * 1000)); +} + +} // namespace rebootbackend diff --git a/src/sonic-framework/tests/reboot_thread_test.cpp b/src/sonic-framework/tests/reboot_thread_test.cpp new file mode 100644 index 000000000000..35778ff864a1 --- /dev/null +++ b/src/sonic-framework/tests/reboot_thread_test.cpp @@ -0,0 +1,276 @@ +#include "reboot_thread.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include "mock_reboot_interfaces.h" +#include "reboot_common.h" +#include "reboot_interfaces.h" +#include "redis_utils.h" +#include "select.h" +#include "selectableevent.h" +#include "status_code_util.h" +#include "system/system.pb.h" +#include "test_utils_common.h" +#include "timestamp.h" + +namespace rebootbackend { + +#define TENTH_SECOND_MS (100) + +using namespace gnoi::system; +namespace gpu = ::google::protobuf::util; +using Progress = ::rebootbackend::RebootThread::Progress; +using RebootThread = ::rebootbackend::RebootThread; +using ::testing::_; +using ::testing::ExplainMatchResult; +using ::testing::HasSubstr; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::StrEq; +using ::testing::StrictMock; + +MATCHER_P2(IsStatus, status, message, "") { + return (arg.status().status() == status && + ExplainMatchResult(message, arg.status().message(), result_listener)); +} + +class RebootStatusTest : public ::testing::Test { + protected: + RebootStatusTest() : m_status() {} + ThreadStatus m_status; +}; + +TEST_F(RebootStatusTest, TestInit) { + RebootStatusResponse response = m_status.get_response(); + + EXPECT_FALSE(response.active()); + EXPECT_THAT(response.reason(), StrEq("")); + EXPECT_EQ(response.count(), 0); + EXPECT_EQ(response.method(), RebootMethod::UNKNOWN); + EXPECT_EQ(response.status().status(), + RebootStatus_Status::RebootStatus_Status_STATUS_UNKNOWN); + EXPECT_THAT(response.status().message(), StrEq("")); + + EXPECT_FALSE(m_status.get_active()); +} + +TEST_F(RebootStatusTest, TestGetStatus) { + std::chrono::nanoseconds curr_ns = std::chrono::high_resolution_clock::now().time_since_epoch(); + + m_status.set_start_status(RebootMethod::COLD, "reboot because"); + + RebootStatusResponse response = m_status.get_response(); + EXPECT_EQ(response.status().status(), + RebootStatus_Status::RebootStatus_Status_STATUS_UNKNOWN); + + m_status.set_completed_status( + RebootStatus_Status::RebootStatus_Status_STATUS_SUCCESS, "anything"); + + response = m_status.get_response(); + + // message should be empty while reboot is active + EXPECT_THAT(response.status().message(), StrEq("")); + + uint64_t reboot_ns = response.when(); + EXPECT_TRUE(reboot_ns > (uint64_t)curr_ns.count()); + + m_status.set_inactive(); + response = m_status.get_response(); + EXPECT_THAT(response.status().message(), StrEq("anything")); + EXPECT_EQ(response.status().status(), + RebootStatus_Status::RebootStatus_Status_STATUS_SUCCESS); + EXPECT_EQ(0, response.when()); +} + +class RebootThreadTest : public ::testing::Test { + protected: + RebootThreadTest() + : m_dbus_interface(), + m_db("STATE_DB", 0), + m_config_db("CONFIG_DB", 0), + m_reboot_thread(m_dbus_interface, + m_finished) { + swss::WarmStart::initialize("app1", "docker1"); + sigterm_requested = false; + } + + void overwrite_reboot_timeout(uint32_t timeout_seconds) { + m_reboot_thread.m_reboot_timeout = timeout_seconds; + } + + RebootStatusResponse get_response(void) { + return m_reboot_thread.m_status.get_response(); + } + + void set_start_status(const RebootMethod &method, const std::string &reason) { + return m_reboot_thread.m_status.set_start_status(method, reason); + } + + void set_completed_status(const RebootStatus_Status &status, + const std::string &message) { + return m_reboot_thread.m_status.set_completed_status(status, message); + } + + void force_inactive(void) { return m_reboot_thread.m_status.set_inactive(); } + + void force_active(void) { return m_reboot_thread.m_status.set_inactive(); } + + void do_reboot(void) { return m_reboot_thread.do_reboot(); } + + Progress wait_for_platform_reboot(swss::Select &s) { + return m_reboot_thread.wait_for_platform_reboot(s); + } + + swss::SelectableEvent &return_m_stop_reference() { + return m_reboot_thread.m_stop; + } + + swss::DBConnector m_db; + swss::DBConnector m_config_db; + NiceMock m_dbus_interface; + swss::SelectableEvent m_finished; + RebootThread m_reboot_thread; +}; + +MATCHER_P2(Status, status, message, "") { + return (arg.status().status() == status && arg.status().message() == message); +} + +TEST_F(RebootThreadTest, TestStop) { + EXPECT_CALL(m_dbus_interface, Reboot(_)) + .Times(1) + .WillOnce(Return(DbusInterface::DbusResponse{ + DbusInterface::DbusStatus::DBUS_SUCCESS, ""})); + RebootRequest request; + request.set_method(RebootMethod::COLD); + overwrite_reboot_timeout(2); + m_reboot_thread.Start(request); + m_reboot_thread.Stop(); + m_reboot_thread.Join(); + gnoi::system::RebootStatusResponse response = m_reboot_thread.GetResponse(); + EXPECT_THAT( + response, + IsStatus(RebootStatus_Status::RebootStatus_Status_STATUS_UNKNOWN, "")); +// EXPECT_THAT(response.status().message(), StrEq("platform failed to reboot")); +} + +TEST_F(RebootThreadTest, TestCleanExit) { + EXPECT_CALL(m_dbus_interface, Reboot(_)) + .Times(1) + .WillOnce(Return(DbusInterface::DbusResponse{ + DbusInterface::DbusStatus::DBUS_SUCCESS, ""})); + + overwrite_reboot_timeout(1); + + swss::Select s; + s.addSelectable(&m_finished); + + RebootRequest request; + request.set_method(RebootMethod::COLD); + request.set_message("time to reboot"); + m_reboot_thread.Start(request); + TestUtils::wait_for_finish(s, m_finished, 5); + + + // Status should be active until we call join + RebootStatusResponse response = get_response(); + EXPECT_TRUE(response.active()); + EXPECT_THAT(response.reason(), StrEq("time to reboot")); + EXPECT_EQ(response.count(), 1); + + EXPECT_THAT(response.status().message(), StrEq("")); + + m_reboot_thread.Join(); + + response = get_response(); + EXPECT_FALSE(response.active()); + EXPECT_THAT(response.status().message(), StrEq("platform failed to reboot")); +} + +TEST_F(RebootThreadTest, TestJoinWithoutStart) { + bool ret = m_reboot_thread.Join(); + EXPECT_FALSE(ret); +} + +// Call Start a second time while first thread is still executing. + TEST_F(RebootThreadTest, TestStartWhileRunning) { + EXPECT_CALL(m_dbus_interface, Reboot(_)) + .Times(1) + .WillOnce(Return(DbusInterface::DbusResponse{ + DbusInterface::DbusStatus::DBUS_SUCCESS, ""})); + + overwrite_reboot_timeout(2); + + RebootRequest request; + request.set_method(RebootMethod::COLD); + request.set_message("time to reboot"); + m_reboot_thread.Start(request); + + // First thread is still running ... + NotificationResponse response = m_reboot_thread.Start(request); + EXPECT_EQ(response.status, swss::StatusCode::SWSS_RC_IN_USE); + EXPECT_THAT(response.json_string, + StrEq("RebootThread: can't Start while active")); + + bool ret = m_reboot_thread.Join(); + EXPECT_TRUE(ret); +} + +// Call Start a second time after first thread completed +// but before first thread was joined. +// Second start should fail. + TEST_F(RebootThreadTest, TestStartWithoutJoin) { + EXPECT_CALL(m_dbus_interface, Reboot(_)) + .Times(1) + .WillOnce(Return(DbusInterface::DbusResponse{ + DbusInterface::DbusStatus::DBUS_SUCCESS, ""})); + + overwrite_reboot_timeout(1); + + swss::Select s; + s.addSelectable(&m_finished); + + RebootRequest request; + request.set_method(RebootMethod::COLD); + request.set_message("time to reboot"); + m_reboot_thread.Start(request); + TestUtils::wait_for_finish(s, m_finished, 3); + + // First thread has stopped: we need to join before + // restart will succeed + NotificationResponse response = m_reboot_thread.Start(request); + EXPECT_EQ(response.status, swss::StatusCode::SWSS_RC_IN_USE); + + // This should join the first start. + bool ret = m_reboot_thread.Join(); + EXPECT_TRUE(ret); +} + +TEST_F(RebootThreadTest, TestUnsupportedRebootType) { + RebootRequest request; + request.set_method(RebootMethod::POWERDOWN); + + NotificationResponse response = m_reboot_thread.Start(request); + EXPECT_EQ(response.status, swss::StatusCode::SWSS_RC_INVALID_PARAM); + EXPECT_EQ(response.json_string, + "RebootThread: Start rx'd unsupported method"); +} + +TEST_F(RebootThreadTest, TestInvalidMethodfDoReboot) { + set_start_status(RebootMethod::POWERUP, "time to reboot"); + do_reboot(); + force_inactive(); + RebootStatusResponse response = m_reboot_thread.GetResponse(); + EXPECT_THAT( + response, + IsStatus(RebootStatus_Status::RebootStatus_Status_STATUS_UNKNOWN, "")); +} + +} // namespace rebootbackend diff --git a/src/sonic-framework/tests/rebootbe_test.cpp b/src/sonic-framework/tests/rebootbe_test.cpp new file mode 100644 index 000000000000..7c23f996bad3 --- /dev/null +++ b/src/sonic-framework/tests/rebootbe_test.cpp @@ -0,0 +1,420 @@ +#include "rebootbe.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "mock_reboot_interfaces.h" +#include "reboot_common.h" +#include "select.h" +#include "status_code_util.h" +#include "system/system.pb.h" +#include "test_utils_common.h" +#include "timestamp.h" + +namespace rebootbackend { + +#define ONE_SECOND (1) +#define TWO_SECONDS (2) +#define TENTH_SECOND_MS (100) +#define HALF_SECOND_MS (500) +#define ONE_SECOND_MS (1000) +#define FIFTEEN_HUNDRED_MS (1500) +#define TWO_SECONDS_MS (2000) + +namespace gpu = ::google::protobuf::util; +using namespace gnoi::system; + +using ::testing::_; +using ::testing::AllOf; +using ::testing::AtLeast; +using ::testing::ExplainMatchResult; +using ::testing::HasSubstr; +using ::testing::InSequence; +using ::testing::Invoke; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::StrEq; +using ::testing::StrictMock; + +MATCHER_P2(IsStatus, status, message, "") { + return (arg.status().status() == status && + ExplainMatchResult(message, arg.status().message(), result_listener)); +} + +MATCHER_P3(ActiveCountMethod, active, count, method, "") { + return (arg.active() == active && arg.count() == (uint32_t)count && + arg.method() == method); +} + +class RebootBETestWithoutStop : public ::testing::Test { + protected: + RebootBETestWithoutStop() + : m_dbus_interface(), + m_db("STATE_DB", 0), + m_config_db("CONFIG_DB", 0), + m_rebootbeRequestChannel(&m_db, REBOOT_REQUEST_NOTIFICATION_CHANNEL), + m_rebootbeReponseChannel(&m_db, REBOOT_RESPONSE_NOTIFICATION_CHANNEL), + m_rebootbe(m_dbus_interface) { + sigterm_requested = false; +// TestUtils::clear_tables(m_db); + + + m_s.addSelectable(&m_rebootbeReponseChannel); + + // Make the tests log to stdout, instead of syslog. + swss::Table logging_table(&m_config_db, CFG_LOGGER_TABLE_NAME); + logging_table.hset("rebootbackend", swss::DAEMON_LOGOUTPUT, "STDOUT"); + swss::Logger::restartLogger(); + } + virtual ~RebootBETestWithoutStop() = default; + + gnoi::system::RebootStatusResponse default_not_started_status() { + InitThreadStatus status; + return status.get_response(); + } + + gnoi::system::RebootStatusResponse default_done_status() { + InitThreadStatus status; + // We can't edit the status without it being active. + status.set_start_status(); + status.set_success(); + status.set_inactive(); + return status.get_response(); + } + + + void start_rebootbe() { + m_rebootbe_thread = + std::make_unique(&RebootBE::Start, &m_rebootbe); + } + + void set_mock_defaults() { + ON_CALL(m_dbus_interface, Reboot(_)) + .WillByDefault(Return(DbusInterface::DbusResponse{ + DbusInterface::DbusStatus::DBUS_SUCCESS, ""})); + } + + void overwrite_reboot_timeout(uint32_t timeout_seconds) { + m_rebootbe.m_reboot_thread.m_reboot_timeout = timeout_seconds; + } + + + void send_stop_reboot_thread() { m_rebootbe.m_reboot_thread.Stop(); } + + void SendRebootRequest(const std::string &op, const std::string &data, + const std::string &field, const std::string &value) { + std::vector values; + values.push_back(swss::FieldValueTuple{field, value}); + + m_rebootbeRequestChannel.send(op, data, values); + } + + void SendRebootViaProto(RebootRequest &request) { + std::string json_string; + gpu::MessageToJsonString(request, &json_string); + + SendRebootRequest("Reboot", "StatusCode", DATA_TUPLE_KEY, json_string); + } + + void SendRebootStatusRequest(void) { + SendRebootRequest("RebootStatus", "StatusCode", DATA_TUPLE_KEY, + "json status request"); + } + + void start_reboot_via_rpc( + RebootRequest &request, + swss::StatusCode expected_result = swss::StatusCode::SWSS_RC_SUCCESS) { + SendRebootViaProto(request); + while (true) { + int ret; + swss::Selectable *sel; + ret = m_s.select(&sel, SELECT_TIMEOUT_250_MS); + if (ret != swss::Select::OBJECT) continue; + if (sel != &m_rebootbeReponseChannel) continue; + break; + } + std::string op, data; + std::vector ret_values; + m_rebootbeReponseChannel.pop(op, data, ret_values); + + EXPECT_THAT(op, StrEq("Reboot")); + EXPECT_THAT(data, StrEq(swss::statusCodeToStr(expected_result))); + } + + gnoi::system::RebootStatusResponse do_reboot_status_rpc() { + SendRebootStatusRequest(); + while (true) { + int ret; + swss::Selectable *sel; + ret = m_s.select(&sel, SELECT_TIMEOUT_250_MS); + if (ret != swss::Select::OBJECT) continue; + if (sel != &m_rebootbeReponseChannel) continue; + break; + } + std::string op, data; + std::vector ret_values; + m_rebootbeReponseChannel.pop(op, data, ret_values); + + EXPECT_THAT(op, StrEq("RebootStatus")); + EXPECT_EQ(data, swss::statusCodeToStr(swss::StatusCode::SWSS_RC_SUCCESS)); + + std::string json_response; + for (auto &fv : ret_values) { + if (DATA_TUPLE_KEY == fvField(fv)) { + json_response = fvValue(fv); + } + } + gnoi::system::RebootStatusResponse response; + gpu::JsonStringToMessage(json_response, &response); + return response; + } + + void GetNotificationResponse(swss::NotificationConsumer &consumer, + std::string &op, std::string &data, + std::vector &values) { + swss::Select s; + s.addSelectable(&consumer); + swss::Selectable *sel; + s.select(&sel, SELECT_TIMEOUT_250_MS); + + consumer.pop(op, data, values); + } + + NotificationResponse handle_reboot_request(std::string &json_request) { + return m_rebootbe.handle_reboot_request(json_request); + } + + + // Mock interfaces. + NiceMock m_dbus_interface; + + // DB connectors + swss::DBConnector m_db; + swss::DBConnector m_config_db; + + // Reboot thread signaling. + swss::NotificationProducer m_rebootbeRequestChannel; + swss::Select m_s; + swss::NotificationConsumer m_rebootbeReponseChannel; + + // Module under test. + std::unique_ptr m_rebootbe_thread; + RebootBE m_rebootbe; + +}; + +class RebootBETest : public RebootBETestWithoutStop { + protected: + ~RebootBETest() { + m_rebootbe.Stop(); + m_rebootbe_thread->join(); + } +}; + +// Test fixture to skip through the startup sequence into the main loop. +// Param indicates if RebootBE should be initialized into a state where the +// system came up in warmboot. +class RebootBEAutoStartTest : public RebootBETest, + public ::testing::WithParamInterface { + protected: + RebootBEAutoStartTest() { + //force_warm_start_state(GetParam()); + + /* if (GetParam()) { + EXPECT_CALL(*m_init_thread, Start()) + .WillOnce(Return(swss::StatusCode::SWSS_RC_SUCCESS)); + EXPECT_CALL(*m_init_thread, Join()).WillOnce(Return(true)); + EXPECT_CALL(*m_init_thread, GetResponse()) + .WillOnce(Return(default_running_status())) + .WillRepeatedly(Return(default_done_status())); + } else { + EXPECT_CALL(*m_init_thread, GetResponse()) + .WillRepeatedly(Return(default_not_started_status())); + } */ + + start_rebootbe(); + +/* if (GetParam()) { + get_stack_unfrozen_select().notify(); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + get_init_done_select().notify(); + } */ + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + EXPECT_EQ(m_rebootbe.GetCurrentStatus(), RebootBE::RebManagerStatus::IDLE); + } +}; + + + +// Normal operation testing. +TEST_P(RebootBEAutoStartTest, NonExistentMessage) { + swss::NotificationConsumer consumer(&m_db, + REBOOT_RESPONSE_NOTIFICATION_CHANNEL); + + // No "MESSAGE" in field/values + SendRebootRequest("Reboot", "StatusCode", "field1", "field1_value"); + EXPECT_EQ(m_rebootbe.GetCurrentStatus(), RebootBE::RebManagerStatus::IDLE); + + std::string op, data; + std::vector ret_values; + GetNotificationResponse(consumer, op, data, ret_values); + + EXPECT_THAT(op, StrEq("Reboot")); + EXPECT_THAT( + data, + StrEq(swss::statusCodeToStr(swss::StatusCode::SWSS_RC_INVALID_PARAM))); +} + +TEST_P(RebootBEAutoStartTest, TestCancelReboot) { + swss::NotificationConsumer consumer(&m_db, + REBOOT_RESPONSE_NOTIFICATION_CHANNEL); + + SendRebootRequest("CancelReboot", "StatusCode", DATA_TUPLE_KEY, + "json cancelreboot request"); + EXPECT_EQ(m_rebootbe.GetCurrentStatus(), RebootBE::RebManagerStatus::IDLE); + + std::string op, data; + std::vector ret_values; + GetNotificationResponse(consumer, op, data, ret_values); + + EXPECT_THAT(op, StrEq("CancelReboot")); + EXPECT_THAT( + data, + StrEq(swss::statusCodeToStr(swss::StatusCode::SWSS_RC_UNIMPLEMENTED))); +} + +TEST_P(RebootBEAutoStartTest, TestUnrecognizedOP) { + swss::NotificationConsumer consumer(&m_db, + REBOOT_RESPONSE_NOTIFICATION_CHANNEL); + + SendRebootRequest("NonOp", "StatusCode", DATA_TUPLE_KEY, "invalid op code"); + EXPECT_EQ(m_rebootbe.GetCurrentStatus(), RebootBE::RebManagerStatus::IDLE); + + std::string op, data; + std::vector ret_values; + GetNotificationResponse(consumer, op, data, ret_values); + + EXPECT_THAT(op, StrEq("NonOp")); + EXPECT_THAT( + data, + StrEq(swss::statusCodeToStr(swss::StatusCode::SWSS_RC_INVALID_PARAM))); +} + +TEST_P(RebootBEAutoStartTest, TestColdRebootDbusToCompletion) { + DbusInterface::DbusResponse dbus_response{ + DbusInterface::DbusStatus::DBUS_SUCCESS, ""}; + EXPECT_CALL(m_dbus_interface, Reboot(_)) + .Times(3) + .WillRepeatedly(Return(dbus_response)); + + overwrite_reboot_timeout(1); + RebootRequest request; + request.set_method(RebootMethod::COLD); + start_reboot_via_rpc(request); + + std::this_thread::sleep_for(std::chrono::milliseconds(TENTH_SECOND_MS)); + EXPECT_EQ(m_rebootbe.GetCurrentStatus(), + RebootBE::RebManagerStatus::COLD_REBOOT_IN_PROGRESS); + sleep(TWO_SECONDS); + + EXPECT_EQ(m_rebootbe.GetCurrentStatus(), RebootBE::RebManagerStatus::IDLE); + gnoi::system::RebootStatusResponse response = do_reboot_status_rpc(); + EXPECT_THAT(response, ActiveCountMethod(false, 1, RebootMethod::COLD)); + EXPECT_THAT(response, + IsStatus(RebootStatus_Status::RebootStatus_Status_STATUS_FAILURE, + "platform failed to reboot")); + + start_reboot_via_rpc(request); + sleep(TWO_SECONDS); + + start_reboot_via_rpc(request); + sleep(TWO_SECONDS); + + response = do_reboot_status_rpc(); + // Verifiy count is 3 after three reboot attempts. + EXPECT_THAT(response, ActiveCountMethod(false, 3, RebootMethod::COLD)); + EXPECT_THAT(response, + IsStatus(RebootStatus_Status::RebootStatus_Status_STATUS_FAILURE, + "platform failed to reboot")); +} + +TEST_P(RebootBEAutoStartTest, TestColdBootSigterm) { + sigterm_requested = true; + set_mock_defaults(); + overwrite_reboot_timeout(1); + + RebootRequest request; + request.set_method(RebootMethod::COLD); + start_reboot_via_rpc(request); + + sleep(ONE_SECOND); + + EXPECT_EQ(m_rebootbe.GetCurrentStatus(), RebootBE::RebManagerStatus::IDLE); + gnoi::system::RebootStatusResponse second_resp = do_reboot_status_rpc(); + EXPECT_THAT(second_resp, ActiveCountMethod(false, 1, RebootMethod::COLD)); + EXPECT_THAT( + second_resp, + IsStatus(RebootStatus_Status::RebootStatus_Status_STATUS_UNKNOWN, "")); +} + +TEST_P(RebootBEAutoStartTest, TestColdBootDbusError) { + // Return FAIL from dbus reboot call. + DbusInterface::DbusResponse dbus_response{ + DbusInterface::DbusStatus::DBUS_FAIL, "dbus reboot failed"}; + EXPECT_CALL(m_dbus_interface, Reboot(_)) + .Times(1) + .WillOnce(Return(dbus_response)); + + RebootRequest request; + request.set_method(RebootMethod::COLD); + start_reboot_via_rpc(request); + + sleep(TWO_SECONDS); + + EXPECT_EQ(m_rebootbe.GetCurrentStatus(), RebootBE::RebManagerStatus::IDLE); + gnoi::system::RebootStatusResponse second_resp = do_reboot_status_rpc(); + EXPECT_THAT(second_resp, ActiveCountMethod(false, 1, RebootMethod::COLD)); + EXPECT_THAT(second_resp, + IsStatus(RebootStatus_Status::RebootStatus_Status_STATUS_FAILURE, + "dbus reboot failed")); +} + +TEST_P(RebootBEAutoStartTest, TestStopDuringColdBoot) { + set_mock_defaults(); + + RebootRequest request; + request.set_method(RebootMethod::COLD); + start_reboot_via_rpc(request); + std::this_thread::sleep_for(std::chrono::milliseconds(TENTH_SECOND_MS)); + EXPECT_EQ(m_rebootbe.GetCurrentStatus(), + RebootBE::RebManagerStatus::COLD_REBOOT_IN_PROGRESS); + + send_stop_reboot_thread(); + std::this_thread::sleep_for(std::chrono::milliseconds(TENTH_SECOND_MS)); + EXPECT_EQ(m_rebootbe.GetCurrentStatus(), RebootBE::RebManagerStatus::IDLE); + + gnoi::system::RebootStatusResponse response = do_reboot_status_rpc(); + EXPECT_THAT(response, ActiveCountMethod(false, 1, RebootMethod::COLD)); + EXPECT_THAT( + response, + IsStatus(RebootStatus_Status::RebootStatus_Status_STATUS_UNKNOWN, "")); +} + +TEST_P(RebootBEAutoStartTest, TestInvalidJsonRebootRequest) { + std::string json_request = "abcd"; + NotificationResponse response = handle_reboot_request(json_request); + EXPECT_EQ(swss::StatusCode::SWSS_RC_INTERNAL, response.status); +} + +INSTANTIATE_TEST_SUITE_P(TestWithStartupWarmbootEnabledState, + RebootBEAutoStartTest, testing::Values(true, false)); + +} // namespace rebootbackend diff --git a/src/sonic-framework/tests/redis_utils_test.cpp b/src/sonic-framework/tests/redis_utils_test.cpp new file mode 100644 index 000000000000..00864059a21e --- /dev/null +++ b/src/sonic-framework/tests/redis_utils_test.cpp @@ -0,0 +1,23 @@ +#include "redis_utils.h" + +#include +#include +#include + +#include +#include +#include + +#include "select.h" +#include "table.h" +#include "test_utils_common.h" +#include "timestamp.h" + +namespace rebootbackend { + +using ::testing::AllOf; +using ::testing::HasSubstr; +using ::testing::StrEq; + + +} // namespace rebootbackend diff --git a/src/sonic-framework/tests/test_main.cpp b/src/sonic-framework/tests/test_main.cpp new file mode 100644 index 000000000000..693d88f181ed --- /dev/null +++ b/src/sonic-framework/tests/test_main.cpp @@ -0,0 +1,7 @@ + +#include "gtest/gtest.h" + +int main(int argc, char* argv[]) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/src/sonic-framework/tests/test_utils_common.cpp b/src/sonic-framework/tests/test_utils_common.cpp new file mode 100644 index 000000000000..ef0088b7bf05 --- /dev/null +++ b/src/sonic-framework/tests/test_utils_common.cpp @@ -0,0 +1,30 @@ +#include "test_utils_common.h" + +#include +#include + +#include +#include + +#include "dbconnector.h" +#include "notificationconsumer.h" +#include "redis_utils.h" +#include "select.h" +#include "selectableevent.h" +#include "table.h" +#include "timestamp.h" + +namespace rebootbackend { + +void TestUtils::wait_for_finish(swss::Select &s, + swss::SelectableEvent &finished, + long timeout_seconds) { + swss::Selectable *sel; + int ret; + + ret = s.select(&sel, timeout_seconds * 1000); + EXPECT_EQ(ret, swss::Select::OBJECT); + EXPECT_EQ(sel, &finished); +} + +} // namespace rebootbackend diff --git a/src/sonic-framework/tests/test_utils_common.h b/src/sonic-framework/tests/test_utils_common.h new file mode 100644 index 000000000000..fbb7c373acd7 --- /dev/null +++ b/src/sonic-framework/tests/test_utils_common.h @@ -0,0 +1,44 @@ +#pragma once +#include +#include + +#include +#include + +#include "dbconnector.h" +#include "notificationconsumer.h" +#include "select.h" +#include "selectableevent.h" + +namespace rebootbackend { + +class TestUtils { + public: + static void wait_for_finish(swss::Select &s, swss::SelectableEvent &finished, + long timeout_seconds); + + static std::string wait_for_state_verification_trigger( + swss::NotificationConsumer &nc, long timeout_seconds, bool freeze); + + static void confirm_no_state_verification_trigger( + swss::NotificationConsumer &nc, long timeout_seconds); + + static void populate_registration_table( + swss::DBConnector &db, const std::string &key, const bool &stop_on_freeze, + const bool &freeze, const bool &checkpoint, const bool &reconciliation); + + static void populate_restart_table_state(swss::DBConnector &db, + const std::string &app_name, + const std::string &state); + + static void write_state_verification_result(swss::DBConnector &db, + const std::string &key, + const std::string &status, + const std::string ×tamp); + + static void clear_tables(swss::DBConnector &db); + + +}; + +} // namespace rebootbackend