Check log output in thread affinity tests
authorTeemu Murtola <teemu.murtola@gmail.com>
Fri, 3 Jun 2016 03:31:22 +0000 (06:31 +0300)
committerTeemu Murtola <teemu.murtola@gmail.com>
Fri, 10 Feb 2017 05:35:59 +0000 (07:35 +0200)
Use the ability to redirect the logging to actually check that the
correct warnings get produced for the tests.  For now, most expected
texts are partially duplicated in each test that produces them, but
these can be centralized if it becomes a problem.

All regular output from thread affinity setting now goes through mdlog,
except for one to stderr.

Change-Id: I603e838693b6593d116fb2597eda7f1bf97c9454

src/gromacs/mdrunutility/tests/threadaffinity-mpi.cpp
src/gromacs/mdrunutility/tests/threadaffinity.cpp
src/gromacs/mdrunutility/tests/threadaffinitytest.h
src/gromacs/mdrunutility/threadaffinity.cpp
src/gromacs/mdrunutility/threadaffinity.h
src/programs/mdrun/runner.cpp
src/testutils/CMakeLists.txt
src/testutils/loggertest.cpp [new file with mode: 0644]
src/testutils/loggertest.h [new file with mode: 0644]

index 245e9edaa5855f19e835bc4cd924bfad73707386..35e83d23f203d08b5eab4d1f57ab5af74167edb7 100644 (file)
@@ -54,6 +54,7 @@ TEST(ThreadAffinityMultiRankTest, PinsWholeNode)
     GMX_MPI_TEST(4);
     ThreadAffinityTestHelper helper;
     helper.setLogicalProcessorCount(4);
+    helper.expectPinningMessage(false, 1);
     helper.expectAffinitySet(gmx_node_rank());
     helper.setAffinity(1);
 }
@@ -65,6 +66,8 @@ TEST(ThreadAffinityMultiRankTest, PinsWithOffsetAndStride)
     helper.setAffinityOption(threadaffON);
     helper.setOffsetAndStride(1, 2);
     helper.setLogicalProcessorCount(8);
+    helper.expectWarningMatchingRegex("Applying core pinning offset 1");
+    helper.expectPinningMessage(true, 2);
     helper.expectAffinitySet(1 + 2*gmx_node_rank());
     helper.setAffinity(1);
 }
@@ -75,6 +78,7 @@ TEST(ThreadAffinityMultiRankTest, PinsTwoNodes)
     ThreadAffinityTestHelper helper;
     helper.setPhysicalNodeId(gmx_node_rank()/2);
     helper.setLogicalProcessorCount(2);
+    helper.expectPinningMessage(false, 1);
     helper.expectAffinitySet(gmx_node_rank()%2);
     helper.setAffinity(1);
 }
@@ -93,6 +97,8 @@ TEST(ThreadAffinityMultiRankTest, HandlesTooManyThreadsWithAuto)
     GMX_MPI_TEST(4);
     ThreadAffinityTestHelper helper;
     helper.setLogicalProcessorCount(6);
+    helper.expectWarningMatchingRegex("Oversubscribing the CPU");
+    helper.expectGenericFailureMessage();
     helper.setAffinity(2);
 }
 
@@ -102,6 +108,8 @@ TEST(ThreadAffinityMultiRankTest, HandlesTooManyThreadsWithForce)
     ThreadAffinityTestHelper helper;
     helper.setAffinityOption(threadaffON);
     helper.setLogicalProcessorCount(6);
+    helper.expectWarningMatchingRegex("Oversubscribing the CPU");
+    helper.expectGenericFailureMessage();
     helper.setAffinity(2);
 }
 
@@ -110,6 +118,7 @@ class ThreadAffinityHeterogeneousNodesTest : public ::testing::Test
     public:
         int currentNode() const { return gmx_node_rank() / 2; }
         int indexInNode() const { return gmx_node_rank() % 2; }
