Support for real MPI unit tests
authorTeemu Murtola <teemu.murtola@gmail.com>
Fri, 13 May 2016 03:38:49 +0000 (06:38 +0300)
committerMark Abraham <mark.j.abraham@gmail.com>
Wed, 25 May 2016 14:39:46 +0000 (16:39 +0200)
Add support for unit tests where the same test code can be run under
both MPI and thread-MPI with a specified number of ranks.
Currently, all tests within the same binary should use the same number
of ranks.

Some C++11 magic is used to get a function pointer to the current test
body, but it seems to work with all the compilers in Jenkins.
Without it, the implementation would either be much more complicated, or
would require a specific fixture type for these tests.

Change-Id: I2957c4cc17cae85bfa2ecf36624160257a17cae3

src/external/gmock-1.7.0/CMakeLists.txt
src/programs/mdrun/tests/moduletest.cpp
src/testutils/CMakeLists.txt
src/testutils/TestMacros.cmake
src/testutils/mpitest.cpp [new file with mode: 0644]
src/testutils/mpitest.h [new file with mode: 0644]
src/testutils/tests/CMakeLists.txt
src/testutils/tests/mpitest.cpp [new file with mode: 0644]

index a02095ee5b5dee5b2fa703046bd0c45ead97a495..257c3582c4b7903a7ef337eb3961ae89e8fb5f41 100644 (file)
@@ -70,6 +70,11 @@ find_package(Threads)
 set(PTHREADS_LIBRARIES)
 if (CMAKE_USE_PTHREADS_INIT)
     set(PTHREADS_LIBRARIES ${CMAKE_THREAD_LIBS_INIT})
+    list(APPEND GMOCK_COMPILE_DEFINITIONS "GTEST_HAS_PTHREAD=1")
+    set(GTEST_IS_THREADSAFE 1)
+else()
+    list(APPEND GMOCK_COMPILE_DEFINITIONS "GTEST_HAS_PTHREAD=0")
+    set(GTEST_IS_THREADSAFE 0)
 endif()
 
 # Skip variadic implementation of matchers if using GCC < 4.7 due to
@@ -106,3 +111,4 @@ set(GMOCK_INCLUDE_DIRS ${GMOCK_INCLUDE_DIRS} PARENT_SCOPE)
 set(GTEST_INCLUDE_DIRS ${GTEST_INCLUDE_DIRS} PARENT_SCOPE)
 set(GMOCK_COMPILE_DEFINITIONS ${GMOCK_COMPILE_DEFINITIONS} PARENT_SCOPE)
 set(GMOCK_COMPILE_FLAGS "${GMOCK_COMPILE_FLAGS}" PARENT_SCOPE)
+set(GTEST_IS_THREADSAFE "${GTEST_IS_THREADSAFE}" PARENT_SCOPE)
index cf1c3ada4ba36f10ee4f030eba1192600862ecbb..5d0760448b990ef85eb5a11b27990d11624cc226 100644 (file)
@@ -59,6 +59,7 @@
 
 #include "testutils/cmdlinetest.h"
 #include "testutils/integrationtests.h"
+#include "testutils/mpitest.h"
 #include "testutils/testoptions.h"
 
 namespace gmx
