diff options
Diffstat (limited to 'src/lib/process/tests/d_controller_unittests.cc')
-rw-r--r-- | src/lib/process/tests/d_controller_unittests.cc | 470 |
1 files changed, 470 insertions, 0 deletions
diff --git a/src/lib/process/tests/d_controller_unittests.cc b/src/lib/process/tests/d_controller_unittests.cc new file mode 100644 index 0000000..4d7b73c --- /dev/null +++ b/src/lib/process/tests/d_controller_unittests.cc @@ -0,0 +1,470 @@ +// Copyright (C) 2013-2021 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <kea_version.h> + +#include <asiolink/testutils/timed_signal.h> +#include <cc/command_interpreter.h> +#include <process/testutils/d_test_stubs.h> + +#include <boost/date_time/posix_time/posix_time.hpp> +#include <gtest/gtest.h> + +#include <sstream> + +using namespace isc::asiolink::test; +using namespace boost::posix_time; + +namespace isc { +namespace process { + +/// @brief Test fixture class for testing DControllerBase class. This class +/// derives from DControllerTest and wraps a DStubController. DStubController +/// has been constructed to exercise DControllerBase. +class DStubControllerTest : public DControllerTest { +public: + /// @brief Constructor. + /// Note the constructor passes in the static DStubController instance + /// method. + DStubControllerTest() : DControllerTest(DStubController::instance) { + controller_ = boost::dynamic_pointer_cast<DStubController> + (DControllerTest:: + getController()); + } + + /// @brief The controller. + DStubControllerPtr controller_; +}; + +/// @brief Basic Controller instantiation testing. +/// Verifies that the controller singleton gets created and that the +/// basic derivation from the base class is intact. +TEST_F(DStubControllerTest, basicInstanceTesting) { + // Verify that the singleton exists and it is the correct type. + DControllerBasePtr& controller = DControllerTest::getController(); + ASSERT_TRUE(controller); + ASSERT_NO_THROW(boost::dynamic_pointer_cast<DStubController>(controller)); + + // Verify that controller's app name is correct. + EXPECT_TRUE(checkAppName(DStubController::stub_app_name_)); + + // Verify that controller's bin name is correct. + EXPECT_TRUE(checkBinName(DStubController::stub_bin_name_)); + + // Verify that controller's IOService exists. + EXPECT_TRUE(checkIOService()); + + // Verify that the Process does NOT exist. + EXPECT_FALSE(checkProcess()); +} + +/// @brief Tests basic command line processing. +/// Verifies that: +/// 1. Standard command line options are supported. +/// 2. Custom command line options are supported. +/// 3. Invalid options are detected. +/// 4. Extraneous command line information is detected. +TEST_F(DStubControllerTest, commandLineArgs) { + + // Verify that verbose flag is false initially. + EXPECT_TRUE(checkVerbose(false)); + + // Verify that standard options can be parsed without error. + char* argv[] = { const_cast<char*>("progName"), + const_cast<char*>("-c"), + const_cast<char*>("cfgName"), + const_cast<char*>("-d") }; + int argc = 4; + EXPECT_NO_THROW(parseArgs(argc, argv)); + + // Verify that verbose is true. + EXPECT_TRUE(checkVerbose(true)); + + // Verify configuration file name is correct + EXPECT_TRUE(checkConfigFileName("cfgName")); + + // Verify that the custom command line option is parsed without error. + char xopt[3] = "- "; + xopt[1] = *DStubController::stub_option_x_; + char* argv1[] = { const_cast<char*>("progName"), xopt}; + argc = 2; + EXPECT_NO_THROW (parseArgs(argc, argv1)); + + // Verify that an unknown option is detected. + char* argv2[] = { const_cast<char*>("progName"), + const_cast<char*>("-bs") }; + argc = 2; + EXPECT_THROW (parseArgs(argc, argv2), InvalidUsage); + + // Verify that extraneous information is detected. + char* argv3[] = { const_cast<char*>("progName"), + const_cast<char*>("extra"), + const_cast<char*>("information") }; + argc = 3; + EXPECT_THROW (parseArgs(argc, argv3), InvalidUsage); +} + +/// @brief Tests application process creation and initialization. +/// Verifies that: +/// 1. An error during process creation is handled. +/// 2. A NULL returned by process creation is handled. +/// 3. An error during process initialization is handled. +/// 4. Process can be successfully created and initialized. +TEST_F(DStubControllerTest, initProcessTesting) { + // Verify that a failure during process creation is caught. + SimFailure::set(SimFailure::ftCreateProcessException); + EXPECT_THROW(initProcess(), DControllerBaseError); + EXPECT_FALSE(checkProcess()); + + // Verify that a NULL returned by process creation is handled. + SimFailure::set(SimFailure::ftCreateProcessNull); + EXPECT_THROW(initProcess(), DControllerBaseError); + EXPECT_FALSE(checkProcess()); + + // Re-create controller, verify that we are starting clean + resetController(); + EXPECT_FALSE(checkProcess()); + + // Verify that an error during process initialization is handled. + SimFailure::set(SimFailure::ftProcessInit); + EXPECT_THROW(initProcess(), DProcessBaseError); + + // Re-create controller, verify that we are starting clean + resetController(); + EXPECT_FALSE(checkProcess()); + + // Verify that the application process can created and initialized. + ASSERT_NO_THROW(initProcess()); + EXPECT_TRUE(checkProcess()); +} + +/// @brief Tests launch handling of invalid command line. +/// This test launches with an invalid command line which should throw +/// an InvalidUsage. +TEST_F(DStubControllerTest, launchInvalidUsage) { + // Command line to run integrated + char* argv[] = { const_cast<char*>("progName"), + const_cast<char*>("-z") }; + int argc = 2; + + // Launch the controller in integrated mode. + EXPECT_THROW(launch(argc, argv), InvalidUsage); +} + +/// @brief Tests launch handling of failure in application process +/// initialization. This test launches with a valid command line but with +/// SimFailure set to fail during process creation. Launch should throw +/// ProcessInitError. +TEST_F(DStubControllerTest, launchProcessInitError) { + // Command line to run integrated + char* argv[] = { const_cast<char*>("progName"), + const_cast<char*>("-c"), + const_cast<char*>(DControllerTest::CFG_TEST_FILE), + const_cast<char*>("-d") }; + int argc = 4; + + // Launch the controller in stand alone mode. + SimFailure::set(SimFailure::ftCreateProcessException); + EXPECT_THROW(launch(argc, argv), ProcessInitError); +} + +/// @brief Tests launch and normal shutdown (stand alone mode). +/// This creates an interval timer to generate a normal shutdown and then +/// launches with a valid, command line, with a valid configuration file +/// and no simulated errors. +TEST_F(DStubControllerTest, launchNormalShutdown) { + // Write the valid, empty, config and then run launch() for 1000 ms + time_duration elapsed_time; + ASSERT_NO_THROW(runWithConfig("{}", 2000, elapsed_time)); + + // Verify that duration of the run invocation is the same as the + // timer duration. This demonstrates that the shutdown was driven + // by an io_service event and callback. + EXPECT_TRUE(elapsed_time.total_milliseconds() >= 1900 && + elapsed_time.total_milliseconds() <= 2300); +} + +/// @brief A variant of the launch and normal shutdown test using a callback. +TEST_F(DStubControllerTest, launchNormalShutdownWithCallback) { + // Write the valid, empty, config and then run launch() for 1000 ms + // Access to the internal state. + auto callback = [&] { EXPECT_FALSE(getProcess()->shouldShutdown()); }; + time_duration elapsed_time; + ASSERT_NO_THROW(runWithConfig("{}", 2000, + static_cast<const TestCallback&>(callback), + elapsed_time)); + + // Verify that duration of the run invocation is the same as the + // timer duration. This demonstrates that the shutdown was driven + // by an io_service event and callback. + EXPECT_TRUE(elapsed_time.total_milliseconds() >= 1900 && + elapsed_time.total_milliseconds() <= 2300); +} + +/// @brief Tests launch with an non-existing configuration file. +TEST_F(DStubControllerTest, nonExistingConfigFile) { + // command line to run standalone + char* argv[] = { const_cast<char*>("progName"), + const_cast<char*>("-c"), + const_cast<char*>("bogus-file"), + const_cast<char*>("-d") }; + int argc = 4; + + // Record start time, and invoke launch(). + EXPECT_THROW(launch(argc, argv), ProcessInitError); +} + +/// @brief Tests launch with configuration file argument but no file name +TEST_F(DStubControllerTest, missingConfigFileName) { + // command line to run standalone + char* argv[] = { const_cast<char*>("progName"), + const_cast<char*>("-c"), + const_cast<char*>("-d") }; + int argc = 3; + + // Record start time, and invoke launch(). + EXPECT_THROW(launch(argc, argv), ProcessInitError); +} + +/// @brief Tests launch with no configuration file argument +TEST_F(DStubControllerTest, missingConfigFileArgument) { + // command line to run standalone + char* argv[] = { const_cast<char*>("progName"), + const_cast<char*>("-d") }; + int argc = 2; + + // Record start time, and invoke launch(). + EXPECT_THROW(launch(argc, argv), LaunchError); +} + +/// @brief Tests launch with an operational error during application execution. +/// This test creates an interval timer to generate a runtime exception during +/// the process event loop. It launches with a valid, stand-alone command line +/// and no simulated errors. Launch should throw ProcessRunError. +TEST_F(DStubControllerTest, launchRuntimeError) { + // Use an asiolink IntervalTimer and callback to generate the + // shutdown invocation. (Note IntervalTimer setup is in milliseconds). + isc::asiolink::IntervalTimer timer(*getIOService()); + timer.setup(genFatalErrorCallback, 2000); + + // Write the valid, empty, config and then run launch() for 5000 ms + time_duration elapsed_time; + EXPECT_THROW(runWithConfig("{}", 5000, elapsed_time), ProcessRunError); + + // Verify that duration of the run invocation is the same as the + // timer duration. This demonstrates that the shutdown was driven + // by an io_service event and callback. + EXPECT_TRUE(elapsed_time.total_milliseconds() >= 1900 && + elapsed_time.total_milliseconds() <= 2300); +} + +/// @brief Configuration update event testing. +/// This really tests just the ability of the handlers to invoke the necessary +/// chain of methods and handle error conditions. Configuration parsing and +/// retrieval should be tested as part of the d2 configuration management +/// implementation. +/// This test verifies that: +/// 1. That a valid configuration update results in successful status return. +/// 2. That an application process error in configuration updating is handled +/// properly. +TEST_F(DStubControllerTest, configUpdateTests) { + int rcode = -1; + isc::data::ConstElementPtr answer; + + // Initialize the application process. + ASSERT_NO_THROW(initProcess()); + EXPECT_TRUE(checkProcess()); + + // Create a configuration set. Content is arbitrary, just needs to be + // valid JSON. + std::string config = "{ \"test-value\": 1000 } "; + isc::data::ElementPtr config_set = isc::data::Element::fromJSON(config); + + // Verify that a valid config gets a successful update result. + answer = updateConfig(config_set); + isc::config::parseAnswer(rcode, answer); + EXPECT_EQ(0, rcode); + + // Verify that a valid config gets a successful check result. + answer = checkConfig(config_set); + isc::config::parseAnswer(rcode, answer); + EXPECT_EQ(0, rcode); + + // Verify that an error in process configure method is handled. + SimFailure::set(SimFailure::ftProcessConfigure); + answer = updateConfig(config_set); + isc::config::parseAnswer(rcode, answer); + EXPECT_EQ(1, rcode); + + // Verify that an error is handled too when the config is checked for. + SimFailure::set(SimFailure::ftProcessConfigure); + answer = checkConfig(config_set); + isc::config::parseAnswer(rcode, answer); + EXPECT_EQ(1, rcode); +} + +// Tests that handleOtherObjects behaves as expected. +TEST_F(DStubControllerTest, handleOtherObjects) { + using namespace isc::data; + + // A bad config. + ElementPtr config = Element::createMap(); + config->set(controller_->getAppName(), Element::create(1)); + config->set("foo", Element::create(2)); + config->set("bar", Element::create(3)); + + // Check the error message. + std::string errmsg; + EXPECT_NO_THROW(errmsg = controller_->handleOtherObjects(config)); + EXPECT_EQ(" contains unsupported 'bar' parameter (and 'foo')", errmsg); + + // Retry with no error. + config = Element::createMap(); + config->set(controller_->getAppName(), Element::create(1)); + EXPECT_NO_THROW(errmsg = controller_->handleOtherObjects(config)); + EXPECT_TRUE(errmsg.empty()); +} + +// Tests that registered signals are caught and handled. +TEST_F(DStubControllerTest, ioSignals) { + // Tell test controller just to record the signals, don't call the + // base class signal handler. + controller_->recordSignalOnly(true); + + // Setup to raise SIGHUP in 10 ms. + TimedSignal sighup(*getIOService(), SIGHUP, 10); + TimedSignal sigint(*getIOService(), SIGINT, 100); + TimedSignal sigterm(*getIOService(), SIGTERM, 200); + + // Write the valid, empty, config and then run launch() for 500 ms + time_duration elapsed_time; + runWithConfig("{}", 500, elapsed_time); + + // Verify that we caught the signals as expected. + std::vector<int>& signals = controller_->getProcessedSignals(); + ASSERT_EQ(3, signals.size()); + EXPECT_EQ(SIGHUP, signals[0]); + EXPECT_EQ(SIGINT, signals[1]); + EXPECT_EQ(SIGTERM, signals[2]); +} + +// Tests that the original configuration is retained after a SIGHUP triggered +// reconfiguration fails due to invalid config content. +TEST_F(DStubControllerTest, invalidConfigReload) { + // Schedule to rewrite the configuration file after launch. This way the + // file is updated after we have done the initial configuration. The + // new content is invalid JSON which will cause the config parse to fail. + scheduleTimedWrite("{ \"string_test\": BOGUS JSON }", 100); + + // Setup to raise SIGHUP in 200 ms. + TimedSignal sighup(*getIOService(), SIGHUP, 200); + + // Write the config and then run launch() for 500 ms + // After startup, which will load the initial configuration this enters + // the process's runIO() loop. We will first rewrite the config file. + // Next we process the SIGHUP signal which should cause us to reconfigure. + time_duration elapsed_time; + runWithConfig("{ \"string_test\": \"first value\" }", 500, elapsed_time); + + // Verify that we saw the signal. + std::vector<int>& signals = controller_->getProcessedSignals(); + ASSERT_EQ(1, signals.size()); + EXPECT_EQ(SIGHUP, signals[0]); +} + +// Tests that the original configuration is retained after a SIGHUP triggered +// reconfiguration fails due to invalid config content. +TEST_F(DStubControllerTest, alternateParsing) { + controller_->useAlternateParser(true); + + // Setup to raise SIGHUP in 200 ms. + TimedSignal sighup(*getIOService(), SIGHUP, 200); + + // Write the config and then run launch() for 500 ms + // After startup, which will load the initial configuration this enters + // the process's runIO() loop. We will first rewrite the config file. + // Next we process the SIGHUP signal which should cause us to reconfigure. + time_duration elapsed_time; + runWithConfig("{ \"string_test\": \"first value\" }", 500, elapsed_time); + + // Verify that we saw the signal. + std::vector<int>& signals = controller_->getProcessedSignals(); + ASSERT_EQ(1, signals.size()); + EXPECT_EQ(SIGHUP, signals[0]); +} + +// Tests that the original configuration is replaced after a SIGHUP triggered +// reconfiguration succeeds. +TEST_F(DStubControllerTest, validConfigReload) { + // Schedule to rewrite the configuration file after launch. This way the + // file is updated after we have done the initial configuration. + scheduleTimedWrite("{ \"string_test\": \"second value\" }", 100); + + // Setup to raise SIGHUP in 200 ms and another at 400 ms. + TimedSignal sighup(*getIOService(), SIGHUP, 200); + TimedSignal sighup2(*getIOService(), SIGHUP, 400); + + // Write the config and then run launch() for 800 ms + time_duration elapsed_time; + runWithConfig("{ \"string_test\": \"first value\" }", 800, elapsed_time); + + // Verify that we saw two occurrences of the signal. + std::vector<int>& signals = controller_->getProcessedSignals(); + ASSERT_EQ(2, signals.size()); + EXPECT_EQ(SIGHUP, signals[0]); + EXPECT_EQ(SIGHUP, signals[1]); +} + +// Tests that the SIGINT triggers a normal shutdown. +TEST_F(DStubControllerTest, sigintShutdown) { + // Setup to raise SIGHUP in 1 ms. + TimedSignal sighup(*getIOService(), SIGINT, 1); + + // Write the config and then run launch() for 1000 ms + time_duration elapsed_time; + runWithConfig("{ \"string_test\": \"first value\" }", 1000, elapsed_time); + + // Verify that we saw the signal. + std::vector<int>& signals = controller_->getProcessedSignals(); + ASSERT_EQ(1, signals.size()); + EXPECT_EQ(SIGINT, signals[0]); + + // Duration should be significantly less than our max run time. + EXPECT_TRUE(elapsed_time.total_milliseconds() < 300); +} + +// Verifies that version and extended version information is correct +TEST_F(DStubControllerTest, getVersion) { + std::string text = controller_->getVersion(false); + EXPECT_EQ(text,VERSION); + + text = controller_->getVersion(true); + EXPECT_NE(std::string::npos, text.find(VERSION)); + EXPECT_NE(std::string::npos, text.find(EXTENDED_VERSION)); + EXPECT_NE(std::string::npos, text.find(controller_->getVersionAddendum())); +} + +// Tests that the SIGTERM triggers a normal shutdown. +TEST_F(DStubControllerTest, sigtermShutdown) { + // Setup to raise SIGHUP in 1 ms. + TimedSignal sighup(*getIOService(), SIGTERM, 1); + + // Write the config and then run launch() for 1000 ms + time_duration elapsed_time; + runWithConfig("{ \"string_test\": \"first value\" }", 1000, elapsed_time); + + // Verify that we saw the signal. + std::vector<int>& signals = controller_->getProcessedSignals(); + ASSERT_EQ(1, signals.size()); + EXPECT_EQ(SIGTERM, signals[0]); + + // Duration should be significantly less than our max run time. + EXPECT_TRUE(elapsed_time.total_milliseconds() < 300); +} + +}; // end of isc::process namespace +}; // end of isc namespace |