+        bool isMaster() const { return gmx_node_rank() == 0; }
 
         void setupNodes(ThreadAffinityTestHelper *helper, std::array<int, 2> cores)
         {
@@ -132,6 +141,12 @@ TEST_F(ThreadAffinityHeterogeneousNodesTest, PinsOnMasterOnly)
     ThreadAffinityTestHelper helper;
     helper.setAffinityOption(threadaffON);
     setupNodes(&helper, {{2, 1}});
+    helper.expectWarningMatchingRegexIf("Oversubscribing the CPU", isMaster() || currentNode() == 1);
+    helper.expectGenericFailureMessageIf(isMaster() || currentNode() == 1);
+    if (currentNode() == 0)
+    {
+        helper.expectPinningMessage(false, 1);
+    }
     expectNodeAffinitySet(&helper, 0, indexInNode());
     helper.setAffinity(1);
 }
@@ -142,6 +157,12 @@ TEST_F(ThreadAffinityHeterogeneousNodesTest, PinsOnNonMasterOnly)
     ThreadAffinityTestHelper helper;
     helper.setAffinityOption(threadaffON);
     setupNodes(&helper, {{1, 2}});
+    helper.expectWarningMatchingRegexIf("Oversubscribing the CPU", currentNode() == 0);
+    helper.expectGenericFailureMessageIf(currentNode() == 0);
+    if (currentNode() == 1)
+    {
+        helper.expectPinningMessage(false, 1);
+    }
     expectNodeAffinitySet(&helper, 1, indexInNode());
     helper.setAffinity(1);
 }
@@ -152,6 +173,12 @@ TEST_F(ThreadAffinityHeterogeneousNodesTest, HandlesUnknownHardwareOnNonMaster)
     ThreadAffinityTestHelper helper;
     helper.setAffinityOption(threadaffON);
     setupNodes(&helper, {{2, 0}});
+    helper.expectWarningMatchingRegexIf("No information on available cores", isMaster() || currentNode() == 1);
+    helper.expectGenericFailureMessageIf(isMaster() || currentNode() == 1);
+    if (currentNode() == 0)
+    {
+        helper.expectPinningMessage(false, 1);
+    }
     expectNodeAffinitySet(&helper, 0, indexInNode());
     helper.setAffinity(1);
 }
@@ -161,6 +188,12 @@ TEST_F(ThreadAffinityHeterogeneousNodesTest, PinsAutomaticallyOnMasterOnly)
     GMX_MPI_TEST(4);
     ThreadAffinityTestHelper helper;
     setupNodes(&helper, {{2, 1}});
+    helper.expectWarningMatchingRegexIf("Oversubscribing the CPU", isMaster() || currentNode() == 1);
+    helper.expectGenericFailureMessageIf(isMaster() || currentNode() == 1);
+    if (currentNode() == 0)
+    {
+        helper.expectPinningMessage(false, 1);
+    }
     expectNodeAffinitySet(&helper, 0, indexInNode());
     helper.setAffinity(1);
 }
@@ -170,6 +203,12 @@ TEST_F(ThreadAffinityHeterogeneousNodesTest, PinsAutomaticallyOnNonMasterOnly)
     GMX_MPI_TEST(4);
     ThreadAffinityTestHelper helper;
     setupNodes(&helper, {{1, 2}});
+    helper.expectWarningMatchingRegexIf("Oversubscribing the CPU", currentNode() == 0);
+    helper.expectGenericFailureMessageIf(currentNode() == 0);
+    if (currentNode() == 1)
+    {
+        helper.expectPinningMessage(false, 1);
+    }
     expectNodeAffinitySet(&helper, 1, indexInNode());
     helper.setAffinity(1);
 }
