Removed linux_code
Change-Id: I7327828d2c9efdf03172d1b90f49d5c51fbba86e
diff --git a/aos/ipc_lib/ipc_stress_test.cc b/aos/ipc_lib/ipc_stress_test.cc
new file mode 100644
index 0000000..aa5d9c5
--- /dev/null
+++ b/aos/ipc_lib/ipc_stress_test.cc
@@ -0,0 +1,256 @@
+#include <errno.h>
+#include <libgen.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <chrono>
+#include <string>
+
+#include "aos/die.h"
+#include "aos/libc/aos_strsignal.h"
+#include "aos/libc/dirname.h"
+#include "aos/logging/logging.h"
+#include "aos/mutex/mutex.h"
+#include "aos/time/time.h"
+#include "aos/type_traits/type_traits.h"
+#include "aos/ipc_lib/core_lib.h"
+#include "aos/testing/test_shm.h"
+
+// This runs all of the IPC-related tests in a bunch of parallel processes for a
+// while and makes sure that they don't fail. It also captures the stdout and
+// stderr output from each test run and only prints it out (not interleaved with
+// the output from any other run) if the test fails.
+//
+// They have to be run in separate processes because (in addition to various
+// parts of our code not being thread-safe...) gtest does not like multiple
+// threads.
+//
+// It's written in C++ for performance. We need actual OS-level parallelism for
+// this to work, which means that Ruby's out because it doesn't have good
+// support for doing that. My Python implementation ended up pretty heavily disk
+// IO-bound, which is a bad way to test CPU contention.
+
+namespace aos {
+
+namespace chrono = ::std::chrono;
+
+// Each test is represented by the name of the test binary and then any
+// arguments to pass to it.
+// Using --gtest_filter is a bad idea because it seems to result in a lot of
+// swapping which causes everything to be disk-bound (at least for me).
+static const size_t kTestMaxArgs = 10;
+static const char * kTests[][kTestMaxArgs] = {
+ {"queue_test"},
+ {"condition_test"},
+ {"mutex_test"},
+ {"raw_queue_test"},
+};
+static const size_t kTestsLength = sizeof(kTests) / sizeof(kTests[0]);
+// These arguments get inserted before any per-test arguments.
+static const char *kDefaultArgs[] = {
+ "--gtest_repeat=30",
+ "--gtest_shuffle",
+};
+
+// How many test processes to run at a time.
+static const int kTesters = 100;
+// How long to test for.
+static constexpr monotonic_clock::duration kTestTime = chrono::seconds(30);
+
+// The structure that gets put into shared memory and then referenced by all of
+// the child processes.
+struct Shared {
+ Shared(const monotonic_clock::time_point stop_time)
+ : stop_time(stop_time), total_iterations(0) {}
+
+ // Synchronizes access to stdout/stderr to avoid interleaving failure
+ // messages.
+ Mutex output_mutex;
+
+ // When to stop.
+ monotonic_clock::time_point stop_time;
+
+ // The total number of iterations. Updated by each child as it finishes.
+ int total_iterations;
+ // Sychronizes writes to total_iterations
+ Mutex total_iterations_mutex;
+
+ const char *path;
+};
+static_assert(shm_ok<Shared>::value,
+ "it's going to get shared between forked processes");
+
+// Gets called after each child forks to run a test.
+void __attribute__((noreturn)) DoRunTest(
+ Shared *shared, const char *(*test)[kTestMaxArgs], int pipes[2]) {
+ if (close(pipes[0]) == -1) {
+ PDie("close(%d) of read end of pipe failed", pipes[0]);
+ }
+ if (close(STDIN_FILENO) == -1) {
+ PDie("close(STDIN_FILENO(=%d)) failed", STDIN_FILENO);
+ }
+ if (dup2(pipes[1], STDOUT_FILENO) == -1) {
+ PDie("dup2(%d, STDOUT_FILENO(=%d)) failed", pipes[1], STDOUT_FILENO);
+ }
+ if (dup2(pipes[1], STDERR_FILENO) == -1) {
+ PDie("dup2(%d, STDERR_FILENO(=%d)) failed", pipes[1], STDERR_FILENO);
+ }
+
+ size_t size = kTestMaxArgs;
+ size_t default_size = sizeof(kDefaultArgs) / sizeof(kDefaultArgs[0]);
+ // There's no chance to free this because we either exec or Die.
+ const char **args = new const char *[size + default_size + 1];
+ // The actual executable to run.
+ ::std::string executable;
+ int i = 0;
+ for (size_t test_i = 0; test_i < size; ++test_i) {
+ const char *c = (*test)[test_i];
+ if (i == 0) {
+ executable = ::std::string(shared->path) + "/" + c;
+ args[0] = executable.c_str();
+ for (const ::std::string &ci : kDefaultArgs) {
+ args[++i] = ci.c_str();
+ }
+ } else {
+ args[i] = c;
+ }
+ ++i;
+ }
+ args[size] = NULL;
+ execv(executable.c_str(), const_cast<char *const *>(args));
+ PDie("execv(%s, %p) failed", executable.c_str(), args);
+}
+
+void DoRun(Shared *shared) {
+ int iterations = 0;
+ // An iterator pointing to a random one of the tests.
+ // We randomize based on PID because otherwise they all end up running the
+ // same test at the same time for the whole test.
+ const char *(*test)[kTestMaxArgs] = &kTests[getpid() % kTestsLength];
+ int pipes[2];
+ while (monotonic_clock::now() < shared->stop_time) {
+ if (pipe(pipes) == -1) {
+ PDie("pipe(%p) failed", &pipes);
+ }
+ switch (fork()) {
+ case 0: // in runner
+ DoRunTest(shared, test, pipes);
+ case -1:
+ PDie("fork() failed");
+ }
+
+ if (close(pipes[1]) == -1) {
+ PDie("close(%d) of write end of pipe failed", pipes[1]);
+ }
+
+ ::std::string output;
+ char buffer[2048];
+ while (true) {
+ ssize_t ret = read(pipes[0], &buffer, sizeof(buffer));
+ if (ret == 0) { // EOF
+ if (close(pipes[0]) == -1) {
+ PDie("close(%d) of pipe at EOF failed", pipes[0]);
+ }
+ break;
+ } else if (ret == -1) {
+ PDie("read(%d, %p, %zd) failed", pipes[0], &buffer, sizeof(buffer));
+ }
+ output += ::std::string(buffer, ret);
+ }
+
+ int status;
+ while (true) {
+ if (wait(&status) == -1) {
+ if (errno == EINTR) continue;
+ PDie("wait(%p) in child failed", &status);
+ } else {
+ break;
+ }
+ }
+ if (WIFEXITED(status)) {
+ if (WEXITSTATUS(status) != 0) {
+ MutexLocker sync(&shared->output_mutex);
+ fprintf(stderr, "Test %s exited with status %d. output:\n",
+ (*test)[0], WEXITSTATUS(status));
+ fputs(output.c_str(), stderr);
+ }
+ } else if (WIFSIGNALED(status)) {
+ MutexLocker sync(&shared->output_mutex);
+ fprintf(stderr, "Test %s terminated by signal %d: %s.\n", (*test)[0],
+ WTERMSIG(status), aos_strsignal(WTERMSIG(status)));
+ fputs(output.c_str(), stderr);
+ } else {
+ CHECK(WIFSTOPPED(status));
+ Die("Test %s was stopped.\n", (*test)[0]);
+ }
+
+ ++test;
+ if (test == kTests + 1) test = kTests;
+ ++iterations;
+ }
+ {
+ MutexLocker sync(&shared->total_iterations_mutex);
+ shared->total_iterations += iterations;
+ }
+}
+
+void Run(Shared *shared) {
+ switch (fork()) {
+ case 0: // in child
+ DoRun(shared);
+ _exit(EXIT_SUCCESS);
+ case -1:
+ PDie("fork() of child failed");
+ }
+}
+
+int Main(int argc, char **argv) {
+ if (argc < 1) {
+ fputs("need an argument\n", stderr);
+ return EXIT_FAILURE;
+ }
+
+ ::aos::testing::TestSharedMemory my_shm_;
+
+ Shared *shared = static_cast<Shared *>(shm_malloc(sizeof(Shared)));
+ new (shared) Shared(monotonic_clock::now() + kTestTime);
+
+ if (asprintf(const_cast<char **>(&shared->path),
+ "%s/../tests", ::aos::libc::Dirname(argv[0]).c_str()) == -1) {
+ PDie("asprintf failed");
+ }
+
+ for (int i = 0; i < kTesters; ++i) {
+ Run(shared);
+ }
+
+ bool error = false;
+ for (int i = 0; i < kTesters; ++i) {
+ int status;
+ if (wait(&status) == -1) {
+ if (errno == EINTR) {
+ --i;
+ } else {
+ PDie("wait(%p) failed", &status);
+ }
+ }
+ if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+ error = true;
+ }
+ }
+
+ printf("Ran a total of %d tests.\n", shared->total_iterations);
+ if (error) {
+ printf("A child had a problem during the test.\n");
+ }
+ return error ? EXIT_FAILURE : EXIT_SUCCESS;
+}
+
+} // namespace aos
+
+int main(int argc, char **argv) {
+ return ::aos::Main(argc, argv);
+}