Browse Source

Begin re-work of integration tests

The integration test shell scripts are fragile. This change moves
towards running integration tests via the igr-runner directly. A bunch
of utility classes and functions are implemented in igr.h. These allow:

- running dinit and capturing stdout/stderr
- waiting for termination, with a timeout
- checking file contents match a specific string

More will be added soon, as the rest of the integration tests are
converted to use the new framework.

The old shell-script based tests remain in place for now as they are
used in the meson build. However, they will be removed once the meson
build is fixed to use igr-runner.
Davin McCall 2 months ago
parent
commit
49a9da28d6
4 changed files with 328 additions and 5 deletions
  1. 1 0
      .gitignore
  2. 4 3
      src/igr-tests/Makefile
  3. 70 2
      src/igr-tests/igr-runner.cc
  4. 253 0
      src/igr-tests/igr.h

+ 1 - 0
.gitignore

@@ -14,6 +14,7 @@
 /src/tests/cptests/corpus
 /src/tests/cptests/fuzz
 /src/igr-tests/igr-runner
+/src/igr-tests/igr-output
 !/src/igr-tests/xdg-config/config/dinit.d/
 /mconfig
 

+ 4 - 3
src/igr-tests/Makefile

@@ -1,10 +1,11 @@
 include ../../mconfig
 
 check-igr: igr-runner
-	./igr-runner
+	mkdir -p igr-output
+	IGR_OUTPUT_BASE=$$PWD/igr-output ./igr-runner
 
-igr-runner: igr-runner.cc
-	$(CXX) $(CXXFLAGS) $(CXXFLAGS_EXTRA) $(LDFLAGS) $(LDFLAGS_EXTRA) igr-runner.cc -o igr-runner
+igr-runner: igr-runner.cc igr.h
+	$(CXX) $(CXXFLAGS) $(CXXFLAGS_EXTRA) -I../../dasynq/include  $(LDFLAGS) $(LDFLAGS_EXTRA) igr-runner.cc -o igr-runner
 
 clean:
 	rm -f igr-runner

+ 70 - 2
src/igr-tests/igr-runner.cc

@@ -5,13 +5,22 @@
 #include <spawn.h>
 #include <unistd.h>
 #include <sys/wait.h>
+#include <sys/stat.h>
+
+#include "igr.h"
 
 extern char **environ;
 
 // Integration test suite runner.
 
