Add stability tests
authorKonrad Kuchciak <k.kuchciak@samsung.com>
Thu, 22 Aug 2019 10:57:20 +0000 (12:57 +0200)
committerKonrad Kuchciak <k.kuchciak@samsung.com>
Thu, 22 Aug 2019 10:57:20 +0000 (12:57 +0200)
Testing programs were created by Michal Bloch (m.bloch@samsung.com)

Change-Id: Ica86065e0c3771a4a856995c4642674e1a07f86e

12 files changed:
packaging/stability-monitor.spec
tests/Makefile [new file with mode: 0644]
tests/config.json [new file with mode: 0644]
tests/test-stability-cpu-utils.cpp [new file with mode: 0644]
tests/test-stability-cpu-utils.hpp [new file with mode: 0644]
tests/test-stability-cpu.cpp [new file with mode: 0644]
tests/test-stability-fd.cpp [new file with mode: 0644]
tests/test-stability-fg-bg.cpp [new file with mode: 0644]
tests/test-stability-io.cpp [new file with mode: 0644]
tests/test-stability-mem.cpp [new file with mode: 0644]
tests/test-stability.cpp [new file with mode: 0644]
tests/test-stability.hpp [new file with mode: 0644]

index bc77df6da03b9996f6c5ba065906f02cf9685962..9c522495c4bf889bbeefa213c660a7d36501ebff 100644 (file)
@@ -15,6 +15,12 @@ BuildRequires: arm-rpi3-linux-kernel-devel
 %description
 This package provides stability monitoring daemon.
 
+%package tests
+Summary:    Stability monitor tests/specification
+
+%description tests
+Tests for stability monitoring tool
+
 %prep
 %setup -q
 %define KMOD_PATH /%{_libdir}/modules/linux/kernel/drivers/misc/stability-monitor/proc-tsm.ko
@@ -26,6 +32,9 @@ cd kernel
 make clean
 make all
 
+cd ../tests
+make all
+
 %install
 make install INSTALL_PREFIX=%{buildroot}/%{_sbindir}
 install -D config/default.conf %{buildroot}/%{_libdir}/stability-monitor/default.conf
@@ -36,6 +45,14 @@ install -D config/stability-monitor.service %{buildroot}/%{_unitdir}/stability-m
 mkdir -p %{buildroot}/%{_unitdir}/multi-user.target.wants
 ln -s ../stability-monitor.service %{buildroot}/%{_unitdir}/multi-user.target.wants/stability-monitor.service
 
+# install -D test-stability-fg-bg %{buildroot}%{_libexecdir}/stability-tests/test-stability-fg-bg
+install -D tests/test-stability-cpu   %{buildroot}%{_libexecdir}/stability-tests/test-stability-cpu
+install -D tests/test-stability-mem   %{buildroot}%{_libexecdir}/stability-tests/test-stability-mem
+install -D tests/test-stability-io    %{buildroot}%{_libexecdir}/stability-tests/test-stability-io
+install -D tests/test-stability-fd    %{buildroot}%{_libexecdir}/stability-tests/test-stability-fd
+
+install -D tests/config.json          %{buildroot}/etc/stability-monitor.d/test-stability.conf
+
 %files
 %license COPYING
 %{_sbindir}/stability-monitor
@@ -44,3 +61,11 @@ ln -s ../stability-monitor.service %{buildroot}/%{_unitdir}/multi-user.target.wa
 %KMOD_PATH
 %{_unitdir}/stability-monitor.service
 %{_unitdir}/multi-user.target.wants/stability-monitor.service
