From 4ee8c68be46b8a740763fda457ac03b776108c66 Mon Sep 17 00:00:00 2001 From: Davin McCall Date: Sat, 13 Jan 2018 16:58:12 +0000 Subject: [PATCH] Add some process-service tests. --- .gitignore | 3 +- src/includes/proc-service.h | 1 + src/tests/Makefile | 14 ++- src/tests/proctests.cc | 113 +++++++++++++++++++ src/tests/test-baseproc.cc | 213 ++++++++++++++++++++++++++++++++++++ 5 files changed, 338 insertions(+), 6 deletions(-) create mode 100644 src/tests/proctests.cc create mode 100644 src/tests/test-baseproc.cc diff --git a/.gitignore b/.gitignore index f838254..9d145ba 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ test shutdown dinit-reboot src/tests/tests -src/tests/includes \ No newline at end of file +src/tests/proctests +src/tests/includes diff --git a/src/includes/proc-service.h b/src/includes/proc-service.h index d32938f..a1935c7 100644 --- a/src/includes/proc-service.h +++ b/src/includes/proc-service.h @@ -27,6 +27,7 @@ class base_process_service : public service_record friend class service_child_watcher; friend class exec_status_pipe_watcher; friend class process_restart_timer; + friend class base_process_service_test; private: // Re-launch process diff --git a/src/tests/Makefile b/src/tests/Makefile index 1688ac0..a49dc4a 100644 --- a/src/tests/Makefile +++ b/src/tests/Makefile @@ -1,12 +1,13 @@ -include ../../mconfig -objects = tests.o test-dinit.o -parent_objs = service.o baseproc-service.o proc-service.o dinit-log.o load_service.o +objects = tests.o test-dinit.o proctests.o test-baseproc.o +parent_objs = service.o proc-service.o dinit-log.o load_service.o # baseproc-service.o check: build-tests ./tests + ./proctests -build-tests: prepare-incdir tests +build-tests: tests proctests # Create an "includes" directory populated with a combination of real and mock headers: prepare-incdir: @@ -15,8 +16,11 @@ prepare-incdir: cd includes; ln -sf ../../includes/*.h . cd includes; ln -sf ../test-includes/*.h . -tests: $(objects) $(parent_objs) - $(CXX) $(SANITIZEOPTS) -o tests $(objects) $(parent_objs) $(EXTRA_LIBS) +tests: prepare-incdir $(parent_objs) tests.o test-dinit.o test-baseproc.o + $(CXX) $(SANITIZEOPTS) -o tests $(parent_objs) tests.o test-dinit.o test-baseproc.o $(EXTRA_LIBS) + +proctests: prepare-incdir $(parent_objs) proctests.o test-dinit.o test-baseproc.o + $(CXX) $(SANITIZEOPTS) -o proctests $(parent_objs) proctests.o test-dinit.o test-baseproc.o $(EXTRA_LIBS) $(objects): %.o: %.cc $(CXX) $(CXXOPTS) $(SANITIZEOPTS) -Iincludes -I../dasynq -c $< -o $@ diff --git a/src/tests/proctests.cc b/src/tests/proctests.cc new file mode 100644 index 0000000..9d71560 --- /dev/null +++ b/src/tests/proctests.cc @@ -0,0 +1,113 @@ +#include +#include +#include +#include +#include + +#include "service.h" +#include "proc-service.h" + +// Tests of process-service related functionality. +// +// These tests work mostly by completely mocking out the base_process_service class. The mock +// implementations can be found in test-baseproc.cc. + +// Friend interface to access base_process_service private/protected members. +class base_process_service_test +{ + public: + static void exec_succeeded(base_process_service *bsp) + { + bsp->exec_succeeded(); + } + + static void handle_exit(base_process_service *bsp, int exit_status) + { + bsp->handle_exit_status(exit_status); + } +}; + +// Regular service start +void test1() +{ + using namespace std; + + service_set sset; + + string command = "test-command"; + list> command_offsets; + command_offsets.emplace_back(0, command.length()); + std::list depends; + + process_service p = process_service(&sset, "testproc", std::move(command), command_offsets, depends); + p.start(true); + + base_process_service_test::exec_succeeded(&p); + + assert(p.get_state() == service_state_t::STARTED); +} + +// Unexpected termination +void test2() +{ + using namespace std; + + service_set sset; + + string command = "test-command"; + list> command_offsets; + command_offsets.emplace_back(0, command.length()); + std::list depends; + + process_service p = process_service(&sset, "testproc", std::move(command), command_offsets, depends); + p.start(true); + + base_process_service_test::exec_succeeded(&p); + + assert(p.get_state() == service_state_t::STARTED); + + base_process_service_test::handle_exit(&p, 0); + + assert(p.get_state() == service_state_t::STOPPED); +} + +// Termination via stop request +void test3() +{ + using namespace std; + + service_set sset; + + string command = "test-command"; + list> command_offsets; + command_offsets.emplace_back(0, command.length()); + std::list depends; + + process_service p = process_service(&sset, "testproc", std::move(command), command_offsets, depends); + p.start(true); + + base_process_service_test::exec_succeeded(&p); + + assert(p.get_state() == service_state_t::STARTED); + + p.stop(true); + + assert(p.get_state() == service_state_t::STOPPING); + + base_process_service_test::handle_exit(&p, 0); + + assert(p.get_state() == service_state_t::STOPPED); +} + + +#define RUN_TEST(name) \ + std::cout << #name "... "; \ + name(); \ + std::cout << "PASSED" << std::endl; + +int main(int argc, char **argv) +{ + RUN_TEST(test1); + RUN_TEST(test2); + RUN_TEST(test3); +} diff --git a/src/tests/test-baseproc.cc b/src/tests/test-baseproc.cc new file mode 100644 index 0000000..20a51f6 --- /dev/null +++ b/src/tests/test-baseproc.cc @@ -0,0 +1,213 @@ +#include "dinit.h" +#include "proc-service.h" + +// This is a mock implementation of the base_process_service class, for testing purposes. + +base_process_service::base_process_service(service_set *sset, string name, + service_type_t service_type_p, string &&command, + std::list> &command_offsets, + const std::list &deplist_p) + : service_record(sset, name, service_type_p, deplist_p), child_listener(this), + child_status_listener(this), restart_timer(this) +{ + program_name = std::move(command); + exec_arg_parts = separate_args(program_name, command_offsets); + + restart_interval_count = 0; + restart_interval_time = {0, 0}; + restart_timer.service = this; + //restart_timer.add_timer(event_loop); + + // By default, allow a maximum of 3 restarts within 10.0 seconds: + restart_interval.seconds() = 10; + restart_interval.nseconds() = 0; + max_restart_interval_count = 3; + + waiting_restart_timer = false; + reserved_child_watch = false; + tracking_child = false; + stop_timer_armed = false; + start_is_interruptible = false; +} + +bool base_process_service::bring_up() noexcept +{ + if (restarting) { + if (pid == -1) { + return restart_ps_process(); + } + return true; + } + else { + //event_loop.get_time(restart_interval_time, clock_type::MONOTONIC); + restart_interval_count = 0; + if (start_ps_process(exec_arg_parts, onstart_flags.starts_on_console)) { + if (start_timeout != time_val(0,0)) { + //restart_timer.arm_timer_rel(event_loop, start_timeout); + stop_timer_armed = true; + } + else if (stop_timer_armed) { + //restart_timer.stop_timer(event_loop); + stop_timer_armed = false; + } + return true; + } + return false; + } +} + +void base_process_service::bring_down() noexcept +{ + waiting_for_deps = false; + if (pid != -1) { + // The process is still kicking on - must actually kill it. We signal the process + // group (-pid) rather than just the process as there's less risk then of creating + // an orphaned process group: + if (! onstart_flags.no_sigterm) { + //kill_pg(SIGTERM); + } + if (term_signal != -1) { + //kill_pg(term_signal); + } + + // In most cases, the rest is done in handle_exit_status. + // If we are a BGPROCESS and the process is not our immediate child, however, that + // won't work - check for this now: + if (get_type() == service_type_t::BGPROCESS && ! tracking_child) { + stopped(); + } + else if (stop_timeout != time_val(0,0)) { + //restart_timer.arm_timer_rel(event_loop, stop_timeout); + stop_timer_armed = true; + } + } + else { + // The process is already dead. + stopped(); + } +} + +void base_process_service::do_smooth_recovery() noexcept +{ + if (! restart_ps_process()) { + emergency_stop(); + services->process_queues(); + } +} + +bool base_process_service::start_ps_process(const std::vector &cmd, bool on_console) noexcept +{ + return false; +} + +void base_process_service::kill_with_fire() noexcept +{ + if (pid != -1) { + //log(loglevel_t::WARN, "Service ", get_name(), " with pid ", pid, " exceeded allowed stop time; killing."); + //kill_pg(SIGKILL); + } +} + +void base_process_service::kill_pg(int signo) noexcept +{ + //pid_t pgid = getpgid(pid); + //if (pgid == -1) { + // only should happen if pid is invalid, which should never happen... + //log(loglevel_t::ERROR, get_name(), ": can't signal process: ", strerror(errno)); + //return; + //} + //kill(-pgid, signo); +} + +bool base_process_service::restart_ps_process() noexcept +{ + using time_val = dasynq::time_val; + + time_val current_time; + event_loop.get_time(current_time, clock_type::MONOTONIC); + + if (max_restart_interval_count != 0) { + // Check whether we're still in the most recent restart check interval: + time_val int_diff = current_time - restart_interval_time; + if (int_diff < restart_interval) { + if (restart_interval_count >= max_restart_interval_count) { + //log(loglevel_t::ERROR, "Service ", get_name(), " restarting too quickly; stopping."); + return false; + } + } + else { + restart_interval_time = current_time; + restart_interval_count = 0; + } + } + + // Check if enough time has lapsed since the prevous restart. If not, start a timer: + time_val tdiff = current_time - last_start_time; + if (restart_delay <= tdiff) { + // > restart delay (normally 200ms) + do_restart(); + } + else { + time_val timeout = restart_delay - tdiff; + //restart_timer.arm_timer_rel(event_loop, timeout); + waiting_restart_timer = true; + } + return true; +} + +void base_process_service::do_restart() noexcept +{ + waiting_restart_timer = false; + restart_interval_count++; + auto service_state = get_state(); + + // We may be STARTING (regular restart) or STARTED ("smooth recovery"). This affects whether + // the process should be granted access to the console: + bool on_console = service_state == service_state_t::STARTING + ? onstart_flags.starts_on_console : onstart_flags.runs_on_console; + + if (service_state == service_state_t::STARTING) { + // for a smooth recovery, we want to check dependencies are available before actually + // starting: + if (! check_deps_started()) { + waiting_for_deps = true; + return; + } + } + + if (! start_ps_process(exec_arg_parts, on_console)) { + restarting = false; + if (service_state == service_state_t::STARTING) { + failed_to_start(); + } + else { + // desired_state = service_state_t::STOPPED; + forced_stop(); + } + services->process_queues(); + } +} + +bool base_process_service::interrupt_start() noexcept +{ + if (waiting_restart_timer) { + //restart_timer.stop_timer(event_loop); + waiting_restart_timer = false; + return service_record::interrupt_start(); + } + else { + //log(loglevel_t::WARN, "Interrupting start of service ", get_name(), " with pid ", pid, " (with SIGINT)."); + kill_pg(SIGINT); + if (stop_timeout != time_val(0,0)) { + restart_timer.arm_timer_rel(event_loop, stop_timeout); + stop_timer_armed = true; + } + else if (stop_timer_armed) { + restart_timer.stop_timer(event_loop); + stop_timer_armed = false; + } + set_state(service_state_t::STOPPING); + notify_listeners(service_event_t::STARTCANCELLED); + return false; + } +} -- 2.25.1