@@ -73,10 +74,6 @@ namespace test
 namespace
 {
 
-#if GMX_THREAD_MPI || defined(DOXYGEN)
-//! Number of tMPI threads for child mdrun call.
-int g_numThreads = 1;
-#endif
 #if GMX_OPENMP || defined(DOXYGEN)
 //! Number of OpenMP threads for child mdrun call.
 int g_numOpenMPThreads = 1;
@@ -85,10 +82,6 @@ int g_numOpenMPThreads = 1;
 GMX_TEST_OPTIONS(MdrunTestOptions, options)
 {
     GMX_UNUSED_VALUE(options);
-#if GMX_THREAD_MPI
-    options->addOption(IntegerOption("nt").store(&g_numThreads)
-                           .description("Number of thread-MPI threads/ranks for child mdrun calls"));
-#endif
 #if GMX_OPENMP
     options->addOption(IntegerOption("nt_omp").store(&g_numOpenMPThreads)
                            .description("Number of OpenMP threads for child mdrun calls"));
@@ -237,18 +230,14 @@ SimulationRunner::callMdrun(const CommandLine &callerRef)
 
 #if GMX_MPI
 #  if GMX_GPU != GMX_GPU_NONE
-#    if GMX_THREAD_MPI
-    int         numGpusNeeded = g_numThreads;
-#    else   /* Must be real MPI */
-    int         numGpusNeeded = gmx_node_num();
-#    endif
+    const int   numGpusNeeded = getNumberOfTestMpiRanks();
     std::string gpuIdString(numGpusNeeded, '0');
     caller.addOption("-gpu_id", gpuIdString.c_str());
 #  endif
 #endif
 
 #if GMX_THREAD_MPI
-    caller.addOption("-ntmpi", g_numThreads);
+    caller.addOption("-ntmpi", getNumberOfTestMpiRanks());
 #endif
 
 #if GMX_OPENMP
@@ -262,14 +251,7 @@ SimulationRunner::callMdrun(const CommandLine &callerRef)
      * node regardless of the number of ranks, because that's true in
      * Jenkins and for most developers running the tests. */
     int numberOfNodes = 1;
-#if GMX_THREAD_MPI
-    /* Can't use gmx_node_num() because it is only valid after spawn of thread-MPI threads */
-    int numberOfRanks = g_numThreads;
-#elif GMX_LIB_MPI
-    int numberOfRanks = gmx_node_num();
-#else
-    int numberOfRanks = 1;
-#endif
+    int numberOfRanks = getNumberOfTestMpiRanks();
     if (numberOfRanks > numberOfNodes && !gmx_multiple_gpu_per_node_supported())
     {
         if (gmx_node_rank() == 0)
index eb0fa8d3ad17cd0b8b2bc98eb7fc723bc49433c5..e6ff8bd9314da767935974137ece374174a0d9d9 100644 (file)
@@ -38,6 +38,7 @@ set(TESTUTILS_SOURCES
     integrationtests.cpp
     interactivetest.cpp
     mpi-printer.cpp
+    mpitest.cpp
     refdata.cpp
     refdata-xml.cpp
     stringtest.cpp
index faff8af3d290a8416d7635334253bbfd31806762..c0c0f13809bf08a06b62a9ab5bfd44184c7529f1 100644 (file)
@@ -148,7 +148,7 @@ function (gmx_register_mpi_integration_test NAME EXENAME NUMPROC)
         elseif(GMX_THREAD_MPI)
             add_test(NAME ${NAME}
                 COMMAND
-                $<TARGET_FILE:${EXENAME}> -nt ${NUMPROC}
+                $<TARGET_FILE:${EXENAME}> -ntmpi ${NUMPROC}
                 --gtest_output=xml:${CMAKE_BINARY_DIR}/Testing/Temporary/${NAME}.xml
                 )
             set_tests_properties(${testname} PROPERTIES LABELS "MpiIntegrationTest")
@@ -165,3 +165,11 @@ function (gmx_add_unit_test NAME EXENAME)
     gmx_add_gtest_executable(${EXENAME} ${ARGN})
     gmx_register_unit_test(${NAME} ${EXENAME})
 endfunction()
+
+function (gmx_add_mpi_unit_test NAME EXENAME RANKS)
+    if (GMX_MPI OR (GMX_THREAD_MPI AND GTEST_IS_THREADSAFE))
+        gmx_add_gtest_executable(${EXENAME} MPI ${ARGN})
+        # TODO: This function needs a new name.
+        gmx_register_mpi_integration_test(${NAME} ${EXENAME} ${RANKS})
+    endif()
+endfunction()
diff --git a/src/testutils/mpitest.cpp b/src/testutils/mpitest.cpp
new file mode 100644 (file)
index 0000000..959b9c5
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * This file is part of the GROMACS molecular simulation package.
+ *
+ * Copyright (c) 2016, by the GROMACS development team, led by
+ * Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
+ * and including many others, as listed in the AUTHORS file in the
+ * top-level source directory and at http://www.gromacs.org.
+ *
+ * GROMACS is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1
+ * of the License, or (at your option) any later version.
+ *
+ * GROMACS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GROMACS; if not, see
+ * http://www.gnu.org/licenses, or write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
+ *
+ * If you want to redistribute modifications to GROMACS, please
+ * consider that scientific software is very special. Version
+ * control is crucial - bugs must be traceable. We will be happy to
+ * consider code for inclusion in the official distribution, but
+ * derived work must not be called official GROMACS. Details are found
+ * in the README & COPYING files - if they are missing, get the
+ * official version at http://www.gromacs.org.
+ *
+ * To help us fund GROMACS development, we humbly ask that you cite
+ * the research papers on the package. Check out http://www.gromacs.org.
+ */
+/*! \internal \file
+ * \brief
+ * Implements functions in mpitest.h.
+ *
+ * \author Teemu Murtola <teemu.murtola@gmail.com>
+ * \ingroup module_testutils
+ */
+#include "gmxpre.h"
+
+#include "mpitest.h"
+
+#include "config.h"
+
+#include <gtest/gtest.h>
+
+#include "thread_mpi/tmpi.h"
+
+#include "gromacs/options/basicoptions.h"
+#include "gromacs/options/ioptionscontainer.h"
+#include "gromacs/utility/basenetwork.h"
+#include "gromacs/utility/exceptions.h"
+
+#include "testutils/testoptions.h"
+
+namespace gmx
+{
+namespace test
+{
+
+#if GMX_THREAD_MPI
+
+namespace
+{
+
+//! Number of tMPI threads.
+int g_numThreads = 1;
+//! \cond
+GMX_TEST_OPTIONS(ThreadMpiTestOptions, options)
+{
+    options->addOption(IntegerOption("ntmpi").store(&g_numThreads)
+                           .description("Number of thread-MPI threads/ranks for the test"));
+}
+//! \endcond
+
+//! Thread entry function for other thread-MPI threads.
+void threadStartFunc(void *data)
+{
+    std::function<void()> &testBody = *reinterpret_cast<std::function<void()> *>(data);
+    try
+    {
+        testBody();
+    }
+    GMX_CATCH_ALL_AND_EXIT_WITH_FATAL_ERROR;
+}
+
+//! Helper function for starting thread-MPI threads for a test.
+bool startThreads(std::function<void()> *testBody)
+{
+    int ret = tMPI_Init_fn(TRUE, g_numThreads, TMPI_AFFINITY_NONE,
+                           threadStartFunc, testBody);
+    return ret == TMPI_SUCCESS;
+}
+
+class InTestGuard
+{
+    public:
+        explicit InTestGuard(bool *inTest) : inTest_(inTest) { *inTest = true; }
+        ~InTestGuard() { *inTest_ = false; }
+
+    private:
+        bool *inTest_;
+};
+
+}       // namespace
+
+//! \cond internal
+bool threadMpiTestRunner(std::function<void()> testBody)
+{
+    static bool inTest = false;
+
+    if (inTest || g_numThreads <= 1)
+    {
+        return true;
+    }
+#if GMX_THREAD_MPI && !GTEST_IS_THREADSAFE
+    ADD_FAILURE()
+    << "Google Test is not thread safe on this platform. "
+    << "Cannot run multi-rank tests with thread-MPI.";
+#else
+    InTestGuard guard(&inTest);
+    if (!startThreads(&testBody))
+    {
+        return false;
+    }
+    try
+    {
+        testBody();
+    }
+    GMX_CATCH_ALL_AND_EXIT_WITH_FATAL_ERROR;
+    tMPI_Finalize();
+#endif
+    return false;
+}
+//! \endcond
+
+#endif
+
+int getNumberOfTestMpiRanks()
+{
+#if GMX_THREAD_MPI
+    return g_numThreads;
+#else
+    return gmx_node_num();
+#endif
+}
+
+} // namespace test
+} // namespace gmx
diff --git a/src/testutils/mpitest.h b/src/testutils/mpitest.h
new file mode 100644 (file)
index 0000000..20629fd
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * This file is part of the GROMACS molecular simulation package.
+ *
+ * Copyright (c) 2016, by the GROMACS development team, led by
+ * Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
+ * and including many others, as listed in the AUTHORS file in the
+ * top-level source directory and at http://www.gromacs.org.
+ *
+ * GROMACS is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1
+ * of the License, or (at your option) any later version.
+ *
+ * GROMACS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GROMACS; if not, see
+ * http://www.gnu.org/licenses, or write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
+ *
+ * If you want to redistribute modifications to GROMACS, please
+ * consider that scientific software is very special. Version
+ * control is crucial - bugs must be traceable. We will be happy to
+ * consider code for inclusion in the official distribution, but
+ * derived work must not be called official GROMACS. Details are found
+ * in the README & COPYING files - if they are missing, get the
+ * official version at http://www.gromacs.org.
+ *
+ * To help us fund GROMACS development, we humbly ask that you cite
+ * the research papers on the package. Check out http://www.gromacs.org.
+ */
+/*! \libinternal \file
+ * \brief
+ * Helper functions for MPI tests to make thread-MPI look like real MPI.
+ *
+ * \author Teemu Murtola <teemu.murtola@gmail.com>
+ * \inlibraryapi
+ * \ingroup module_testutils
+ */
+#ifndef GMX_TESTUTILS_MPITEST_H
+#define GMX_TESTUTILS_MPITEST_H
+
+#include "config.h"
+
+#include <functional>
+#include <type_traits>
+
+#include "gromacs/utility/basenetwork.h"
+
+namespace gmx
+{
+namespace test
+{
+
+/*! \brief
+ * Returns the number of MPI ranks to use for an MPI test.
+ *
+ * For thread-MPI builds, this will return the requested number of ranks
+ * even before the thread-MPI threads have been started.
+ *
+ * \ingroup module_testutils
+ */
+int getNumberOfTestMpiRanks();
+//! \cond internal
+/*! \brief
+ * Helper function for GMX_MPI_TEST().
+ *
+ * \ingroup module_testutils
+ */
+bool threadMpiTestRunner(std::function<void()> testBody);
+//! \endcond
+
+/*! \brief
+ * Declares that this test is an MPI-enabled unit test.
+ *
+ * \param[in] expectedRankCount Expected number of ranks for this test.
+ *     The test will fail if run with unsupported number of ranks.
+ *
+ * To write unit tests that run under MPI, you need to do a few things:
+ *  - Put GMX_MPI_TEST() as the first statement in your test body and
+ *    specify the number of ranks this test expects.
+ *  - Declare your unit test in CMake with gmx_add_mpi_unit_test().
+ *    Note that all tests in the binary should fulfill the conditions above,
+ *    and work with the same number of ranks.
+ * TODO: Figure out a mechanism for mixing tests with different rank counts in
+ * the same binary (possibly, also MPI and non-MPI tests).
+ *
+ * When you do the above, the following will happen:
+ *  - The test will get compiled only if thread-MPI or real MPI is enabled.
+ *  - The test will get executed on the number of ranks specified.
+ *    If you are using real MPI, the whole test binary is run under MPI and
+ *    test execution across the processes is synchronized (GMX_MPI_TEST()
+ *    actually has no effect in this case, the synchronization is handled at a
+ *    higher level).
+ *    If you are using thread-MPI, GMX_MPI_TEST() is required and it
+ *    initializes thread-MPI with the specified number of threads and runs the
+ *    rest of the test on each of the threads.
+ *
+ * You need to be extra careful for variables in the test fixture, if you use
+ * one: when run under thread-MPI, these will be shared across all the ranks,
+ * while under real MPI, these are naturally different for each process.
+ * Local variables in the test body are private to each rank in both cases.
+ *
+ * Currently, it is not possible to specify the number of ranks as one, because
+ * that will lead to problems with (at least) thread-MPI, but such tests can be
+ * written as serial tests anyways.
+ *
+ * \ingroup module_testutils
+ */
+#if GMX_THREAD_MPI
+#define GMX_MPI_TEST(expectedRankCount) \
+    do { \
+        ASSERT_EQ(expectedRankCount, ::gmx::test::getNumberOfTestMpiRanks()); \
+        typedef std::remove_reference<decltype(*this)>::type MyTestClass; \
+        if (!::gmx::test::threadMpiTestRunner(std::bind(&MyTestClass::TestBody, this))) \
+        { \
+            return; \
+        } \
+    } while (0)
+#else
+#define GMX_MPI_TEST(expectedRankCount) \
+    ASSERT_EQ(expectedRankCount, ::gmx::test::getNumberOfTestMpiRanks())
+#endif
+
+} // namespace test
+} // namespace gmx
+
+#endif
index 6f8cb9c9b7b69a2d6725456f9623b6e3ff8963ab..3070802cbf1f6398423ba507ab41f43d4a575b9a 100644 (file)
@@ -1,7 +1,7 @@
 #
 # This file is part of the GROMACS molecular simulation package.
 #
-# Copyright (c) 2011,2012,2014,2015, by the GROMACS development team, led by
+# Copyright (c) 2011,2012,2014,2015,2016, by the GROMACS development team, led by
 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
 # and including many others, as listed in the AUTHORS file in the
 # top-level source directory and at http://www.gromacs.org.
@@ -37,3 +37,6 @@ gmx_add_unit_test(TestUtilsUnitTests testutils-test
                   refdata_tests.cpp
                   testasserts_tests.cpp
                   xvgtest_tests.cpp)
+
+gmx_add_mpi_unit_test(TestUtilsMpiUnitTests testutils-mpi-test 2
+                      mpitest.cpp)
diff --git a/src/testutils/tests/mpitest.cpp b/src/testutils/tests/mpitest.cpp
new file mode 100644 (file)
index 0000000..b810a25
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * This file is part of the GROMACS molecular simulation package.
+ *
+ * Copyright (c) 2016, by the GROMACS development team, led by
+ * Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
+ * and including many others, as listed in the AUTHORS file in the
+ * top-level source directory and at http://www.gromacs.org.
+ *
+ * GROMACS is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1
+ * of the License, or (at your option) any later version.
+ *
+ * GROMACS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GROMACS; if not, see
+ * http://www.gnu.org/licenses, or write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
+ *
+ * If you want to redistribute modifications to GROMACS, please
+ * consider that scientific software is very special. Version
+ * control is crucial - bugs must be traceable. We will be happy to
+ * consider code for inclusion in the official distribution, but
+ * derived work must not be called official GROMACS. Details are found
+ * in the README & COPYING files - if they are missing, get the
+ * official version at http://www.gromacs.org.
+ *
+ * To help us fund GROMACS development, we humbly ask that you cite
+ * the research papers on the package. Check out http://www.gromacs.org.
+ */
+/*! \internal \file
+ * \brief
+ * Tests for infrastructure for running tests under MPI.
+ *
+ * \author Teemu Murtola <teemu.murtola@gmail.com>
+ * \ingroup module_testutils
+ */
+#include "gmxpre.h"
+
+#include "testutils/mpitest.h"
+
+#include "config.h"
+
+#include <gtest/gtest.h>
+
+#include "gromacs/utility/basenetwork.h"
+#include "gromacs/utility/gmxmpi.h"
+
+namespace
+{
+
+class MpiSelfTest : public ::testing::Test
+{
+    public:
+        MpiSelfTest() : reached {0, 0}
+        {}
+
+        int reached[2];
+};
+
+TEST_F(MpiSelfTest, Runs)
+{
+    GMX_MPI_TEST(2);
+#if GMX_THREAD_MPI
+    reached[gmx_node_rank()] = 1;
+    MPI_Barrier(MPI_COMM_WORLD);
+#else
+    int value = 1;
+    MPI_Gather(&value, 1, MPI_INT, reached, 1, MPI_INT, 0, MPI_COMM_WORLD);
+#endif
+    if (gmx_node_rank() == 0)
+    {
+        EXPECT_EQ(1, reached[0]);
+        EXPECT_EQ(1, reached[1]);
+    }
+}
+
+} // namespace