+
+%files tests
+# %{_libexecdir}/stability-tests/test-stability-fg-bg
+%{_libexecdir}/stability-tests/test-stability-cpu
+%{_libexecdir}/stability-tests/test-stability-mem
+%{_libexecdir}/stability-tests/test-stability-io
+%{_libexecdir}/stability-tests/test-stability-fd
+/etc/stability-monitor.d/test-stability.conf
diff --git a/tests/Makefile b/tests/Makefile
new file mode 100644 (file)
index 0000000..e29c7a1
--- /dev/null
@@ -0,0 +1,7 @@
+all:
+       # Temporarily disabled, needs an overhaul
+       # g++ -std=c++14 -o test-stability-fg-bg -pthread `pkg-config --cflags glib-2.0 gio-2.0` test-stability.cpp test-stability-cpu-utils.cpp test-stability-fg-bg.cpp `pkg-config --libs glib-2.0 gio-2.0`
+       g++ -std=c++14 -o test-stability-cpu   -pthread `pkg-config --cflags glib-2.0 gio-2.0` test-stability.cpp test-stability-cpu-utils.cpp test-stability-cpu.cpp   `pkg-config --libs glib-2.0 gio-2.0`
+       g++ -std=c++14 -o test-stability-mem   -pthread `pkg-config --cflags glib-2.0 gio-2.0` test-stability.cpp                              test-stability-mem.cpp   `pkg-config --libs glib-2.0 gio-2.0`
+       g++ -std=c++14 -o test-stability-io    -pthread `pkg-config --cflags glib-2.0 gio-2.0` test-stability.cpp                              test-stability-io.cpp    `pkg-config --libs glib-2.0 gio-2.0`
+       g++ -std=c++14 -o test-stability-fd    -pthread `pkg-config --cflags glib-2.0 gio-2.0` test-stability.cpp                              test-stability-fd.cpp    `pkg-config --libs glib-2.0 gio-2.0`
diff --git a/tests/config.json b/tests/config.json
new file mode 100644 (file)
index 0000000..ff472b4
--- /dev/null
@@ -0,0 +1,50 @@
+{
+       "global" : {
+               "sampling_rate" : 1.0
+       },
+       "test-stability-cpu" : {
+               "monitor" : 1,
+               "print_current": 1,
+               "kill" : 0,
+               "report" : 0,
+               "sampling_rate": 1.0,
+               "apply_to_children" : 0,
+               "cpu_limit_avg" : 0.40,
+               "cpu_limit_peak" : 0.90,
+               "cpu_avg_period" : 3
+       },
+       "test-stability-mem" : {
+               "monitor" : 1,
+               "print_current": 1,
+               "kill" : 0,
+               "report" : 0,
+               "sampling_rate": 1.0,
+               "apply_to_children" : 0,
+               "mem_limit_avg" : 0.30,
+               "mem_limit_peak" : 0.60,
+               "comment_to_above" : "% of reference target (1 GB on rpi3)",
+               "mem_limit_period" : 3
+       },
+       "test-stability-fd" : {
+               "monitor" : 1,
+               "print_current": 1,
+               "kill" : 0,
+               "report" : 0,
+               "sampling_rate": 1.0,
+               "apply_to_children" : 0,
+               "fd_limit" : 200
+       },
+       "test-stability-io" : {
+               "monitor" : 1,
+               "print_current": 1,
+               "kill" : 0,
+               "report" : 0,
+               "sampling_rate": 1.0,
+               "apply_to_children" : 0,
+               "io_limit_avg" : 10.0,
+               "io_limit_peak" : 30.0,
+               "comment_to_above" : "in megabytes (on reference target)",
+               "io_limit_period" : 3
+       }
+}
+
diff --git a/tests/test-stability-cpu-utils.cpp b/tests/test-stability-cpu-utils.cpp
new file mode 100644 (file)
index 0000000..b1c0017
--- /dev/null
@@ -0,0 +1,86 @@
+#include "test-stability-cpu-utils.hpp"
+
+// C
+#include <cstdlib>
+
+// C++
+#include <thread>
+#include <vector>
+
+// POSIX
+#include <sys/sysinfo.h>
+
+size_t get_processor_count ()
+{
+       /* Single-threaded applications may look like they are not using much CPU
+        * power from the system's point of view while actually using an entire core
+        * in a multi-core setup. For example, a program using 25% looks okay at a
+        * glance, but could be a hogging 100% of one of 4 available cores.
+        *
+        * Therefore, we'd like to be able to test both heavy usage of a single core
+        * and of the entire system (that is, all cores). */
+
+       const auto processor_count = get_nprocs ();
+       if (processor_count <= 0)
+               throw std::runtime_error ("Couldn't get processor count");
+       if (processor_count > CPU_SETSIZE)
+               throw std::runtime_error ("Too many processors");
+       return static_cast <size_t> (processor_count);
+}
+
+void * cpu_heavy (void * arg)
+{
+       /* Do some intensive numerical calculations while avoiding system calls and
+        * general I/O to maximize CPU usage. The loop is designed never to end but
+        * be complicated enough that the compiler can't tell and optimize it out.
+        * Note that `cpu_light` looks the same except the sleep on one line, but
+        * putting it in a conditional branch could (would!) ruin CPU usage whether
+        * that branch were taken or not, which is why it's a separate function.
+        * `if constexpr` sounds like it would allow to merge them but is C++17. */
+
+       float number = 123.4 + 567 * reinterpret_cast <intptr_t> (arg);
+       while (number > 0.1337 && !global_stop) {
+               for (size_t i = 0; i < 2; ++i) {
+                       float half = number * 0.5F;
+                       long tmp = 0x5F3759DF - ((* reinterpret_cast <long *> (& number)) >> 1);
+                       number = * reinterpret_cast <float *> (& tmp);
+                       number *= 1.5F - (half * number * number);
+                       if (number < 0)
+                               number = - number;
+               }
+               number *= number;
+               number *= number;
+
+               // no sleep (cf. `cpu_light` below)
+       }
+       return reinterpret_cast <void *> (static_cast <int> (number));
+}
+
+void * cpu_light (void * arg)
+{
+       /* This function is similar to the above, except stability monitor should
+        * consider it kosher because it has an extra sleep (that can't be put in
+        * a conditional, see the comments in `cpu_heavy`) which removes most of
+        * its CPU usage. */
+
+       float number = 123.4 + 567 * reinterpret_cast <intptr_t> (arg);
+       while (number > 0.1337 && !global_stop) {
+               for (size_t i = 0; i < 2; ++i) {
+                       float half = number * 0.5F;
+                       long tmp = 0x5F3759DF - ((* reinterpret_cast <long *> (& number)) >> 1);
+                       number = * reinterpret_cast <float *> (& tmp);
+                       number *= 1.5F - (half * number * number);
+                       if (number < 0)
+                               number = - number;
+               }
+               number *= number;
+               number *= number;
+
+               std::this_thread::sleep_for (std::chrono::milliseconds (1));
+       }
+       return reinterpret_cast <void *> (static_cast <int> (number));
+}
+
+bool run_cpu_test (thread_func_t * func, size_t processors, size_t test_time, dbus_signal_handler * handler)
+{ return run_test (func, processors, handler, test_time); }
+
diff --git a/tests/test-stability-cpu-utils.hpp b/tests/test-stability-cpu-utils.hpp
new file mode 100644 (file)
index 0000000..c508276
--- /dev/null
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "test-stability.hpp"
+
+void * cpu_light (void * arg);
+void * cpu_heavy (void * arg);
+size_t get_processor_count ();
+bool run_cpu_test (thread_func_t * func, size_t processors, size_t test_time, dbus_signal_handler * handler);
+
+struct cpu_handler : public dbus_signal_handler {
+       const std::string limitType;
+       static constexpr const char * const gtype = "(isdda{sv})";
+
+       cpu_handler (const char * lt) : limitType (lt) { }
+
+       bool match (const gchar * objpath, const gchar * iface, GVariant * parameters) const override {
+               if ("/Org/Tizen/StabilityMonitor/tsm_cpu"s != objpath
+               ||  "org.tizen.abnormality.cpu.relative"s  != iface
+               ||  !g_variant_is_of_type (parameters, G_VARIANT_TYPE(gtype)))
+                       return false;
+
+               const char * paramType = nullptr;
+               int pid = -1;
+               g_variant_get (parameters, gtype, & pid, & paramType, nullptr, nullptr, nullptr);
+               return pid == getpid() && limitType == paramType;
+       }
+};
diff --git a/tests/test-stability-cpu.cpp b/tests/test-stability-cpu.cpp
new file mode 100644 (file)
index 0000000..d0563ee
--- /dev/null
@@ -0,0 +1,75 @@
+
+// stability-monitor
+#include "test-stability-cpu-utils.hpp"
+#include "test-stability.hpp"
+
+// C
+#include <cstdlib>
+
+// C++
+#include <thread>
+#include <vector>
+
+// POSIX
+#include <sys/sysinfo.h>
+
+cpu_handler handler_avg ("avg"), handler_peak ("peak");
+
+int main (int argc, char ** argv)
+{
+       const std::vector <std::pair <bool (*) (), std::string>> test_cases
+               { { []() {
+                       /* Do some light calculation for the test period.
+                        * Expect no signal during that time.
+                        * This test has no multi-core variante since
+                        * all cores have little load regardless, and no
+                        * peak variante because . */
+                       return run_cpu_test (cpu_light, 1, TEST_TIME_PEAK, & handler_peak);
+               } , "no CPU load, expecting no signal"
+               } , { []() {
+                       /* Do heavy calculation on 1 core for the test period.
+                        * Expect a signal. */
+                       return ! run_cpu_test (cpu_heavy, 1, TEST_TIME_PEAK, & handler_peak);
+               } , "heavy 1 CPU load, expecting peak signal"
+               } , { []() {
+                       /* Same as above, except on all cores. */
+                       return ! run_cpu_test (cpu_heavy, get_processor_count (), TEST_TIME_PEAK, & handler_peak);
+               } , "heavy all CPU load, expecting peak signal"
+               } , { []() {
+                       /* Run heavy calculation on 1 core, expect a signal.
+                        * Wait a bit to make sure any straggler signals are gone.
+                        * Run light calculation on 1 core, expect no signal.
+                        * Switch to heavy calculation on 1 core again, expect a signal. */
+                       if (run_cpu_test (cpu_heavy, 1, TEST_TIME_PEAK, & handler_peak))
+                               return false;
+                       std::this_thread::sleep_for (CATCH_STRAGGLER_SIGNALS_TIME);
+                       if (! run_cpu_test (cpu_light, 1, TEST_TIME_PEAK, & handler_peak))
+                               return false;
+                       return ! run_cpu_test (cpu_heavy, 1, TEST_TIME_PEAK, & handler_peak);
+               } , "heavy 1 CPU load, expecting peak signal; then toning down to light load, expecting no signal"
+               } , { []() {
+                       /* Same as above, except on all cores. */
+                       const size_t procs = get_processor_count ();
+                       if (run_cpu_test (cpu_heavy, procs, TEST_TIME_PEAK, & handler_peak))
+                               return false;
+                       std::this_thread::sleep_for (CATCH_STRAGGLER_SIGNALS_TIME);
+                       if (! run_cpu_test (cpu_light, 1, TEST_TIME_PEAK, & handler_peak))
+                               return false;
+                       return ! run_cpu_test (cpu_heavy, procs, TEST_TIME_PEAK, & handler_peak);
+               } , "heavy all CPU load, expecting peak signal; then toning down to light load, expecting no signal"
+               } , { []() {
+                       /* Run for a longer time (for avg) at half speed (so as
+                        * not to trigger peak detection). */
+                       return run_throttled (60
+                               , [] () {
+                                       if (! run_cpu_test (cpu_heavy, 1, TEST_TIME_PEAK, & handler_peak))
+                                               return false;
+                                       return ! run_cpu_test (cpu_heavy, 1, TEST_TIME_AVG - TEST_TIME_PEAK, & handler_avg);
+                               }
+                       );
+               } , "running at ~60% CPU (using SIGSTOP/SIGCONT from child), expecting average signal (but not peak)"
+               }
+       };
+
+       return standard_main (test_cases, argc, argv);
+}
diff --git a/tests/test-stability-fd.cpp b/tests/test-stability-fd.cpp
new file mode 100644 (file)
index 0000000..47ffa0c
--- /dev/null
@@ -0,0 +1,72 @@
+#include "test-stability.hpp"
+
+// C
+#include <cstdlib>
+
+// C++
+#include <fstream>
+#include <thread>
+#include <vector>
+
+// POSIX
+#include <fcntl.h>
+
+static void * open_fds (size_t count)
+{
+       std::vector <int> fds (count, -1);
+       for (auto & fd : fds)
+               fd = open ("/dev/null", O_RDONLY);
+
+       while (!global_stop)
+               std::this_thread::sleep_for (std::chrono::seconds (1));
+
+       for (const auto fd : fds)
+               if (fd != -1)
+                       close (fd);
+
+       return nullptr;
+}
+
+static void * fd_heavy (void * arg)
+{ return open_fds (729); }
+static void * fd_light (void * arg)
+{ return open_fds   (3); }
+
+struct fd_handler : public dbus_signal_handler {
+       const std::string limitType;
+       static constexpr const char * const gtype = "(isiia{sv})";
+
+       fd_handler (const char * lt) : limitType (lt) { }
+
+       bool match (const gchar * objpath, const gchar * iface, GVariant * parameters) const override {
+               if ("/Org/Tizen/StabilityMonitor/tsm_fd"s != objpath
+               ||  "org.tizen.abnormality.fd.absolute"s  != iface
+               ||  !g_variant_is_of_type (parameters, G_VARIANT_TYPE(gtype)))
+                       return false;
+
+               const char * paramType = nullptr;
+               int pid = -1;
+               g_variant_get (parameters, gtype, & pid, & paramType, nullptr, nullptr, nullptr);
+               return pid == getpid() && limitType == paramType;
+       }
+} handler_avg ("avg"), handler_peak ("peak");
+
+bool run_fd_test (thread_func_t * func, size_t test_time, dbus_signal_handler * handler)
+{ return run_test (func, 1, handler, test_time); }
+
+int main (int argc, char ** argv)
+{
+       const std::vector <std::pair <bool (*) (), std::string>> test_cases
+               { { []() {
+                       /* Open a handful of FDs. Expect no signal. */
+                       return run_fd_test (fd_light, TEST_TIME_PEAK, & handler_peak);
+               } , "3 FDs (+ defaults), expecting no signal"
+               } , { []() {
+                       /* Open a whole lot of FDs. Expect a signal. */
+                       return ! run_fd_test (fd_heavy, TEST_TIME_PEAK, & handler_peak);
+               } , "729 FDs (+ defaults), expecting a signal"
+               }
+       };
+
+       return standard_main (test_cases, argc, argv);
+}
diff --git a/tests/test-stability-fg-bg.cpp b/tests/test-stability-fg-bg.cpp
new file mode 100644 (file)
index 0000000..eefa89a
--- /dev/null
@@ -0,0 +1,115 @@
+#include "test-stability.hpp"
+#include "test-stability-cpu-utils.hpp"
+
+// C
+#include <cstdlib>
+
+// C++
+#include <thread>
+#include <vector>
+
+// POSIX
+#include <sys/sysinfo.h>
+
+static void unref_gvariant (GVariant * gv)
+{
+       if (!gv)
+               return;
+       g_variant_unref (gv);
+}
+static void unref_obj (gpointer ptr)
+{
+       if (!ptr)
+               return;
+       g_object_unref (ptr);
+}
+
+static void change_self_status (const char * status)
+{
+       managed_GError err;
+
+       std::unique_ptr <GVariant, void (*) (GVariant *)> params
+               ( g_variant_new ( "(i&s&s&s&s)"
+                       , (int) getpid ()
+                       , "stability-monitor-appID"
+                       , "stability-monitor-pkgID"
+                       , "widget" // svc, widget, watch, gui
+                       , status // fg, bg
+               ) ?: throw std::runtime_error ("g_variant_new failed")
+               , unref_gvariant
+       );
+
+       std::unique_ptr <GDBusConnection, void (*) (gpointer)> connection
+               ( g_bus_get_sync (G_BUS_TYPE_SYSTEM, nullptr, & err.err)
+                       ?: throw std::runtime_error (err.err->message)
+               , unref_obj
+       );
+
+       g_dbus_connection_emit_signal
+               ( connection.get ()
+               , nullptr
+               , "/Org/Tizen/Aul/AppStatus"
+               , "org.tizen.aul.AppStatus"
+               , "AppStatusChange"
+               , params.release () /* `g_variant_new` returns a "floating" reference.
+                                    * This means that unless it is "sunk" explicitly
+                                    * using `g_variant_ref_sink`, various GIO functions
+                                    * which accept a GVariant, such as this one, take
+                                    * over its ownership. This is why the pointer is
+                                    * release'd instead of just get'd. */
+               , & err.err
+       ) ?: throw std::runtime_error (err.err->message);
+
+       /* Unlike most GIO functions, `g_dbus_connection_emit_signal` doesn't seem
+        * to have a synchronous version so the signal has to be flushed manually. */
+       g_dbus_connection_flush_sync
+               ( connection.get ()
+               , nullptr
+               , & err.err
+       ) ?: throw std::runtime_error (err.err->message);
+}
+
+static void switch_to_foreground ()
+{ change_self_status("fg"); }
+
+static void switch_to_background ()
+{ change_self_status("bg"); }
+
+cpu_handler handler_avg ("avg"), handler_peak ("peak");
+
+int main (int argc, char ** argv)
+{
+       std::cout << "test removed for pressing ceremonial reasons" << std::endl;
+       return EXIT_SUCCESS;
+
+       const std::vector <std::pair <bool (*) (), std::string>> test_cases
+               { { []() {
+                       /* Switch to foreground and do heavy processing. Expect no
+                        * signal. Then switch to background and restart processing,
+                        * expecting a signal this time. */
+                       const size_t procs = get_processor_count ();
+
+                       switch_to_foreground ();
+                       if (! run_cpu_test (cpu_heavy, procs, TEST_TIME_PEAK, & handler_peak))
+                               return false;
+                       switch_to_background ();
+                       return ! run_cpu_test (cpu_heavy, procs, TEST_TIME_PEAK, & handler_peak);
+               } , "~100% FG CPU load (no signal expected), then switch to BG while keeping load (expecting a signal)"
+               } , { []() {
+                       /* Similar to the above, but the other way around: start in
+                        * the background and see if switching to the foreground
+                        * stops the signals. */
+                       const size_t procs = get_processor_count ();
+
+                       switch_to_background ();
+                       if (run_cpu_test (cpu_heavy, procs, TEST_TIME_PEAK, & handler_peak))
+                               return false;
+                       switch_to_foreground ();
+                       std::this_thread::sleep_for (CATCH_STRAGGLER_SIGNALS_TIME);
+                       return run_cpu_test (cpu_heavy, procs, TEST_TIME_PEAK, & handler_peak);
+               } , "~100% BG CPU load (signal expected), then switch to FG while keeping load (expecting signals to stop)"
+               }
+       };
+
+       return standard_main (test_cases, argc, argv);
+}
diff --git a/tests/test-stability-io.cpp b/tests/test-stability-io.cpp
new file mode 100644 (file)
index 0000000..629c91c
--- /dev/null
@@ -0,0 +1,101 @@
+#include "test-stability.hpp"
+
+// C
+#include <cstdlib>
+
+// C++
+#include <fstream>
+#include <thread>
+#include <vector>
+
+static void * do_io (size_t delay)
+{
+       /* Write to an actual file on the disk (i.e. not block devices
+        * nor tmpfs files). The read can be from anywhere since only
+        * the sum of both matters and it's good not to have to keep
+        * extra files on the disk to read from.  */
+
+       static constexpr const char *  IN_FILE_PATH = "/dev/zero";
+       static constexpr const char * OUT_FILE_PATH = "/opt/usr/stability-test.out";
+
+       std::ifstream in  ( IN_FILE_PATH, std::ifstream::binary);
+       std::ofstream out (OUT_FILE_PATH, std::ofstream::binary);
+       if (!  in.good ()
+       ||  ! out.good ())
+               throw std::runtime_error (std::string("Couldn't open ") + IN_FILE_PATH + " and " + OUT_FILE_PATH);
+
+       /* Buffering is undesirable here; it affects the underlying I/O
+        * in an unpredictable way, ruining measurements. */
+        in.rdbuf ()-> pubsetbuf (nullptr, 0);
+       out.rdbuf ()-> pubsetbuf (nullptr, 0);
+
+       while (!global_stop) {
+               for (size_t i = 0; i < 64; ++i) {
+                       char buffer [16384];
+                        in.read  (buffer, sizeof buffer);
+                       out.write (buffer, sizeof buffer);
+               }
+
+               std::this_thread::sleep_for (std::chrono::milliseconds (delay));
+       }
+
+        in.close ();
+       out.close ();
+
+       std::remove (OUT_FILE_PATH);
+
+       return nullptr;
+}
+
+static void * io_heavy (void * arg)
+{ do_io (10); }
+static void * io_light (void * arg)
+{ do_io (1000); }
+static void * io_medium (void * arg)
+{ do_io (40); }
+
+struct io_handler : public dbus_signal_handler {
+       const std::string limitType;
+       static constexpr const char * const gtype = "(isdda{sv})";
+
+       io_handler (const char * lt) : limitType (lt) { }
+
+       bool match (const gchar * objpath, const gchar * iface, GVariant * parameters) const override {
+               if ("/Org/Tizen/StabilityMonitor/tsm_io"s != objpath
+               ||  "org.tizen.abnormality.io.absolute"s  != iface
+               ||  !g_variant_is_of_type (parameters, G_VARIANT_TYPE(gtype)))
+                       return false;
+
+               const char * paramType = nullptr;
+               int pid = -1;
+               g_variant_get (parameters, gtype, & pid, & paramType, nullptr, nullptr, nullptr);
+               return pid == getpid() && limitType == paramType;
+       }
+} handler_avg ("avg"), handler_peak ("peak");
+
+bool run_io_test (thread_func_t * func, size_t test_time, dbus_signal_handler * handler)
+{ return run_test (func, 1, handler, test_time); }
+
+int main (int argc, char ** argv)
+{
+       const std::vector <std::pair <bool (*) (), std::string>> test_cases
+               { { []() {
+                       /* Light, periodic I/O. Expect no signal. */
+                       return run_io_test (io_light, TEST_TIME_PEAK, & handler_peak);
+               } , "light I/O, expecting no signal"
+               } , { []() {
+                       /* Constant, heavy I/O. Expect a signal. */
+                       return ! run_io_test (io_heavy, TEST_TIME_PEAK, & handler_peak);
+               } , "heavy I/O, expecting peak signal"
+               } , { []() {
+                       /* Heavy (but limited) I/O distributed over a longer time.
+                        * Expect a signal (but not too early, would mean peak). */
+                       if (! run_io_test (io_medium, TEST_TIME_PEAK, & handler_peak))
+                               return false;
+                       return ! run_io_test (io_medium, TEST_TIME_AVG - TEST_TIME_PEAK, & handler_avg);
+               } , "medium I/O over time, expecting average signal but not peak"
+               }
+       };
+
+       return standard_main (test_cases, argc, argv);
+}
diff --git a/tests/test-stability-mem.cpp b/tests/test-stability-mem.cpp
new file mode 100644 (file)
index 0000000..f0e413d
--- /dev/null
@@ -0,0 +1,86 @@
+#include "test-stability.hpp"
+
+// C
+#include <cstdlib>
+
+// C++
+#include <thread>
+#include <vector>
+
+// POSIX
+#include <sys/sysinfo.h>
+
+static void * do_mem (size_t to_alloc)
+{
+       /* Allocate memory and pretend we're using it (so as to make it actually
+        * count to RSS use). The allocated amount stays constant. The memory is
+        * volatile to prevent smart compilers from optimizing stuff out. */
+
+       std::unique_ptr <volatile char []> ptr (new volatile char [to_alloc]);
+       for (size_t i = 0; i < to_alloc; i += 4096) // FIXME: 4096 -> page size
+               ptr[i] = 'a' + (i % ('z' - 'a'));
+
+       while (!global_stop)
+               std::this_thread::sleep_for (std::chrono::seconds (1));
+
+       return nullptr;
+}
+
+unsigned long long operator "" _MB (unsigned long long n)
+{ return 1024 * 1024 * n; }
+
+static void * mem_heavy_peak (void * arg)
+{ return do_mem (750_MB); }
+
+static void * mem_heavy_avg (void * arg)
+{ return do_mem (400_MB); }
+
+static void * mem_light (void * arg)
+{ return do_mem (25_MB); }
+
+struct mem_handler : public dbus_signal_handler {
+       const std::string limitType;
+       static constexpr const char * const gtype = "(isdda{sv})";
+
+       mem_handler (const char * lt) : limitType (lt) { }
+
+       bool match (const gchar * objpath, const gchar * iface, GVariant * parameters) const override {
+               if ("/Org/Tizen/StabilityMonitor/tsm_mem"s != objpath
+               ||  "org.tizen.abnormality.mem.relative"s  != iface
+               ||  !g_variant_is_of_type (parameters, G_VARIANT_TYPE(gtype)))
+                       return false;
+
+               const char * paramType = nullptr;
+               int pid = -1;
+               g_variant_get (parameters, gtype, & pid, & paramType, nullptr, nullptr, nullptr);
+               return pid == getpid() && limitType == paramType;
+       }
+} handler_avg ("avg"), handler_peak ("peak");
+
+bool run_mem_test (thread_func_t * func, size_t test_time, dbus_signal_handler * handler)
+{ return run_test (func, 1, handler, test_time); }
+
+int main (int argc, char ** argv)
+{
+       const std::vector <std::pair <bool (*) (), std::string>> test_cases
+               { { []() {
+                       /* Do a reasonable amount of memory (re)allocation.
+                        * Expect no signal. */
+                       return run_mem_test (mem_light, TEST_TIME_PEAK, & handler_peak);
+               } , "small allocation (25 MB + overhead), expecting no signal"
+               } , { []() {
+                       /* Allocate and keep lots of memory.
+                        * Expect a signal. */
+                       return ! run_mem_test (mem_heavy_peak, TEST_TIME_PEAK, & handler_peak);
+               } , "heavy allocation (750 MB + overhead), expecting peak signal"
+               } , { []() {
+                       /* Allocate an amount of memory not high enough
+                        * to trigger peak, but keep it long enough to
+                        * trigger average. */
+                       return ! run_mem_test (mem_heavy_avg, TEST_TIME_AVG, & handler_avg);
+               } , "medium allocation (400 MB + overhead), expecting avg signal"
+               }
+       };
+
+       return standard_main (test_cases, argc, argv);
+}
diff --git a/tests/test-stability.cpp b/tests/test-stability.cpp
new file mode 100644 (file)
index 0000000..b8abbfe
--- /dev/null
@@ -0,0 +1,266 @@
+/*
+ * This file is a part of stability-monitor.
+ *
+ * Copyright © 2019 Samsung Electronics
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "test-stability.hpp"
+
+// C
+#include <cassert>
+#include <cstdio>
+#include <cstdlib>
+
+// C++
+#include <chrono>
+#include <exception>
+#include <iostream>
+#include <fstream>
+#include <map>
+#include <memory>
+#include <numeric>
+#include <system_error>
+#include <thread>
+#include <vector>
+
+// POSIX
+#include <fcntl.h>
+#include <pthread.h>
+#include <unistd.h>
+
+static inline void throw_errno (const std::string & what)
+{ throw std::system_error (errno, std::generic_category (), what); }
+
+/* Globals because passing them around would be a massive hassle. */
+bool timed_out = false;
+bool global_stop = false;
+GMainLoop * loop = nullptr;
+
+/* Can't use a typedef for function declarations, so a macro it is. */
+#define SIGNAL_ARGS \
+         GDBusConnection * connection     \
+       , const gchar     * sender_name    \
+       , const gchar     * object_path    \
+       , const gchar     * interface_name \
+       , const gchar     * signal_name    \
+       , GVariant        * parameters     \
+       , gpointer          user_data
+
+typedef void signal_cb_t (SIGNAL_ARGS);
+
+static void signal_cb (SIGNAL_ARGS)
+{
+       assert (user_data);
+       if (signal_name != SIGNAL_NAME)
+               return; // would ideally be assert but I don't trust dbus enough
+
+       const auto handler = reinterpret_cast <const dbus_signal_handler *> (user_data);
+       if (handler->match (object_path, interface_name, parameters))
+               g_main_loop_quit (loop);
+}
+
+struct dbus_signal_listener {
+       managed_GError err;
+       GDBusConnection * connection;
+       guint signal_subscription;
+
+       dbus_signal_listener (const dbus_signal_handler * handler)
+               : err ()
+               , connection (g_bus_get_sync (G_BUS_TYPE_SYSTEM, nullptr, & err.err)
+                       ?: throw std::runtime_error (err.err->message)
+               )
+               , signal_subscription (g_dbus_connection_signal_subscribe
+                       ( connection
+                       , nullptr // sender
+                       , nullptr // interface
+                       , SIGNAL_NAME.c_str ()
+                       , nullptr // object_path
+                       , nullptr // arg0
+                       , G_DBUS_SIGNAL_FLAGS_NONE
+                       , signal_cb
+                       , const_cast <void *> (reinterpret_cast <const void *> (handler))
+                       , nullptr
+               ) // apparently (according to the docs), signal_subscribe can't fail
+       ) { }
+
+       ~ dbus_signal_listener ()
+       {
+               g_dbus_connection_signal_unsubscribe (connection, signal_subscription);
+               g_object_unref (connection);
+       }
+};
+
+void * timed_quit (void * userdata)
+{
+       /* Split the wait into smaller periods so we can notice along
+        * the way that we've received a signal and interrupt the wait. */
+
+       const auto timeout = reinterpret_cast <intptr_t> (userdata);
+       for (auto i = 0; i < timeout; ++i) {
+               std::this_thread::sleep_for (std::chrono::seconds (1));
+               if (global_stop)
+                       return nullptr;
+       }
+
+       timed_out = true;
+       g_main_loop_quit (loop);
+       return nullptr;
+}
+
+static void set_affinity (pthread_t pt, size_t affinity)
+{
+       /* We're setting specific affinities to make sure that in the scenario
+        * where we want to use the entire system's CPU resources, we actually
+        * have them assigned to us. The OS is not guaranteed to do this on its
+        * own (even if we spawn as many threads as the system has cores) and
+        * needs to be asked. Linux seems to give a guarantee that this wish
+        * is respected. */
+
+       cpu_set_t cpu_set;
+       CPU_ZERO (& cpu_set);
+       CPU_SET (affinity, & cpu_set);
+       pthread_setaffinity_np (pt, sizeof cpu_set, & cpu_set);
+}
+
+static inline pthread_t spawn_thread (thread_func_t * func, int param)
+{
+       pthread_t pt;
+       if (pthread_create(& pt, nullptr, func, reinterpret_cast <void *> (param)))
+               throw_errno ("pthread_create");
+       return pt;
+}
+
+static std::vector <pthread_t> spawn_threads (size_t processor_count, thread_func_t * func, size_t test_time)
+{
+       /* We're using pthread_t directly instead of std::thread (which on systems
+        * compliant with POSIX is also usually just a wrapper around pthread_t) as
+        * we want to control thread CPU affinity - a platform-specific feature that
+        * std::thread doesn't expose. Doesn't stop us from using std::this_thread
+        * later where convenient though. */
+
+       std::vector <pthread_t> threads;
+       threads.reserve (processor_count + 1);
+       for (size_t i = 0; i < processor_count; ++i) {
+               threads.push_back (spawn_thread (func, i));
+               set_affinity (threads.back (), i);
+       }
+
+       /* Give stability monitor a limited time to notice our resource usage.
+        * It should detect anomalies without excessive delay. */
+       threads.push_back (spawn_thread (timed_quit, test_time));
+
+       return threads;
+}
+
+bool run_test (thread_func_t * func, size_t processors, const dbus_signal_handler * handler, size_t test_time)
+{
+       global_stop = false;
+       timed_out = false;
+
+       auto threads = spawn_threads (processors, func, test_time);
+       dbus_signal_listener dsl (handler);
+
+       loop = g_main_loop_new (nullptr, false);
+       if (!loop)
+               throw std::runtime_error ("g_main_loop_new failed");
+       g_main_loop_run (loop);
+       global_stop = true;
+
+       for (auto thread : threads)
+               pthread_join (thread, nullptr);
+       loop = nullptr;
+
+       return timed_out;
+}
+
+// FIXME: throttling doesn't end so only works correctly on the last test in a batch
+void throttle (size_t percent, pid_t pid)
+{
+       int current_sig = SIGSTOP;
+       while (true) {
+               std::this_thread::sleep_for (std::chrono::microseconds (100 * percent));
+               if (kill (pid, current_sig) == -1)
+                       break;
+               percent = 100 - percent;
+               current_sig = (SIGSTOP + SIGCONT) - current_sig;
+       }
+}
+
+bool run_throttled (size_t percent, bool (*func) ())
+{
+       /* The child throttles the parent, not the other way around.
+        * This is because the parent is the one to return a relevant
+        * value to the caller. */
+       pid_t parent_pid = getpid ();
+       int child_pid = fork ();
+       if (child_pid == -1)
+               throw_errno ("fork failed");
+
+       if (!child_pid) {
+               throttle (percent, parent_pid);
+               exit (EXIT_SUCCESS);
+       } else
+               return func ();
+}
+
+static bool run_single_testcase (size_t index, const std::pair <bool (*)(), std::string> & test_case)
+{
+       std::cout << '#' << index << ": " << test_case.second << "... " << std::flush;
+       std::this_thread::sleep_for (std::chrono::seconds (2)); // let resource usage from initialisation or previous attempts dissipate
+
+       bool result;
+       try {
+               result = test_case.first ();
+       } catch (const std::exception & ex) {
+               std::cerr << ex.what () << std::endl;
+               result = false;
+       }
+       std::cout << (result ? "PASS" : "FAIL") << std::endl;
+       return result;
+}
+
+static int print_usage (const char * progname, size_t max_case)
+{
+       std::cout << "Usage: " << progname << " [1-" << max_case << ']' << std::endl;
+       return EXIT_FAILURE;
+}
+
+int standard_main (const std::vector <std::pair <bool (*)(), std::string>> & test_cases, int argc, char ** argv)
+{
+       auto print_usage = [&] () { std::cout << "Usage: " << argv[0] << " [1-" << test_cases.size () << ']' << std::endl; return EXIT_FAILURE; };
+
+       if (argc > 2)
+               return print_usage ();
+
+       if (argc == 2) {
+               size_t i;
+               try {
+                       i = std::stoi (argv[1]) - 1;
+               } catch (std::invalid_argument & ex) {
+                       return print_usage ();
+               }
+               if (i >= test_cases.size ())
+                       return print_usage ();
+               return run_single_testcase (i + 1, test_cases.at(i)) ? EXIT_SUCCESS : EXIT_FAILURE;
+       }
+
+       size_t i = 0, pass = 0, fail = 0;
+       for (const auto & test_case : test_cases)
+               ++ (run_single_testcase (++i, test_case) ? pass : fail);
+       std::cout << std::endl
+                 << pass << '/' << i << " tests passed" << std::endl;
+
+       return (pass == i) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/tests/test-stability.hpp b/tests/test-stability.hpp
new file mode 100644 (file)
index 0000000..8546de3
--- /dev/null
@@ -0,0 +1,50 @@
+#pragma once
+
+// C
+#include <cstdlib>
+
+// C++
+#include <chrono>
+#include <string>
+#include <vector>
+
+// POSIX
+#include <unistd.h>
+
+// Gnome
+#include <glib.h>
+#include <gio/gio.h>
+
+using namespace std::string_literals;
+
+static constexpr size_t TEST_TIME_PEAK = 5; // not actually a duration, but the number of 1s periods
+static constexpr size_t TEST_TIME_AVG = 10; // a fair bit longer than PEAK
+static constexpr auto CATCH_STRAGGLER_SIGNALS_TIME = std::chrono::seconds (3);
+
+static const std::string SIGNAL_NAME = "AbnormalityDetected";
+
+struct managed_GError {
+       /* Functions typically work with (GError **) so we can't
+        * use std::unique_ptr or the other <memory> classes for
+        * automated memory management (we only really care about
+        * the destructor though). */
+       GError * err { nullptr };
+       ~ managed_GError () { if (err) g_error_free (err); }
+};
+
+struct dbus_signal_handler {
+       virtual compl dbus_signal_handler ( ) { }
+       virtual bool match (const gchar * objpath, const gchar * iface, GVariant * parameters) const = 0;
+};
+
+extern bool timed_out;
+extern bool global_stop;
+extern GMainLoop * loop;
+
+typedef void * thread_func_t (void *);
+
+bool run_throttled (size_t percent, bool (*func) ());
+bool run_test (thread_func_t * func, size_t processors, const dbus_signal_handler * handler, size_t test_time);
+
+int standard_main (const std::vector <std::pair <bool (*) (), std::string>> & test_cases, int argc, char ** argv);
+