@@ -181,6 +220,13 @@ TEST_F(ThreadAffinityHeterogeneousNodesTest, HandlesInvalidOffsetOnNonMasterOnly
     helper.setAffinityOption(threadaffON);
     helper.setOffsetAndStride(2, 0);
     setupNodes(&helper, {{4, 2}});
+    helper.expectWarningMatchingRegex("Applying core pinning offset 2");
+    helper.expectWarningMatchingRegexIf("Requested offset too large", isMaster() || currentNode() == 1);
+    helper.expectGenericFailureMessageIf(isMaster() || currentNode() == 1);
+    if (currentNode() == 0)
+    {
+        helper.expectPinningMessage(false, 1);
+    }
     expectNodeAffinitySet(&helper, 0, indexInNode()+2);
     helper.setAffinity(1);
 }
@@ -192,6 +238,12 @@ TEST_F(ThreadAffinityHeterogeneousNodesTest, HandlesInvalidStrideOnNonMasterOnly
     helper.setAffinityOption(threadaffON);
     helper.setOffsetAndStride(0, 2);
     setupNodes(&helper, {{4, 2}});
+    helper.expectWarningMatchingRegexIf("Requested stride too large", isMaster() || currentNode() == 1);
+    helper.expectGenericFailureMessageIf(isMaster() || currentNode() == 1);
+    if (currentNode() == 0)
+    {
+        helper.expectPinningMessage(true, 2);
+    }
     expectNodeAffinitySet(&helper, 0, 2*indexInNode());
     helper.setAffinity(1);
 }
index 6d11834cd4aefb1cca864e079340471b28ac4240..9d1ccf13bcd5445c00ba075d2348c591e0bd1b54 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * This file is part of the GROMACS molecular simulation package.
  *
- * Copyright (c) 2016, by the GROMACS development team, led by
+ * Copyright (c) 2016,2017, 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.
@@ -48,6 +48,7 @@ namespace
 class ThreadAffinityTest : public ::testing::Test
 {
     public:
+
         gmx::test::ThreadAffinityTestHelper helper_;
 };
 
@@ -66,12 +67,16 @@ TEST_F(ThreadAffinityTest, DoesNothingWhenNotSupported)
 TEST_F(ThreadAffinityTest, DoesNothingWithAutoAndTooFewThreads)
 {
     helper_.setLogicalProcessorCount(4);
+    helper_.expectWarningMatchingRegex("The number of threads is not equal to the number of");
+    helper_.expectGenericFailureMessage();
     helper_.setAffinity(2);
 }
 
 TEST_F(ThreadAffinityTest, DoesNothingWithAutoAndTooManyThreads)
 {
     helper_.setLogicalProcessorCount(4);
+    helper_.expectWarningMatchingRegex("Oversubscribing the CPU");
+    helper_.expectGenericFailureMessage();
     helper_.setAffinity(8);
 }
 
@@ -79,6 +84,8 @@ TEST_F(ThreadAffinityTest, DoesNothingWithUnknownHardware)
 {
     helper_.setAffinityOption(threadaffON);
     helper_.setLogicalProcessorCount(0);
+    helper_.expectWarningMatchingRegex("No information on available cores");
+    helper_.expectGenericFailureMessage();
     helper_.setAffinity(2);
 }
 
@@ -86,6 +93,8 @@ TEST_F(ThreadAffinityTest, DoesNothingWithTooManyThreads)
 {
     helper_.setAffinityOption(threadaffON);
     helper_.setLogicalProcessorCount(4);
+    helper_.expectWarningMatchingRegex("Oversubscribing the CPU");
+    helper_.expectGenericFailureMessage();
     helper_.setAffinity(8);
 }
 
@@ -94,6 +103,9 @@ TEST_F(ThreadAffinityTest, DoesNothingWithTooLargeOffset)
     helper_.setAffinityOption(threadaffON);
     helper_.setOffsetAndStride(2, 0);
     helper_.setLogicalProcessorCount(4);
