/*------------------------------------------------------------------ * ssg-test.cc - systemd-sonic-generator Unit Test * * Initial: Apr 2021 * * Copyright (c) 2021 by Cisco Systems, Inc. *------------------------------------------------------------------ */ #include #include #include #include #include #include #include #include "systemd-sonic-generator.h" namespace fs = boost::filesystem; namespace SSGTest { #define IS_MULTI_ASIC(x) ((x) > 1) #define IS_SINGLE_ASIC(x) ((x) <= 1) #define NUM_UNIT_FILES 9 /* * This test class uses following directory hierarchy for input and output * data for systemd-sonic-generator. * * tests/ssg-test/ --- Test data directory * | * |---generated_services.conf * |---machine.conf (systemd-sonic-generator fetch platform from here) * |---systemd/ * | |--- *.service (Test unit files are copied from * | tests/testfiles/ to here) * |----test_platform/ (test platform) * | |---asic.conf * | * |----generator/ (Output Directory) * */ const std::string TEST_ROOT_DIR = "tests/ssg-test/"; const std::string TEST_UNIT_FILE_PREFIX = TEST_ROOT_DIR + "systemd/"; const std::string TEST_ASIC_CONF_FORMAT = TEST_ROOT_DIR + "%s/asic.conf"; const std::string TEST_PLATFORM_CONF_FORMAT = TEST_ROOT_DIR + "%s/platform.json"; const std::string TEST_MACHINE_CONF = TEST_ROOT_DIR + "machine.conf"; const std::string TEST_PLATFORM_DIR = TEST_ROOT_DIR + "test_platform/"; const std::string TEST_ASIC_CONF = TEST_PLATFORM_DIR + "asic.conf"; const std::string TEST_PLATFORM_CONF = TEST_PLATFORM_DIR + "platform.json"; const std::string TEST_OUTPUT_DIR = TEST_ROOT_DIR + "generator/"; const std::string TEST_CONFIG_FILE = TEST_ROOT_DIR + "generated_services.conf"; const std::string TEST_UNIT_FILES = "tests/testfiles/"; /* Input data for generated_services.conf */ const std::vector generated_services = { "multi_inst_a.service", /* Single instance of a multi asic service a */ "multi_inst_a@.service", /* Multi-instance of a multi asic service a */ "multi_inst_b@.service", /* Multi-instance of a multi asic service b */ "single_inst.service", /* A single instance service */ "test.service", /* A single instance test service to test dependency creation */ "test.timer", /* A timer service */ "midplane-network.service", /* A midplane network service for smart switch*/ "database.service", /* A database service*/ "database@.service", /* A database service for multi instances */ }; static std::mutex g_ssg_test_mutex; class SystemdSonicGeneratorFixture : public testing::Test { protected: /* Save global variables before running tests */ virtual void SetUp() { /* one test runs at a time */ g_ssg_test_mutex.lock(); unit_file_prefix_ = g_unit_file_prefix; config_file_ = g_config_file; machine_config_file_ = g_machine_config_file; asic_conf_format_ = g_asic_conf_format; } /* Restore global vars */ virtual void TearDown() { g_unit_file_prefix = unit_file_prefix_; g_config_file = config_file_; g_machine_config_file = machine_config_file_; g_asic_conf_format = asic_conf_format_; g_ssg_test_mutex.unlock(); } private: const char* unit_file_prefix_; const char* config_file_; const char* machine_config_file_; const char* asic_conf_format_; }; /* * class SsgFunctionTest * Implements functions to execute functional level tests. */ class SsgFunctionTest : public SystemdSonicGeneratorFixture { protected: /* This function generates the generated_services.conf file */ void generate_generated_services_conf() { FILE* fp = fopen(TEST_CONFIG_FILE.c_str(), "w"); ASSERT_NE(fp, nullptr); for (std::string str : generated_services) { fputs(str.c_str(), fp); fputs("\n", fp); } fclose(fp); } /* copy files from src_dir to dest_dir */ void copyfiles(const char* src_dir, const char* dest_dir) { // Iterate through the source directory for (fs::directory_iterator file(src_dir); file != fs::directory_iterator(); ++file) { try { fs::path current(file->path()); if(!fs::is_directory(current)) { /* Copy file */ fs::copy_file( current, dest_dir / current.filename()); } } catch(fs::filesystem_error const & e) { std:: cerr << e.what() << '\n'; } } } /* Save global variables before running tests */ virtual void SetUp() { FILE* fp; SystemdSonicGeneratorFixture::SetUp(); /* Setup Input and Output directories and files */ fs::path path{TEST_UNIT_FILE_PREFIX.c_str()}; fs::create_directories(path); path = fs::path(TEST_OUTPUT_DIR.c_str()); fs::create_directories(path); path = fs::path(TEST_PLATFORM_DIR.c_str()); fs::create_directories(path); fp = fopen(TEST_MACHINE_CONF.c_str(), "w"); ASSERT_NE(fp, nullptr); fputs("onie_platform=test_platform", fp); fclose(fp); generate_generated_services_conf(); copyfiles(TEST_UNIT_FILES.c_str(), TEST_UNIT_FILE_PREFIX.c_str()); } /* Restore global vars */ virtual void TearDown() { /* Delete ssg_test directory */ EXPECT_TRUE(fs::exists(TEST_ROOT_DIR.c_str())); fs::path path{TEST_ROOT_DIR.c_str()}; fs::remove_all(path); SystemdSonicGeneratorFixture::TearDown(); } private: }; struct SsgMainConfig { int num_asics; bool is_smart_switch_npu; bool is_smart_switch_dpu; int num_dpus; SsgMainConfig() { num_asics = 0; is_smart_switch_npu = false; is_smart_switch_dpu = false; num_dpus = 0; } }; /* * class SsgMainTest * Implements functions to test ssg_main routine. */ class SsgMainTest : public SsgFunctionTest { protected: /* Retrun true if string belongs to a multi instance service */ bool is_multi_instance(const std::string str) { return (str.find("@") != std::string::npos) ? true : false; } /* Returns true if it is a timer service */ bool is_timer_service(const std::string str) { return (str.find(".timer") != std::string::npos) ? true : false; } /* Find a string in a file */ bool find_string_in_file(std::string str, std::string file_name, int num_asics) { bool found = false; std::string line; std::ifstream file(TEST_UNIT_FILE_PREFIX + file_name); while (getline(file, line) && !found) { if (str == line) { found = true; } } return found; } /* This function validates if a given dependency list for an unit file * exists in the unit file as per expected_result. The items in the list * should exist if expected_result is true. */ void validate_output_dependency_list(std::vector strs, std::string target, bool expected_result, int num_asics) { for (std::string str : strs) { bool finished = false; for (int i = 0 ; i < num_asics && !finished; ++i) { auto str_t = str; if (is_multi_instance(str)) { /* insert instance id in string */ str_t = (boost::format{str} % i).str(); } else { /* Run once for single instance */ finished = true; } EXPECT_EQ(find_string_in_file(str_t, target, num_asics), expected_result) << "Error validating " + str_t + " in " + target; } } } /* This function validates if unit file paths in the provided * list strs exists or not as per expected_result. The unit files * should exist if expected_result is true. */ void validate_output_unit_files(std::vector strs, std::string target, bool expected_result, int num_instances) { for (std::string str : strs) { bool finished = false; for (int i = 0 ; i < num_instances && !finished; ++i) { auto str_t = str; if (is_multi_instance(str)) { /* insert instance id in string */ str_t = (boost::format{str} % i).str(); } else { /* Run once for single instance */ finished = true; } fs::path path{TEST_OUTPUT_DIR + target + "/" + str_t}; EXPECT_EQ(fs::exists(path), expected_result) << "Failed validation: " << path; } } } /* * This function validates the generated dependencies in a Unit File. */ void validate_depedency_in_unit_file(const SsgMainConfig &cfg) { std::string test_service = "test.service"; /* Validate Unit file dependency creation for multi instance * services. These entries should be present for multi asic * system but not present for single asic system. */ validate_output_dependency_list(multi_asic_dependency_list, test_service, IS_MULTI_ASIC(cfg.num_asics), cfg.num_asics); /* This section handles a tricky scenario. * When the number of DPUs (Data Processing Units) is greater than 0, * the dependency list will be split. Otherwise, it remains in one line. * Despite the split, the final result remains equivalent. */ if (cfg.num_dpus > 0) { /* Validate Unit file dependency creation for single instance * services. These entries should not be present for multi asic * system but present for single asic system. */ validate_output_dependency_list(single_asic_dependency_list_split, test_service, IS_SINGLE_ASIC(cfg.num_asics), cfg.num_asics); } else { /* Validate Unit file dependency creation for single instance * services. These entries should not be present for multi asic * system but present for single asic system. */ validate_output_dependency_list(single_asic_dependency_list, test_service, IS_SINGLE_ASIC(cfg.num_asics), cfg.num_asics); } /* Validate Unit file dependency creation for single instance * common services. These entries should not be present for multi * and single asic system. */ validate_output_dependency_list(common_dependency_list, test_service, true, cfg.num_asics); /* Validate Unit file dependency creation for smart switch * services. These entries should not be present for npu * and dpu of smart switch. */ validate_output_dependency_list(smart_switch_dependency_list, "database.service", cfg.is_smart_switch_dpu || cfg.is_smart_switch_npu, cfg.num_asics); validate_output_dependency_list(smart_switch_dependency_list, "database@.service", cfg.is_smart_switch_npu, cfg.num_asics); } /* * This function validates the list of generated Service Unit Files. */ void validate_service_file_generated_list(const SsgMainConfig &cfg) { std::string test_target = "multi-user.target.wants"; validate_output_unit_files(multi_asic_service_list, test_target, IS_MULTI_ASIC(cfg.num_asics), cfg.num_asics); validate_output_unit_files(single_asic_service_list, test_target, IS_SINGLE_ASIC(cfg.num_asics), cfg.num_asics); validate_output_unit_files(common_service_list, test_target, true, cfg.num_asics); validate_output_unit_files(smart_switch_service_list, test_target, cfg.is_smart_switch_dpu || cfg.is_smart_switch_npu, cfg.num_dpus); } /* ssg_main test routine. * input: num_asics number of asics */ void ssg_main_test(const SsgMainConfig &cfg) { FILE* fp; std::vector argv_; std::vector arguments = { "ssg_main", TEST_OUTPUT_DIR.c_str() }; std::string num_asic_str = "NUM_ASIC=" + std::to_string(cfg.num_asics); std::string unit_file_path = fs::current_path().string() + "/" +TEST_UNIT_FILE_PREFIX; g_unit_file_prefix = unit_file_path.c_str(); g_config_file = TEST_CONFIG_FILE.c_str(); g_machine_config_file = TEST_MACHINE_CONF.c_str(); g_asic_conf_format = TEST_ASIC_CONF_FORMAT.c_str(); g_platform_file_format = TEST_PLATFORM_CONF_FORMAT.c_str(); /* Set NUM_ASIC value in asic.conf */ fp = fopen(TEST_ASIC_CONF.c_str(), "w"); ASSERT_NE(fp, nullptr); fputs(num_asic_str.c_str(), fp); fclose(fp); /* Set platform file for smart switch */ if (cfg.is_smart_switch_dpu || cfg.is_smart_switch_npu) { nlohmann::json platform_config; if (cfg.is_smart_switch_dpu) { ASSERT_EQ(cfg.num_dpus, 0); ASSERT_EQ(cfg.is_smart_switch_npu, false); platform_config["DPU"] = nlohmann::json::object(); } else if (cfg.is_smart_switch_npu) { ASSERT_EQ(cfg.is_smart_switch_dpu, false); nlohmann::json::array_t dpus; for (int i = 0; i < cfg.num_dpus; i++) { dpus.push_back(nlohmann::json::object()); } platform_config["DPUS"] = dpus; } fp = fopen(TEST_PLATFORM_CONF.c_str(), "w"); ASSERT_NE(fp, nullptr); fputs(platform_config.dump().c_str(), fp); fclose(fp); } /* Create argv list for ssg_main. */ for (const auto& arg : arguments) { argv_.push_back((char*)arg.data()); } argv_.push_back(nullptr); /* Call ssg_main */ EXPECT_EQ(ssg_main(argv_.size(), argv_.data()), 0); /* Validate systemd service template creation. */ validate_service_file_generated_list(cfg); /* Validate Test Unit file for dependency creation. */ validate_depedency_in_unit_file(cfg); } /* Save global variables before running tests */ virtual void SetUp() { SsgFunctionTest::SetUp(); } /* Restore global vars */ virtual void TearDown() { SsgFunctionTest::TearDown(); } private: static const std::vector single_asic_service_list; static const std::vector multi_asic_service_list; static const std::vector common_service_list; static const std::vector smart_switch_service_list; static const std::vector single_asic_dependency_list; static const std::vector single_asic_dependency_list_split; static const std::vector multi_asic_dependency_list; static const std::vector common_dependency_list; static const std::vector smart_switch_dependency_list; }; /* * The following list defines the Service unit files symlinks generated by * Systemd sonic generator for single and multi asic systems. The test case * use these lists to check for presence/absence of unit files based on * num_asics value. */ /* Systemd service Unit file list for single asic only system */ const std::vector SsgMainTest::single_asic_service_list = { "multi_inst_b.service", }; /* Systemd service Unit file list for multi asic only system. * %1% is formatter for boost::format API and replaced by asic num. */ const std::vector SsgMainTest::multi_asic_service_list = { "multi_inst_a@%1%.service", "multi_inst_b@%1%.service", "database@%1%.service", }; /* Common Systemd service Unit file list for single and multi asic system. */ const std::vector SsgMainTest::common_service_list = { "multi_inst_a.service", "single_inst.service", "test.service", "database.service", }; /* Common Systemd service Unit file list for single and multi asic system. */ const std::vector SsgMainTest::smart_switch_service_list = { "database@dpu%1%.service", "midplane-network.service", }; /* * The following list defines the systemd dependencies in a unit file to be * varified for single and multi asic systems. Based on num_asics and type of * service listed as dependency in Unit file, systemd sonic generator modifies * the original unit file, if required, for multi asic system. * For example: if test.service file defines a dependency "After=multi_inst_a.service", * as multi_inst_a.service is a multi instance service, * for a system with 2 asics, systemd sonic generator shall modify * test.service to include following dependency strings: * "After=multi_inst_a@0.service" * After=multi_inst_a@1.service" */ /* Systemd service Unit file dependency entries for Single asic system. */ const std::vector SsgMainTest::single_asic_dependency_list = { "After=multi_inst_a.service multi_inst_b.service", }; /* Systemd service Unit file dependency entries for Single asic system. */ const std::vector SsgMainTest::single_asic_dependency_list_split = { "After=multi_inst_a.service", "After=multi_inst_b.service", }; /* Systemd service Unit file dependency entries for multi asic system. */ const std::vector SsgMainTest::multi_asic_dependency_list = { "After=multi_inst_a@%1%.service", "After=multi_inst_b@%1%.service", }; /* Common Systemd service Unit file dependency entries for single and multi asic * systems. */ const std::vector SsgMainTest::common_dependency_list = { "Before=single_inst.service", }; const std::vector SsgMainTest::smart_switch_dependency_list = { "Requires=midplane-network.service", "After=midplane-network.service", }; /* Test get functions for global vasr*/ TEST_F(SystemdSonicGeneratorFixture, get_global_vars) { EXPECT_EQ(g_unit_file_prefix, nullptr); EXPECT_STREQ(get_unit_file_prefix(), UNIT_FILE_PREFIX); g_unit_file_prefix = TEST_UNIT_FILE_PREFIX.c_str(); EXPECT_STREQ(get_unit_file_prefix(), TEST_UNIT_FILE_PREFIX.c_str()); EXPECT_EQ(g_config_file, nullptr); EXPECT_STREQ(get_config_file(), CONFIG_FILE); g_config_file = TEST_CONFIG_FILE.c_str(); EXPECT_STREQ(get_config_file(), TEST_CONFIG_FILE.c_str()); EXPECT_EQ(g_machine_config_file, nullptr); EXPECT_STREQ(get_machine_config_file(), MACHINE_CONF_FILE); g_machine_config_file = TEST_MACHINE_CONF.c_str(); EXPECT_STREQ(get_machine_config_file(), TEST_MACHINE_CONF.c_str()); EXPECT_EQ(g_asic_conf_format, nullptr); EXPECT_STREQ(get_asic_conf_format(), ASIC_CONF_FORMAT); g_asic_conf_format = TEST_ASIC_CONF_FORMAT.c_str(); EXPECT_STREQ(get_asic_conf_format(), TEST_ASIC_CONF_FORMAT.c_str()); } TEST_F(SystemdSonicGeneratorFixture, global_vars) { EXPECT_EQ(g_unit_file_prefix, nullptr); EXPECT_STREQ(get_unit_file_prefix(), UNIT_FILE_PREFIX); EXPECT_EQ(g_config_file, nullptr); EXPECT_STREQ(get_config_file(), CONFIG_FILE); EXPECT_EQ(g_machine_config_file, nullptr); EXPECT_STREQ(get_machine_config_file(), MACHINE_CONF_FILE); } /* TEST machine/unit/config if file is missing */ TEST_F(SsgFunctionTest, missing_file) { EXPECT_TRUE(fs::exists(TEST_MACHINE_CONF.c_str())); EXPECT_TRUE(fs::exists(TEST_UNIT_FILE_PREFIX.c_str())); EXPECT_TRUE(fs::exists(TEST_OUTPUT_DIR.c_str())); EXPECT_TRUE(fs::exists(TEST_PLATFORM_DIR.c_str())); } /* TEST insert_instance_number() */ TEST_F(SsgFunctionTest, insert_instance_number) { char input[] = "test@.service"; for (int i = 0; i <= 100; ++i) { std::string out = "test@" + std::to_string(i) + ".service"; char* ret = insert_instance_number(input, i, ""); ASSERT_NE(ret, nullptr); EXPECT_STREQ(ret, out.c_str()); } } /* TEST get_num_of_asic() */ TEST_F(SsgFunctionTest, get_num_of_asic) { FILE* fp; g_machine_config_file = TEST_MACHINE_CONF.c_str(); g_asic_conf_format = TEST_ASIC_CONF_FORMAT.c_str(); fp = fopen(TEST_ASIC_CONF.c_str(), "w"); ASSERT_NE(fp, nullptr); fputs("NUM_ASIC=1", fp); fclose(fp); EXPECT_EQ(get_num_of_asic(), 1); fp = fopen(TEST_ASIC_CONF.c_str(), "w"); ASSERT_NE(fp, nullptr); fputs("NUM_ASIC=10", fp); fclose(fp); EXPECT_EQ(get_num_of_asic(), 10); fp = fopen(TEST_ASIC_CONF.c_str(), "w"); ASSERT_NE(fp, nullptr); fputs("NUM_ASIC=40", fp); fclose(fp); EXPECT_EQ(get_num_of_asic(), 40); } /* TEST get_unit_files()*/ TEST_F(SsgFunctionTest, get_unit_files) { g_unit_file_prefix = TEST_UNIT_FILE_PREFIX.c_str(); g_config_file = TEST_CONFIG_FILE.c_str(); char* unit_files[NUM_UNIT_FILES] = { NULL }; int num_unit_files = get_unit_files(unit_files); // Exclude the midplane-network.service which is only used for smart switch auto non_smart_switch_generated_services = generated_services; non_smart_switch_generated_services.erase( std::remove(non_smart_switch_generated_services.begin(), non_smart_switch_generated_services.end(), "midplane-network.service"), non_smart_switch_generated_services.end()); EXPECT_EQ(num_unit_files, non_smart_switch_generated_services.size()); for (std::string service : non_smart_switch_generated_services) { bool found = false; for (auto& unit_file : unit_files) { if (unit_file == NULL) { continue; } if(unit_file == service) { found = true; break; } } EXPECT_TRUE(found) << "unit file not found: " << service; } } /* TEST ssg_main() argv error */ TEST_F(SsgMainTest, ssg_main_argv) { FILE* fp; std::vector argv_; std::vector arguments = { "ssg_main", }; /* Create argv list for ssg_main. */ for (const auto& arg : arguments) { argv_.push_back((char*)arg.data()); } /* Call ssg_main */ EXPECT_EQ(ssg_main(argv_.size(), argv_.data()), 1); } /* TEST ssg_main() single asic */ TEST_F(SsgMainTest, ssg_main_single_npu) { SsgMainConfig cfg; cfg.num_asics = 1; ssg_main_test(cfg); } /* TEST ssg_main() multi(10) asic */ TEST_F(SsgMainTest, ssg_main_10_npu) { SsgMainConfig cfg; cfg.num_asics = 10; ssg_main_test(cfg); } /* TEST ssg_main() multi(40) asic */ TEST_F(SsgMainTest, ssg_main_40_npu) { SsgMainConfig cfg; cfg.num_asics = 40; ssg_main_test(cfg); } TEST_F(SsgMainTest, ssg_main_smart_switch_npu) { SsgMainConfig cfg; cfg.num_asics = 1; cfg.is_smart_switch_npu = true; cfg.num_dpus = 8; ssg_main_test(cfg); } TEST_F(SsgMainTest, ssg_main_smart_switch_dpu) { SsgMainConfig cfg; cfg.num_asics = 1; cfg.is_smart_switch_dpu = true; ssg_main_test(cfg); } TEST_F(SsgMainTest, ssg_main_smart_switch_double_execution) { SsgMainConfig cfg; cfg.num_asics = 1; cfg.is_smart_switch_npu = true; cfg.num_dpus = 8; ssg_main_test(cfg); ssg_main_test(cfg); } } int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }