/* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */ /* * Copyright 2013-2020 Couchbase, Inc. * * 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. */ /** * Rather than hacking together a shell script or depending on some scripting * language, we'll use a simple C++ application to run 'unit-tests' * with the appropriate settings we need. */ #include #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "check_config.h" #include "mocksupport/procutil.h" #define CLIOPTS_ENABLE_CXX #include "contrib/cliopts/cliopts.h" #include #define TESTS_BASE "sock-tests;nonio-tests;rdb-tests;mc-tests;" #define PLUGIN_ENV_VAR "LCB_IOPS_NAME" #define LCB_SRCROOT_ENV_VAR "srcdir" #define DEFAULT_TEST_NAMES TESTS_BASE "unit-tests" #ifdef _WIN32 const char default_plugins_string[] = "select;iocp;libuv"; #define PATHSEP "\\" #define usleep(n) Sleep((n) / 1000) #define setenv(key, value, ignored) SetEnvironmentVariable(key, value) #else #include #include /* usleep */ const char default_plugins_string[] = "select" #if defined(HAVE_LIBEV3) || defined(HAVE_LIBEV4) ";libev" #endif #if defined(HAVE_LIBEVENT) || defined(HAVE_LIBEVENT2) ";libevent" #endif #ifdef HAVE_LIBUV ";libuv" #endif ; #define PATHSEP "/" #endif typedef std::vector< std::string > strlist; class TestConfiguration { public: TestConfiguration() : opt_debugger("debugger"), opt_plugins("plugins"), opt_jobs("jobs"), opt_srcdir("srcdir"), opt_bindir("testdir"), opt_interactive("interactive"), opt_verbose("verbose"), opt_cycles("repeat"), opt_libdir("libdir"), opt_bins("tests"), opt_realcluster("cluster"), opt_gtest_filter("gtest_filter"), opt_gtest_break_on_failure("gtest_break_on_failure"), opt_gtest_catch_exceptions("gtest_catch_exceptions") { opt_debugger.abbrev('d').description("Verbatim string to prepend to the binary command line"); opt_plugins.abbrev('p') .description("semicolon-delimited list of plugins to test") .setDefault(default_plugins_string); opt_jobs.abbrev('j').description("Execute this many processes concurrently").setDefault(1); opt_srcdir.abbrev('S') .description("root directory of source tree (for locating mock)") .setDefault(getEffectiveSrcroot()); opt_bindir.abbrev('T') .description("Directory where test binaries are located") .setDefault(getEffectiveTestdir()); opt_interactive.abbrev('I').description( "Set this to true when using an interactive debugger. This unblocks stdin"); opt_bins.abbrev('B').description("semicolon delimited list of tests to run").setDefault(DEFAULT_TEST_NAMES); opt_cycles.abbrev('n').description("Number of times to run the tests").setDefault(1); opt_libdir.abbrev('L').description("Directory where plugins are located. Useful on OS X"); opt_realcluster.abbrev('C').description("Path to real cluster"); opt_verbose.abbrev('v'); } ~TestConfiguration() {} static void splitSemicolonString(const std::string &s, strlist &l) { std::string cur; for (const char *c = s.c_str(); *c; c++) { if (*c == ';') { l.push_back(cur); cur.clear(); continue; } cur += *c; } if (!cur.empty()) { l.push_back(cur); } } bool parseOptions(int argc, char **argv) { std::stringstream ss; cliopts::Parser parser("check-all"); parser.addOption(opt_debugger); parser.addOption(opt_plugins); parser.addOption(opt_jobs); parser.addOption(opt_srcdir); parser.addOption(opt_bindir); parser.addOption(opt_interactive); parser.addOption(opt_verbose); parser.addOption(opt_cycles); parser.addOption(opt_libdir); parser.addOption(opt_bins); parser.addOption(opt_realcluster); parser.addOption(opt_gtest_filter); parser.addOption(opt_gtest_break_on_failure); parser.addOption(opt_gtest_catch_exceptions); if (!parser.parse(argc, argv, false)) { return false; } using std::string; using std::vector; const vector< string > &args = parser.getRestArgs(); for (size_t ii = 0; ii < args.size(); ii++) { ss << args[ii] << " "; } if (!opt_gtest_filter.result().empty()) { ss << " " << "--gtest_filter=" << opt_gtest_filter.result(); } if (opt_gtest_break_on_failure.passed()) { ss << " " << "--gtest_break_on_failure=1"; } if (opt_gtest_catch_exceptions.passed()) { ss << " " << "--gtest_catch_exceptions=1"; } binOptions = ss.str(); srcroot = opt_srcdir.result(); testdir = opt_bindir.result(); debugger = opt_debugger.result(); libDir = opt_libdir.result(); realClusterEnv = opt_realcluster.result(); // Verbosity isVerbose = opt_verbose.result(); // isInteractive isInteractive = opt_interactive.result(); // Jobs maxJobs = opt_jobs.result(); maxCycles = opt_cycles.result(); setJobsFromEnvironment(); // Plugin list: splitSemicolonString(opt_plugins.result(), plugins); // Test names: splitSemicolonString(opt_bins.result(), testnames); // Set the library dir, if not set if (libDir.empty()) { libDir = testdir + "/../" + "lib"; } return true; } // Sets up the command line, appending any debugger info and paths std::string setupCommandline(std::string &name) { std::stringstream ss; std::string ret; if (!debugger.empty()) { ss << debugger << " "; } ss << testdir << PATHSEP << name; if (!binOptions.empty()) { ss << " " << binOptions; } return ss.str(); } // Options passed to the binary itself std::string binOptions; std::string srcroot; std::string testdir; std::string debugger; std::string libDir; std::string realClusterEnv; strlist plugins; strlist testnames; bool isVerbose; bool isInteractive; int maxJobs; int maxCycles; int getVerbosityLevel() { return opt_verbose.numSpecified(); } private: cliopts::StringOption opt_debugger; cliopts::StringOption opt_plugins; cliopts::UIntOption opt_jobs; cliopts::StringOption opt_srcdir; cliopts::StringOption opt_bindir; cliopts::BoolOption opt_interactive; cliopts::BoolOption opt_verbose; cliopts::IntOption opt_cycles; cliopts::StringOption opt_libdir; cliopts::StringOption opt_bins; cliopts::StringOption opt_realcluster; cliopts::StringOption opt_gtest_filter; cliopts::BoolOption opt_gtest_break_on_failure; cliopts::BoolOption opt_gtest_catch_exceptions; void setJobsFromEnvironment() { char *tmp = getenv("MAKEFLAGS"); if (tmp == NULL || *tmp == '\0') { return; } if (strstr(tmp, "-j")) { maxJobs = 32; } else { maxJobs = 1; } } std::string getEffectiveSrcroot() { const char *tmp = getenv(LCB_SRCROOT_ENV_VAR); if (tmp && *tmp) { return tmp; } return getDefaultSrcroot(); } std::string getEffectiveTestdir() { const char *tmp = getenv("outdir"); if (tmp && *tmp) { return tmp; } return getDefaultTestdir(); } #ifndef _WIN32 // Evaluated *before* std::string getDefaultSrcroot() { return TEST_SRC_DIR; } std::string getDefaultTestdir() { return (srcroot + PATHSEP) + "tests"; } #else std::string getSelfDirname() { DWORD result; char pathbuf[4096] = {0}; result = GetModuleFileName(NULL, pathbuf, sizeof(pathbuf)); assert(result > 0); assert(result < sizeof(pathbuf)); for (DWORD ii = result; ii; ii--) { if (pathbuf[ii] == '\\') { break; } pathbuf[ii] = '\0'; } return pathbuf; } // For windows, we reside in the same directory as the binaries std::string getDefaultSrcroot() { std::string dir = getSelfDirname(); std::stringstream ss; ss << dir; int components_max; #ifdef _MSC_VER // Visual Studio projects are usually something like: // $ROOT\VS\10.0\bin\Debug // (1)..\bin, (2)..\10.0, (3)..\VS, (4)..\$ROOT components_max = 4; #else // For MinGW, it's something like $ROOT\$BUILD\bin; so // (1) ..\BUILD, (2) ..\ROOT components_max = 2; #endif for (int ii = 0; ii < components_max; ii++) { ss << PATHSEP << ".."; } return ss.str(); } std::string getDefaultTestdir() { return getSelfDirname(); } #endif }; static void setPluginEnvironment(std::string &name) { const char *v = NULL; if (name != "default") { v = name.c_str(); } setenv(PLUGIN_ENV_VAR, v, 1); fprintf(stderr, "%s=%s ... ", PLUGIN_ENV_VAR, name.c_str()); struct lcb_cntl_iops_info_st ioi; memset(&ioi, 0, sizeof(ioi)); lcb_STATUS err = lcb_cntl(NULL, LCB_CNTL_GET, LCB_CNTL_IOPS_DEFAULT_TYPES, &ioi); if (err != LCB_SUCCESS) { fprintf(stderr, "LCB Error 0x%x\n", err); } else { fprintf(stderr, "Plugin ID: 0x%x\n", ioi.v.v0.effective); } } static void setLinkerEnvironment(std::string &path) { #ifdef _WIN32 if (false) { return; } #endif if (path.empty()) { return; } #if __APPLE__ const char *varname = "DYLD_LIBRARY_PATH"; #else const char *varname = "LD_LIBRARY_PATH"; #endif const char *existing = getenv(varname); std::string newenv; if (existing) { newenv += existing; newenv += ":"; } newenv += path; fprintf(stderr, "export %s=%s\n", varname, newenv.c_str()); setenv(varname, newenv.c_str(), 1); } struct Process { child_process_t proc_; std::string commandline; std::string logfileName; std::string pluginName; std::string testName; bool exitedOk; bool verbose; Process(std::string &plugin, std::string &name, std::string &cmd, TestConfiguration &config) { this->pluginName = plugin; this->testName = name; this->commandline = cmd; this->verbose = config.isVerbose; proc_.interactive = config.isInteractive; this->logfileName = "check-all-" + pluginName + "-" + testName + ".log"; } void writeLog(const char *msg) { std::ofstream out(logfileName.c_str(), std::ios::app); out << msg << std::endl; out.close(); } void setupPointers() { memset(&proc_, 0, sizeof(proc_)); proc_.name = commandline.c_str(); if (!verbose) { proc_.redirect = logfileName.c_str(); } } }; class TestScheduler { public: TestScheduler(unsigned int lim) : limit(lim) {} typedef std::list< Process * > proclist; std::vector< Process > _all; proclist executing; proclist scheduled; proclist completed; unsigned int limit; void schedule(Process proc) { _all.push_back(proc); } bool runAll() { proclist::iterator iter; scheduleAll(); while (!(executing.empty() && scheduled.empty())) { while ((!scheduled.empty()) && executing.size() < limit) { Process *proc = scheduled.front(); scheduled.pop_front(); invokeScheduled(proc); } // Wait for them to complete proclist to_remove_e; for (iter = executing.begin(); iter != executing.end(); iter++) { Process *cur = *iter; int rv = wait_process(&cur->proc_, -1); if (rv == 0) { char msg[2048]; cur->exitedOk = cur->proc_.status == 0; snprintf(msg, 2048, "REAP [%s] '%s' .. %s", cur->pluginName.c_str(), cur->commandline.c_str(), cur->exitedOk ? "OK" : "FAIL"); cur->writeLog(msg); fprintf(stderr, "%s\n", msg); cleanup_process(&cur->proc_); to_remove_e.push_back(cur); } } for (iter = to_remove_e.begin(); iter != to_remove_e.end(); iter++) { executing.remove(*iter); completed.push_back(*iter); } usleep(5000); } for (iter = completed.begin(); iter != completed.end(); iter++) { if (!(*iter)->exitedOk) { return false; } } return true; } private: void scheduleAll() { for (unsigned int ii = 0; ii < _all.size(); ii++) { Process *p = &_all[ii]; scheduled.push_back(p); } } void invokeScheduled(Process *proc) { proc->setupPointers(); setPluginEnvironment(proc->pluginName); char msg[2048]; snprintf(msg, 2048, "START [%s] '%s'", proc->pluginName.c_str(), proc->commandline.c_str()); proc->writeLog(msg); fprintf(stderr, "%s\n", msg); int rv = create_process(&proc->proc_); if (rv < 0) { snprintf(msg, 2048, "FAIL couldn't invoke [%s] '%s'", proc->pluginName.c_str(), proc->commandline.c_str()); proc->writeLog(msg); fprintf(stderr, "%s\n", msg); proc->exitedOk = false; completed.push_back(proc); return; } else { executing.push_back(proc); } } }; static bool runSingleCycle(TestConfiguration &config) { TestScheduler scheduler(config.maxJobs); setLinkerEnvironment(config.libDir); for (strlist::iterator iter = config.plugins.begin(); iter != config.plugins.end(); iter++) { fprintf(stderr, "Testing with plugin '%s'\n", iter->c_str()); #ifdef __linux__ { char buf[1024] = {0}; sprintf(buf, "ldd %s/libcouchbase_%s.so", config.libDir.c_str(), iter->c_str()); fprintf(stderr, "%s\n", buf); system(buf); } #endif for (strlist::iterator iterbins = config.testnames.begin(); iterbins != config.testnames.end(); iterbins++) { std::string cmdline = config.setupCommandline(*iterbins); fprintf(stderr, "Command line '%s'\n", cmdline.c_str()); scheduler.schedule(Process(*iter, *iterbins, cmdline, config)); } } return scheduler.runAll(); } int main(int argc, char **argv) { #ifndef _WIN32 signal(SIGPIPE, SIG_IGN); #endif TestConfiguration config; if (!config.parseOptions(argc, argv)) { exit(EXIT_FAILURE); } // Set the environment for 'srcdir' std::stringstream ss; fprintf(stderr, "export %s=%s\n", LCB_SRCROOT_ENV_VAR, config.srcroot.c_str()); setenv(LCB_SRCROOT_ENV_VAR, config.srcroot.c_str(), 1); fprintf(stderr, "export LCB_VERBOSE_TESTS=1\n"); setenv("LCB_VERBOSE_TESTS", "1", 1); char loglevel_s[4096] = {0}; if (config.getVerbosityLevel() > 0) { sprintf(loglevel_s, "%d", config.getVerbosityLevel()); setenv("LCB_LOGLEVEL", loglevel_s, 0); fprintf(stderr, "export LCB_LOGLEVEL=%s\n", loglevel_s); } if (!config.realClusterEnv.empty()) { // format the string setenv("LCB_TEST_CLUSTER_CONF", config.realClusterEnv.c_str(), 0); fprintf(stderr, "export LCB_TEST_CLUSTER_CONF=%s\n", config.realClusterEnv.c_str()); } for (int ii = 0; ii < config.maxCycles; ii++) { if (!runSingleCycle(config)) { return EXIT_FAILURE; } } return EXIT_SUCCESS; }