+    helper_.expectWarningMatchingRegex("Applying core pinning offset 2");
+    helper_.expectWarningMatchingRegex("Requested offset too large");
+    helper_.expectGenericFailureMessage();
     helper_.setAffinity(3);
 }
 
@@ -102,6 +114,8 @@ TEST_F(ThreadAffinityTest, DoesNothingWithTooLargeStride)
     helper_.setAffinityOption(threadaffON);
     helper_.setOffsetAndStride(0, 2);
     helper_.setLogicalProcessorCount(4);
+    helper_.expectWarningMatchingRegex("Requested stride too large");
+    helper_.expectGenericFailureMessage();
     helper_.setAffinity(3);
 }
 
@@ -109,6 +123,7 @@ TEST_F(ThreadAffinityTest, PinsSingleThreadWithAuto)
 {
     helper_.setLogicalProcessorCount(1);
     helper_.expectAffinitySet(0);
+    helper_.expectPinningMessage(false, 1);
     helper_.setAffinity(1);
 }
 
@@ -116,6 +131,7 @@ TEST_F(ThreadAffinityTest, PinsSingleThreadWhenForced)
 {
     helper_.setAffinityOption(threadaffON);
     helper_.setLogicalProcessorCount(2);
+    helper_.expectPinningMessage(false, 2);
     helper_.expectAffinitySet(0);
     helper_.setAffinity(1);
 }
@@ -125,6 +141,8 @@ TEST_F(ThreadAffinityTest, PinsSingleThreadWithOffsetWhenForced)
     helper_.setAffinityOption(threadaffON);
     helper_.setOffsetAndStride(2, 0);
     helper_.setLogicalProcessorCount(4);
+    helper_.expectWarningMatchingRegex("Applying core pinning offset 2");
+    helper_.expectPinningMessage(false, 2);
     helper_.expectAffinitySet(2);
     helper_.setAffinity(1);
 }
@@ -132,6 +150,8 @@ TEST_F(ThreadAffinityTest, PinsSingleThreadWithOffsetWhenForced)
 TEST_F(ThreadAffinityTest, HandlesPinningFailureWithSingleThread)
 {
     helper_.setLogicalProcessorCount(1);
+    helper_.expectPinningMessage(false, 1);
+    helper_.expectGenericFailureMessage();
     helper_.expectAffinitySetThatFails(0);
     helper_.setAffinity(1);
 }
@@ -142,6 +162,7 @@ TEST_F(ThreadAffinityTest, HandlesPinningFailureWithSingleThread)
 TEST_F(ThreadAffinityTest, PinsMultipleThreadsWithAuto)
 {
     helper_.setLogicalProcessorCount(2);
+    helper_.expectPinningMessage(false, 1);
     helper_.expectAffinitySet({0, 1});
     helper_.setAffinity(2);
 }
@@ -151,6 +172,7 @@ TEST_F(ThreadAffinityTest, PinsMultipleThreadsWithStrideWhenForced)
     helper_.setAffinityOption(threadaffON);
     helper_.setOffsetAndStride(0, 2);
     helper_.setLogicalProcessorCount(4);
+    helper_.expectPinningMessage(true, 2);
     helper_.expectAffinitySet({0, 2});
     helper_.setAffinity(2);
 }
@@ -159,6 +181,8 @@ TEST_F(ThreadAffinityTest, HandlesPinningFailureWithOneThreadFailing)
 {
     helper_.setAffinityOption(threadaffON);
     helper_.setLogicalProcessorCount(2);
+    helper_.expectPinningMessage(false, 1);
+    helper_.expectGenericFailureMessage();
     helper_.expectAffinitySet(0);
     helper_.expectAffinitySetThatFails(1);
     helper_.setAffinity(2);
index 4dcc0a38a4c4aea1bd372dbd8b16ab47c2c20931..71941a2fb49839ca37f48973dc9960e1bebae74f 100644 (file)
@@ -43,6 +43,9 @@
 #include "gromacs/hardware/hw_info.h"
 #include "gromacs/mdrunutility/threadaffinity.h"
 #include "gromacs/utility/logger.h"
+#include "gromacs/utility/stringutil.h"
+
+#include "testutils/loggertest.h"
 
 struct t_commrec;
 
@@ -117,14 +120,53 @@ class ThreadAffinityTestHelper
                 .WillOnce(Return(false));
         }
 
