Add some process-service tests.
authorDavin McCall <davmac@davmac.org>
Sat, 13 Jan 2018 16:58:12 +0000 (16:58 +0000)
committerDavin McCall <davmac@davmac.org>
Sat, 13 Jan 2018 16:58:12 +0000 (16:58 +0000)
.gitignore
src/includes/proc-service.h
src/tests/Makefile
src/tests/proctests.cc [new file with mode: 0644]
src/tests/test-baseproc.cc [new file with mode: 0644]

index f8382542e60d8ee847b2000a0f539634ef98174c..9d145ba8304daa65a7b081e191cdb133322e005c 100644 (file)
@@ -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
index d32938f53629374483cf08e50a68003121aa39f2..a1935c770a1afef659ee71bc4dcd4761fd356a34 100644 (file)
@@ -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
index 1688ac077dbc991727294b80c5e1297a348cbeb4..a49dc4a29c88f4008698bb124558c7d2d424dd4b 100644 (file)
@@ -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 (file)
index 0000000..9d71560
--- /dev/null
@@ -0,0 +1,113 @@
+#include <cassert>
+#include <iostream>
+#include <list>
+#include <utility>
+#include <string>
+
+#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<pair<unsigned,unsigned>> command_offsets;
+    command_offsets.emplace_back(0, command.length());
+    std::list<prelim_dep> 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<pair<unsigned,unsigned>> command_offsets;
+    command_offsets.emplace_back(0, command.length());
+    std::list<prelim_dep> 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<pair<unsigned,unsigned>> command_offsets;
+    command_offsets.emplace_back(0, command.length());
+    std::list<prelim_dep> 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 (file)
index 0000000..20a51f6
--- /dev/null
@@ -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<std::pair<unsigned,unsigned>> &command_offsets,
+        const std::list<prelim_dep> &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<const char *> &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;
+    }
+}