+std::string igr_output_basedir;
+std::string igr_dinit_socket_path;
+
+void basic_test();
+
 int main(int argc, char **argv)
 {
+    void (*test_funcs[])() = { basic_test };
     const char * const test_dirs[] = { "basic", "environ", "environ2", "ps-environ", "chain-to", "force-stop",
             "restart", "check-basic", "check-cycle", "check-cycle2", "check-lint", "reload1", "reload2",
             "no-command-error", "add-rm-dep", "var-subst", "svc-start-fail", "dep-not-found", "pseudo-cycle",
@@ -19,22 +28,59 @@ int main(int argc, char **argv)
             "cycles" };
     constexpr int num_tests = sizeof(test_dirs) / sizeof(test_dirs[0]);
 
+    dinit_bindir = "../.."; // XXX
+    igr_output_basedir = "igr-output"; // XXX
+
     int passed = 0;
     int skipped = 0;
     int failed = 0;
 
     bool aborted_run = false;
 
+    char *env_igr_output_base = getenv("IGR_OUTPUT_BASE");
+    if (env_igr_output_base != nullptr) {
+        igr_output_basedir = env_igr_output_base;
+    }
+
+    igr_dinit_socket_path = igr_output_basedir + "/dinit.socket";
+
     std::cout << "============== INTEGRATION TESTS =====================" << std::endl;
 
+    if (mkdir(igr_output_basedir.c_str(), 0700) == -1 && errno != EEXIST) {
+        std::system_error(errno, std::generic_category(), std::string("mkdir: ") + igr_output_basedir);
+    }
+
     for (int i = 0; i < num_tests; i++) {
         const char * test_dir = test_dirs[i];
 
+        std::cout << test_dir << "... ";
+
+        if (i < (sizeof(test_funcs) / sizeof(test_funcs[0]))) {
+            // run function instead
+            bool success;
+            try {
+                test_funcs[i]();
+                success = true;
+            }
+            catch (igr_failure_exc &exc) {
+                success = false;
+            }
+
+            if (success) {
+                std::cout << "PASSED" << std::endl;
+                passed++;
+            }
+            else {
+                std::cout << "FAILED" << std::endl;
+                failed++;
+            }
+
+            continue;
+        }
+
         std::string prog_path = "./run-test.sh";
         char * const p_argv[2] = { const_cast<char *>(prog_path.c_str()), nullptr };
 
-        std::cout << test_dir << "... ";
-
         // "Use posix_spawn", they said. "It will be easy", they said.
 
         if (chdir(test_dir) != 0) {
@@ -120,3 +166,25 @@ int main(int argc, char **argv)
 
     return failed == 0 ? 0 : 1;
 }
+
+void basic_test()
+{
+    std::string output_dir = igr_output_basedir + "/basic";
+    if (mkdir(output_dir.c_str(), 0700) == -1 && errno != EEXIST) {
+        std::system_error(errno, std::generic_category(), std::string("mkdir: ") + output_dir);
+    }
+
+    setenv("IGR_OUTPUT", output_dir.c_str(), true);
+
+    // Start the "basic" service. This creates an output file, "basic-ran", containing "ran\n".
+    dinit_proc dinit_p;
+    dinit_p.start("basic", {"-u", "-d", "sd", "-p", igr_dinit_socket_path, "-q", "basic"});
+    dinit_p.wait_for_term({1,0} /* max 1 second */);
+
+    igr_assert_eq("", dinit_p.get_stdout());
+    igr_assert_eq("", dinit_p.get_stderr());
+
+    check_file_contents(igr_output_basedir + "/basic/basic-ran", "ran\n");
+
+    unsetenv("IGR_OUTPUT");
+}

+ 253 - 0
src/igr-tests/igr.h

@@ -0,0 +1,253 @@
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <dasynq.h>
+
+using event_loop_t = dasynq::event_loop_n;
+event_loop_t event_loop;
+
+// directory containing built executables
+std::string dinit_bindir;
+
+// exception to be thrown on failure
+class igr_failure_exc
+{
+    std::string message;
+public:
+    igr_failure_exc(std::string message_p) : message(message_p) { }
+};
+
+// A process watcher that cleans up by terminating the child process
+class igr_proc_watch : public event_loop_t::child_proc_watcher_impl<igr_proc_watch>
+{
+public:
+    bool did_exit = false;
+    pid_t child_pid = -1;
+    int status = 0;
+
+    dasynq::rearm status_change(event_loop_t &, pid_t child, int status_p)
+    {
+        status = status_p;
+        did_exit = true;
+        child_pid = -1;
+        return dasynq::rearm::REMOVE;
+    }
+
+    pid_t fork(event_loop_t &eloop, bool from_reserved = false, int prio = dasynq::DEFAULT_PRIORITY)
+    {
+        child_pid = event_loop_t::child_proc_watcher_impl<igr_proc_watch>::fork(eloop, from_reserved, prio);
+        return child_pid;
+    }
+
+    ~igr_proc_watch()
+    {
+        if (child_pid != -1) {
+            deregister(event_loop, child_pid);
+            kill(child_pid, SIGKILL);
+        }
+    }
+};
+
+// A simple timer that allows watching for an arbitrary timeout
+class simple_timer : public event_loop_t::timer_impl<simple_timer> {
+    bool is_registered = false;;
+    bool is_expired = false;
+public:
+    simple_timer()
+    {
+        add_timer(event_loop, dasynq::clock_type::MONOTONIC);
+        is_registered = true;
+    }
+
+    dasynq::rearm timer_expiry(event_loop_t &loop, int expiry_count)
+    {
+        is_expired = true;
+        is_registered = false;
+        return dasynq::rearm::REMOVE;
+    }
+
+    void arm(const timespec &timeout)
+    {
+        is_expired = false;
+        arm_timer_rel(event_loop, timeout);
+    }
+
+    bool did_expire()
+    {
+        return is_expired;
+    }
+
+    ~simple_timer()
+    {
+        if (is_registered) {
+            deregister(event_loop);
+        }
+    }
+};
+
+// Consume and buffer and output from a pipe
+class pipe_consume_buffer : public event_loop_t::fd_watcher_impl<pipe_consume_buffer> {
+    int fds[2];
+    std::string buffer;
+
+public:
+    pipe_consume_buffer()
+    {
+        if(pipe(fds) != 0) {
+            throw std::system_error(errno, std::generic_category(), "pipe_consume_buffer: pipe");
+        }
+        if (fcntl(fds[0], F_SETFD, FD_CLOEXEC) != 0) {
+            throw std::system_error(errno, std::generic_category(), "pipe_consume_buffer: fcntl");
+        }
+        if (fcntl(fds[0], F_SETFL, O_ASYNC) != 0) {
+            throw std::system_error(errno, std::generic_category(), "pipe_consume_buffer: fcntl");
+        }
+
+        try {
+            add_watch(event_loop, fds[0], dasynq::IN_EVENTS);
+        }
+        catch (...) {
+            close(fds[0]);
+            close(fds[1]);
+            throw;
+        }
+    }
+
+    ~pipe_consume_buffer()
+    {
+        deregister(event_loop);
+    }
+
+    int get_output_fd()
+    {
+        return fds[1];
+    }
+
+    std::string get_output()
+    {
+        return buffer;
+    }
+
+    dasynq::rearm fd_event(event_loop_t &loop, int fd, int flags)
+    {
+        // read all we can
+        char buf[1024];
+
+        ssize_t r = read(fd, buf, 1024);
+        while (r != 0) {
+            if (r == -1) {
+                if (errno != EAGAIN && errno != EWOULDBLOCK) {
+                    // actual error, not expected
+                    throw std::system_error(errno, std::generic_category(), "pipe_consume_buffer: read");
+                }
+                // otherwise: would block, finish reading now
+                return dasynq::rearm::REARM;
+            }
+
+            buffer.append(buf, r);
+            r = read(fd, buf, 1024);
+        }
+
+        // leave disarmed:
+        return dasynq::rearm::NOOP;
+    }
+};
+
+// dinit process
+class dinit_proc
+{
+    igr_proc_watch pwatch;
+    pipe_consume_buffer out;
+    pipe_consume_buffer err;
+
+public:
+    dinit_proc() {}
+
+    ~dinit_proc() {}
+
+    // start, in specified working directory, with given arguments
+    void start(const char *wdir, std::vector<std::string> args = {})
+    {
+        std::string dinit_exec = dinit_bindir + "/dinit";
+        char **arg_arr = new char *[args.size() + 1];
+        arg_arr[0] = const_cast<char *>(dinit_exec.c_str());
+        for (unsigned i = 0; i < args.size(); ++i) {
+            arg_arr[i + 1] = const_cast<char *>(args[i].c_str());
+        }
+
+        pid_t pid = pwatch.fork(event_loop);
+        if (pid == 0) {
+            chdir(wdir);
+            dup2(out.get_output_fd(), STDOUT_FILENO);
+            dup2(err.get_output_fd(), STDERR_FILENO);
+
+            execv(dinit_exec.c_str(), arg_arr);
+            exit(EXIT_FAILURE);
+        }
+    }
+
+    void wait_for_term(dasynq::time_val timeout)
+    {
+        if (pwatch.did_exit) return;
+
+        simple_timer timer;
+        timer.arm(timeout);
+
+        while (!pwatch.did_exit && !timer.did_expire()) {
+            event_loop.run();
+        }
+
+        if (!pwatch.did_exit) {
+            throw igr_failure_exc("timeout waiting for termination");
+        }
+    }
+
+    std::string get_stdout()
+    {
+        return out.get_output();
+    }
+
+    std::string get_stderr()
+    {
+        return err.get_output();
+    }
+};
+
+// read entire file contents as a string
+inline std::string read_file_contents(const std::string &filename)
+{
+    std::string contents;
+
+    int fd = open(filename.c_str(), O_RDONLY);
+    if (fd == -1) {
+        throw std::system_error(errno, std::generic_category(), "read_file_contents: open");
+    }
+
+    char buf[1024];
+    int r = read(fd, buf, 1024);
+    while (r > 0) {
+        contents.append(buf, r);
+        r = read(fd, buf, 1024);
+    }
+
+    if (r == -1) {
+        throw std::system_error(errno, std::generic_category(), "read_file_contents: read");
+    }
+
+    return contents;
+}
+
+inline void check_file_contents(const std::string &file_path, const std::string &expected_contents)
+{
+    std::string contents = read_file_contents(file_path);
+    if (contents != expected_contents) {
+        throw igr_failure_exc(std::string("File contents do not match expected for file ") + file_path);
+    }
+}
+
+inline void igr_assert_eq(const std::string &expected, const std::string &actual)
+{
+    if (expected != actual) {
+        throw igr_failure_exc(std::string("Test assertion failed:\n") + "Expected: " + expected + "\nActual: " + actual);
+    }
+}