+        void expectWarningMatchingRegex(const char *re)
+        {
+            expectWarningMatchingRegexIf(re, true);
+        }
+        void expectWarningMatchingRegexIf(const char *re, bool condition)
+        {
+            expectLogMessageMatchingRegexIf(MDLogger::LogLevel::Warning, re, condition);
+        }
+        void expectInfoMatchingRegex(const char *re)
+        {
+            expectInfoMatchingRegexIf(re, true);
+        }
+        void expectInfoMatchingRegexIf(const char *re, bool condition)
+        {
+            expectLogMessageMatchingRegexIf(MDLogger::LogLevel::Info, re, condition);
+        }
+        void expectGenericFailureMessage()
+        {
+            expectGenericFailureMessageIf(true);
+        }
+        void expectGenericFailureMessageIf(bool condition)
+        {
+            expectWarningMatchingRegexIf("NOTE: Thread affinity setting failed.", condition);
+        }
+        void expectPinningMessage(bool userSpecifiedStride, int stride)
+        {
+            std::string pattern = formatString("Pinning threads .* %s.* stride of %d",
+                                               userSpecifiedStride ? "user" : "auto",
+                                               stride);
+            expectInfoMatchingRegex(pattern.c_str());
+        }
+        void expectLogMessageMatchingRegexIf(MDLogger::LogLevel level,
+                                             const char *re, bool condition)
+        {
+            if (condition)
+            {
+                logHelper_.expectEntryMatchingRegex(level, re);
+            }
+        }
+
         void setAffinity(int nthread_local)
         {
             if (hwTop_ == nullptr)
             {
                 setLogicalProcessorCount(1);
             }
-            MDLogger mdlog;
-            gmx_set_thread_affinity(nullptr, mdlog, cr_, hwOpt_, *hwTop_,
+            gmx_set_thread_affinity(logHelper_.logger(), cr_, hwOpt_, *hwTop_,
                                     nthread_local, &affinityAccess_);
         }
 
@@ -133,6 +175,7 @@ class ThreadAffinityTestHelper
         gmx_hw_opt_t                      *hwOpt_;
         std::unique_ptr<HardwareTopology>  hwTop_;
         MockThreadAffinityAccess           affinityAccess_;
+        LoggerTestHelper                   logHelper_;
 };
 
 } // namespace test
index 3475f5fbd71e63a9856fc3146bd9e21a77a14d2a..a4370e9450063b546ac8c56626cccc82d6c0cf74 100644 (file)
@@ -111,7 +111,7 @@ static bool invalidWithinSimulation(const t_commrec *cr, bool invalidLocally)
 }
 
 static bool
-get_thread_affinity_layout(FILE *fplog, const gmx::MDLogger &mdlog,
+get_thread_affinity_layout(const gmx::MDLogger &mdlog,
                            const t_commrec *cr,
                            const gmx::HardwareTopology &hwTop,
                            int   threads,
@@ -250,9 +250,10 @@ get_thread_affinity_layout(FILE *fplog, const gmx::MDLogger &mdlog,
     }
     validLayout = validLayout && !invalidValue;
 
-    if (validLayout && fplog != nullptr)
+    if (validLayout)
     {
-        fprintf(fplog, "Pinning threads with a%s logical core stride of %d\n",
+        GMX_LOG(mdlog.info).appendTextFormatted(
+                "Pinning threads with a%s logical core stride of %d",
                 bPickPinStride ? "n auto-selected" : " user-specified",
                 *pin_stride);
     }
@@ -343,6 +344,7 @@ static bool set_affinity(const t_commrec *cr, int nthread_local, int thread0_id_
                     nthread_local > 1 ? "s" : "");
         }
 
+        // TODO: This output should also go through mdlog.
         fprintf(stderr, "NOTE: %sAffinity setting %sfailed.\n", sbuf1, sbuf2);
     }
     return allAffinitiesSet;
@@ -358,8 +360,7 @@ static bool set_affinity(const t_commrec *cr, int nthread_local, int thread0_id_
    if only PME is using threads.
  */
 void
-gmx_set_thread_affinity(FILE                        *fplog,
-                        const gmx::MDLogger         &mdlog,
+gmx_set_thread_affinity(const gmx::MDLogger         &mdlog,
                         const t_commrec             *cr,
                         const gmx_hw_opt_t          *hw_opt,
                         const gmx::HardwareTopology &hwTop,
@@ -427,7 +428,7 @@ gmx_set_thread_affinity(FILE                        *fplog,
 
     bool automatic = (hw_opt->thread_affinity == threadaffAUTO);
     bool validLayout
-        = get_thread_affinity_layout(fplog, mdlog, cr, hwTop, nthread_node, automatic,
+        = get_thread_affinity_layout(mdlog, cr, hwTop, nthread_node, automatic,
                                      offset, &core_pinning_stride, &localityOrder);
     const gmx::sfree_guard  localityOrderGuard(localityOrder);
 
index 521a8c1177e74a9a23d79d035420581e6052c8b5..5511c82e0d2f3a35d82c33707ab6be1829adc949 100644 (file)
@@ -72,8 +72,7 @@ class IThreadAffinityAccess
  * Sets the thread affinity using the requested setting stored in hw_opt.
  */
 void
-gmx_set_thread_affinity(FILE                        *fplog,
-                        const gmx::MDLogger         &mdlog,
+gmx_set_thread_affinity(const gmx::MDLogger         &mdlog,
                         const t_commrec             *cr,
                         const gmx_hw_opt_t          *hw_opt,
                         const gmx::HardwareTopology &hwTop,
index 78ab6dc34abb9728024d44d24e668aaca0b18886..3cfbcaa05f476531d2969b52fcb66579fa4c0156 100644 (file)
@@ -1291,7 +1291,7 @@ int mdrunner(gmx_hw_opt_t *hw_opt,
         }
 
         /* Set the CPU affinity */
-        gmx_set_thread_affinity(fplog, mdlog, cr, hw_opt, *hwinfo->hardwareTopology,
+        gmx_set_thread_affinity(mdlog, cr, hw_opt, *hwinfo->hardwareTopology,
                                 nthread_local, nullptr);
     }
 
index 87eb4495d178599de3dbe56254293a8c859118a1..0b3dc645eed307cd51dd40afde6f009baa64fe1c 100644 (file)
@@ -1,7 +1,7 @@
 #
 # This file is part of the GROMACS molecular simulation package.
 #
-# Copyright (c) 2011,2012,2013,2014,2015,2016, by the GROMACS development team, led by
+# Copyright (c) 2011,2012,2013,2014,2015,2016,2017, 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.
@@ -42,6 +42,7 @@ set(TESTUTILS_SOURCES
     cmdlinetest.cpp
     integrationtests.cpp
     interactivetest.cpp
+    loggertest.cpp
     mpi-printer.cpp
     mpitest.cpp
     refdata.cpp
diff --git a/src/testutils/loggertest.cpp b/src/testutils/loggertest.cpp
new file mode 100644 (file)
index 0000000..7d3076e
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+ * This file is part of the GROMACS molecular simulation package.
+ *
+ * Copyright (c) 2016,2017, 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 gmx::test::LoggerTestHelper.
+ *
+ * \author Teemu Murtola <teemu.murtola@gmail.com>
+ * \ingroup module_testutils
+ */
+#include "gmxpre.h"
+
+#include "loggertest.h"
+
+#include <gmock/gmock.h>
+
+#include "gromacs/utility/basedefinitions.h"
+#include "gromacs/utility/logger.h"
+
+namespace gmx
+{
+namespace test
+{
+
+using ::testing::NiceMock;
+
+namespace
+{
+class MockLogTarget : public ILogTarget
+{
+    public:
+        MOCK_METHOD1(writeEntry, void(const LogEntry &));
+};
+}       // namespace
+
+/********************************************************************
+ * LoggerTestHelper::Impl
+ */
+
+class LoggerTestHelper::Impl
+{
+    public:
+        Impl()
+        {
+            // TODO: Add support for -stdout for echoing the log to stdout.
+            logger_.warning = LogLevelHelper(&getTarget(MDLogger::LogLevel::Warning));
+            logger_.info    = LogLevelHelper(&getTarget(MDLogger::LogLevel::Info));
+        }
+
+        NiceMock<MockLogTarget> &getTarget(MDLogger::LogLevel level)
+        {
+            return targets_[static_cast<int>(level)];
+        }
+
+        NiceMock<MockLogTarget>  targets_[MDLogger::LogLevelCount];
+        MDLogger                 logger_;
+};
+
+/********************************************************************
+ * LoggerTestHelper
+ */
+
+LoggerTestHelper::LoggerTestHelper()
+    : impl_(new Impl)
+{
+}
+
+LoggerTestHelper::~LoggerTestHelper()
+{
+}
+
+const MDLogger &LoggerTestHelper::logger()
+{
+    return impl_->logger_;
+}
+
+void LoggerTestHelper::expectEntryMatchingRegex(gmx::MDLogger::LogLevel level,
+                                                const char             *re)
+{
+    using ::testing::ContainsRegex;
+    using ::testing::Field;
+    auto &target = impl_->getTarget(level);
+    EXPECT_CALL(target, writeEntry(Field(&LogEntry::text, ContainsRegex(re))));
+}
+
+} // namespace test
+} // namespace gmx
diff --git a/src/testutils/loggertest.h b/src/testutils/loggertest.h
new file mode 100644 (file)
index 0000000..1fc4fbf
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * This file is part of the GROMACS molecular simulation package.
+ *
+ * Copyright (c) 2016,2017, 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
+ * Declares gmx::test::LoggerTestHelper.
+ *
+ * \author Teemu Murtola <teemu.murtola@gmail.com>
+ * \inlibraryapi
+ * \ingroup module_testutils
+ */
+#ifndef GMX_TESTUTILS_LOGGERTEST_H
+#define GMX_TESTUTILS_LOGGERTEST_H
+
+#include "gromacs/utility/classhelpers.h"
+#include "gromacs/utility/logger.h"
+
+namespace gmx
+{
+
+namespace test
+{
+
+/*! \libinternal \brief
+ * Helper class for tests to check output written to a logger.
+ *
+ * \inlibraryapi
+ * \ingroup module_testutils
+ */
+class LoggerTestHelper
+{
+    public:
+        LoggerTestHelper();
+        ~LoggerTestHelper();
+
+        //! Returns the logger to pass to code under test.
+        const MDLogger &logger();
+
+        /*! \brief
+         * Expects a log entry at a given level matching a given regex.
+         *
+         * Currently, the order of the entries is not checked, and if this
+         * method is called once for a log level, then it needs to be called
+         * for all entries produced by the test.
+         *
+         * If not called for a log level, all entries for that level are
+         * accepted.
+         */
+        void expectEntryMatchingRegex(gmx::MDLogger::LogLevel level,
+                                      const char             *re);
+
+    private:
+        class Impl;
+
+        PrivateImplPointer<Impl> impl_;
+};
+
+} // namespace test
+} // namespace gmx
+
+#endif