Enable command-line options for unit tests.
authorTeemu Murtola <teemu.murtola@gmail.com>
Sat, 14 Apr 2012 05:16:30 +0000 (08:16 +0300)
committerTeemu Murtola <teemu.murtola@gmail.com>
Sat, 14 Apr 2012 06:11:19 +0000 (09:11 +0300)
Add a simple framework for easily parsing command-line options in unit
tests.  Use the framework to add a -seldebug option for the selection
tests to make it easier to debug issues in failing tests.  Adjusted the
initialization code for testutils to go through a single function.
Also fixed a noticed issue in the command-line parser that made it
impossible to pass in negative numbers.

Change-Id: Idb17a44d3a2669a91cf077acc99b3475d2c251a0

12 files changed:
src/gromacs/options/cmdlineparser.cpp
src/gromacs/options/cmdlineparser.h
src/gromacs/options/tests/cmdlineparser.cpp
src/gromacs/selection/tests/selectioncollection.cpp
src/testutils/CMakeLists.txt
src/testutils/refdata-common.cpp
src/testutils/refdata.h
src/testutils/test_main_gmock.cpp
src/testutils/test_main_gtest.cpp
src/testutils/testexceptions.h
src/testutils/testoptions.cpp [new file with mode: 0644]
src/testutils/testoptions.h [new file with mode: 0644]

index 76acdcd8b0ab3b7ae92afd9452770ea0ab6c8590..27c2f862135c639aac99f68b2999a34e2a75826e 100644 (file)
@@ -37,6 +37,8 @@
  */
 #include "gromacs/options/cmdlineparser.h"
 
+#include <cctype>
+
 #include "gromacs/fatalerror/exceptions.h"
 #include "gromacs/fatalerror/messagestringcollector.h"
 #include "gromacs/options/optionsassigner.h"
@@ -71,17 +73,27 @@ CommandLineParser::~CommandLineParser()
 }
 
 void CommandLineParser::parse(int *argc, char *argv[])
+{
+    std::vector<std::string> commandLine;
+    for (int i = 0; i < *argc; ++i)
+    {
+        commandLine.push_back(argv[i]);
+    }
+    parse(&commandLine);
+}
+
+void CommandLineParser::parse(std::vector<std::string> *commandLine)
 {
     MessageStringCollector errors;
-    int  i = 1;
     // Start in the discard phase to skip options that can't be understood.
     bool bDiscard = true;
 
     _impl->_assigner.start();
-    while (i < *argc)
+    std::vector<std::string>::const_iterator arg;
+    for (arg = commandLine->begin() + 1; arg != commandLine->end(); ++arg)
     {
-        // Lone '-' is passed as a value.
-        if (argv[i][0] == '-' && argv[i][1] != '\0')
+        // Lone '-' and numbers are passed as values.
+        if ((*arg)[0] == '-' && std::isalpha((*arg)[1]))
         {
             if (!bDiscard)
             {
@@ -95,11 +107,11 @@ void CommandLineParser::parse(int *argc, char *argv[])
                 }
                 errors.finishContext();
             }
-            errors.startContext("In command-line option " + std::string(argv[i]));
+            errors.startContext("In command-line option " + *arg);
             bDiscard = false;
             try
             {
-                const char *name = &argv[i][1];
+                const char *name = arg->c_str() + 1;
                 _impl->_assigner.startOption(name);
             }
             catch (const UserInputError &ex)
@@ -113,14 +125,13 @@ void CommandLineParser::parse(int *argc, char *argv[])
         {
             try
             {
-                _impl->_assigner.appendValue(argv[i]);
+                _impl->_assigner.appendValue(*arg);
             }
             catch (const UserInputError &ex)
             {
                 errors.append(ex.what());
             }
         }
-        ++i;
     }
     if (!bDiscard)
     {
index 6198874f97d4e1bc9a8838bbf57630049e4968fc..cc2ea2304b8ec292712d77670b27b1137a84d699 100644 (file)
@@ -39,6 +39,9 @@
 #ifndef GMX_OPTIONS_CMDLINEPARSER_H
 #define GMX_OPTIONS_CMDLINEPARSER_H
 
+#include <string>
+#include <vector>
+
 #include "../utility/common.h"
 
 namespace gmx
@@ -68,19 +71,45 @@ class CommandLineParser
          * Creates a command-line parser that sets values for options.
          *
          * \param[in] options  Options object whose options should be set.
+         * \throws  std::bad_alloc if out of memory.
          */
         CommandLineParser(Options *options);
         ~CommandLineParser();
 
         /*! \brief
-         * Parses the command-line.
+         * Parses the command line.
          *
-         * \throws InvalidInputError if any errors were detected in the input.
+         * \throws  std::bad_alloc if out of memory.
+         * \throws  InvalidInputError if any errors were detected in the input.
          *
          * All command-line arguments are parsed, and an aggregate exception
          * with all the detected errors is thrown in the end.
+         *
+         * Currently, the input parameters are not modified, but this may
+         * change if/when support for parsing only part of the options is
+         * implemented.
          */
         void parse(int *argc, char *argv[]);
+        /*! \brief
+         * Parses the command line from a std::vector.
+         *
+         * \param[in] commandLine  Array of command-line strings.
+         * \throws  std::bad_alloc if out of memory.
+         * \throws  InvalidInputError if any errors were detected in the input.
+         *
+         * \p commandLine should relate to the standard \c argv array
+         * one-to-one.
+         *
+         * This method is provided for convenience for cases where the command
+         * line needs to be stored before parsing.
+         *
+         * Currently, the input parameters are not modified, but this may
+         * change if/when support for parsing only part of the options is
+         * implemented.
+         *
+         * \see parse(int *, char *[])
+         */
+        void parse(std::vector<std::string> *commandLine);
 
     private:
         class Impl;
index a2409eb3636691233c0db3838ce191801a76cab5..f700668c9ad0375360e8745ded913b9ff4b819af 100644 (file)
@@ -121,4 +121,18 @@ TEST_F(CommandLineParserTest, HandlesSingleValues)
     EXPECT_DOUBLE_EQ(2.7, _dvalues[0]);
 }
 
+TEST_F(CommandLineParserTest, HandlesNegativeNumbers)
+{
+    const char *cmdline[] = {"-mvi", "1", "-2", "-mvd", "-2.7", NULL};
+    createArguments(cmdline);
+    ASSERT_NO_THROW(_parser.parse(&_argc, _argv));
+    ASSERT_NO_THROW(_options.finish());
+
+    ASSERT_EQ(2U, _ivalues.size());
+    EXPECT_EQ(1, _ivalues[0]);
+    EXPECT_EQ(-2, _ivalues[1]);
+    ASSERT_EQ(1U, _dvalues.size());
+    EXPECT_DOUBLE_EQ(-2.7, _dvalues[0]);
+}
+
 } // namespace
index c9936196d3766bfca4555d1aa946c0b265f0f6a7..f4390d4518577a3be7d8a06c86944edc983115b5 100644 (file)
@@ -47,6 +47,8 @@
 #include "vec.h"
 
 #include "gromacs/fatalerror/exceptions.h"
+#include "gromacs/options/basicoptions.h"
+#include "gromacs/options/options.h"
 #include "gromacs/selection/poscalc.h"
 #include "gromacs/selection/selectioncollection.h"
 #include "gromacs/selection/selection.h"
@@ -55,6 +57,7 @@
 
 #include "testutils/datapath.h"
 #include "testutils/refdata.h"
+#include "testutils/testoptions.h"
 
 namespace
 {
@@ -66,6 +69,10 @@ namespace
 class SelectionCollectionTest : public ::testing::Test
 {
     public:
+        static void SetUpTestCase();
+
+        static int               s_debugLevel;
+
         SelectionCollectionTest();
         ~SelectionCollectionTest();
 
@@ -81,10 +88,20 @@ class SelectionCollectionTest : public ::testing::Test
         t_trxframe              *_frame;
 };
 
+int SelectionCollectionTest::s_debugLevel = 0;
+
+void SelectionCollectionTest::SetUpTestCase()
+{
+    gmx::Options options(NULL, NULL);
+    options.addOption(gmx::IntegerOption("seldebug").store(&s_debugLevel));
+    gmx::test::parseTestOptions(&options);
+}
+
 
 SelectionCollectionTest::SelectionCollectionTest()
     : _top(NULL), _frame(NULL)
 {
+    _sc.setDebugLevel(s_debugLevel);
     _sc.setReferencePosType("atom");
     _sc.setOutputPosType("atom");
 }
index bf163d968a1da9e78f9336add0cb276aa1d8c430..bb234a0d34161b759512e5f2000c50d8e27fb20e 100644 (file)
@@ -1,5 +1,5 @@
 set(TESTUTILS_HAVE_REFDATA FALSE)
-set(COMMON_SOURCES datapath.cpp refdata-common.cpp)
+set(COMMON_SOURCES datapath.cpp refdata-common.cpp testoptions.cpp)
 if (GMX_USE_GTEST AND LIBXML2_FOUND)
     include_directories(${GTEST_INCLUDE_DIRS})
     list(APPEND COMMON_SOURCES refdata.cpp)
index bfa3c2d7b66b8a4e54b89dbc655744b9bf31efa3..2a23c488912722d67d363f1e1c85ed7614733630 100644 (file)
@@ -38,6 +38,9 @@
 #include "refdata.h"
 
 #include <cstring>
+#include <cstdio>
+
+#include <new>
 
 #include "testutils/datapath.h"
 
@@ -67,7 +70,7 @@ std::string getReferenceDataPath()
 }
 
 
-int initReferenceData(int *argc, char **argv)
+void initReferenceData(int *argc, char **argv)
 {
     int i, newi;
 
@@ -87,10 +90,16 @@ int initReferenceData(int *argc, char **argv)
     }
     *argc = newi;
 #ifdef TESTUTILS_HAVE_REFDATA
-    internal::addGlobalReferenceDataEnvironment();
+    try
+    {
+        internal::addGlobalReferenceDataEnvironment();
+    }
+    catch (const std::bad_alloc &)
+    {
+        std::fprintf(stderr, "Out of memory\n");
+        std::exit(1);
+    }
 #endif
-
-    return 0;
 }
 
 } // namespace test
index ca792e4b143fb2cb6ccf2eb3f6183d17d055f531..5f2f210e972f43b062f790a184c478756929b96a 100644 (file)
@@ -106,10 +106,12 @@ std::string getReferenceDataPath();
  * can be used to change it.
  * Recognized command-line arguments are removed from the list.
  *
- * This function is automatically called by test_main_gtest.cpp and
- * test_main_gmock.cpp.
+ * Does not throw.  Terminates the program with a non-zero error code if an
+ * error occurs.
+ *
+ * This function is automatically called by initTestUtils().
  */
-int initReferenceData(int *argc, char **argv);
+void initReferenceData(int *argc, char **argv);
 
 /*! \cond internal */
 /*! \internal \brief
index 8b2ac908fdb48c44290ae7e53027ac2401dcdf5e..fecce9654db704533a8b9039aadb5f41e2913be6 100644 (file)
  * main() for unit tests that use Google C++ Mocking Framework.
  *
  * \author Teemu Murtola <teemu.murtola@cbr.su.se>
+ * \ingroup module_testutils
  */
 #include <gmock/gmock.h>
 
-#include "gromacs/fatalerror/errorcodes.h"
-#include "testutils/datapath.h"
-#include "testutils/refdata.h"
+#include "testutils/testoptions.h"
+
+#ifndef TEST_DATA_PATH
+//! Path to test input data directory (needs to be set by the build system).
+#define TEST_DATA_PATH 0
+#endif
 
 /*! \brief
  * Initializes unit testing with Google C++ Mocking Framework.
 int main(int argc, char *argv[])
 {
     ::testing::InitGoogleMock(&argc, argv);
-#ifdef TEST_DATA_PATH
-    ::gmx::test::setTestDataPath(TEST_DATA_PATH);
-    if (::gmx::test::initReferenceData(&argc, argv) != 0)
-    {
-        return 1;
-    }
-#endif
-    ::gmx::setFatalErrorHandler(NULL);
+    ::gmx::test::initTestUtils(TEST_DATA_PATH, &argc, argv);
     return RUN_ALL_TESTS();
 }
index b275a1be2c16d0775ccc29a7d766f923913b3a8d..5ad85d3742ba8b48a4e03c2d381e381b03a8b0fa 100644 (file)
  * main() for unit tests that use Google C++ Testing Framework.
  *
  * \author Teemu Murtola <teemu.murtola@cbr.su.se>
+ * \ingroup module_testutils
  */
 #include <gtest/gtest.h>
 
-#include "gromacs/fatalerror/errorcodes.h"
-#include "testutils/datapath.h"
-#include "testutils/refdata.h"
+#include "testutils/testoptions.h"
+
+#ifndef TEST_DATA_PATH
+//! Path to test input data directory (needs to be set by the build system).
+#define TEST_DATA_PATH 0
+#endif
 
 /*! \brief
  * Initializes unit testing with Google C++ Testing Framework.
 int main(int argc, char *argv[])
 {
     ::testing::InitGoogleTest(&argc, argv);
-#ifdef TEST_DATA_PATH
-    ::gmx::test::setTestDataPath(TEST_DATA_PATH);
-    if (::gmx::test::initReferenceData(&argc, argv) != 0)
-    {
-        return 1;
-    }
-#endif
-    ::gmx::setFatalErrorHandler(NULL);
+    ::gmx::test::initTestUtils(TEST_DATA_PATH, &argc, argv);
     return RUN_ALL_TESTS();
 }
index 5e0fa5f4c57664c64dad5b57d0faf222e4cf16b5..afc3da7abf670068ce7cbfa89de25e340d51d342 100644 (file)
@@ -66,10 +66,48 @@ class TestException : public GromacsException
          */
         explicit TestException(const std::string &reason)
             : GromacsException(reason) {}
+        /*! \brief
+         * Creates a test exception based on another GromacsException object.
+         *
+         * \param[in] base  Exception to wrap.
+         *
+         * \see GMX_THROW_WRAPPER_TESTEXCEPTION
+         */
+        explicit TestException(const GromacsException &base)
+            : GromacsException(base) {}
 
         virtual int errorCode() const { return -1; }
 };
 
+/*! \brief
+ * Macro for throwing a TestException that wraps another exception.
+ *
+ * \param[in] e    Exception object to wrap.
+ *
+ * This macro is intended for wrapping exceptions thrown by Gromacs methods
+ * that are called from a test for the test's internal purposes.  It wraps the
+ * exception in a TestException to make it possible to tell from the type of
+ * the exception whether the exception was thrown by the code under test, or by
+ * the test code itself.
+ *
+ * \p e should evaluate to an instance of an object derived from
+ * GromacsException.
+ *
+ * Typical usage in test code:
+ * \code
+try
+{
+    // some code that may throw a GromacsException
+}
+catch (const GromacsException &ex)
+{
+    GMX_THROW_WRAPPER_TESTEXCEPTION(ex);
+}
+ * \endcode
+ */
+#define GMX_THROW_WRAPPER_TESTEXCEPTION(e) \
+    throw ::boost::enable_current_exception(::gmx::test::TestException(e))
+
 } // namespace test
 } // namespace gmx
 
diff --git a/src/testutils/testoptions.cpp b/src/testutils/testoptions.cpp
new file mode 100644 (file)
index 0000000..db930dc
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ *
+ *                This source code is part of
+ *
+ *                 G   R   O   M   A   C   S
+ *
+ *          GROningen MAchine for Chemical Simulations
+ *
+ * Written by David van der Spoel, Erik Lindahl, Berk Hess, and others.
+ * Copyright (c) 1991-2000, University of Groningen, The Netherlands.
+ * Copyright (c) 2001-2009, The GROMACS development team,
+ * check out http://www.gromacs.org for more information.
+
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * If you want to redistribute modifications, 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 www.gromacs.org.
+ *
+ * To help us fund GROMACS development, we humbly ask that you cite
+ * the papers on the package - you can find them in the top README file.
+ *
+ * For more info, check our website at http://www.gromacs.org
+ */
+/*! \internal \file
+ * \brief
+ * Implements functions in testoptions.h.
+ *
+ * \author Teemu Murtola <teemu.murtola@cbr.su.se>
+ * \ingroup module_testutils
+ */
+#include "testoptions.h"
+
+#include <cstdio>
+#include <cstdlib>
+
+#include <new>
+#include <string>
+#include <vector>
+
+#include <boost/scoped_ptr.hpp>
+
+#include "gromacs/fatalerror/errorcodes.h"
+#include "gromacs/fatalerror/gmxassert.h"
+#include "gromacs/options/cmdlineparser.h"
+#include "gromacs/options/options.h"
+
+#include "datapath.h"
+#include "refdata.h"
+#include "testexceptions.h"
+
+static boost::scoped_ptr<std::vector<std::string> > s_commandLine;
+
+namespace gmx
+{
+namespace test
+{
+
+void initTestUtils(const char *dataPath, int *argc, char *argv[])
+{
+    if (dataPath != NULL)
+    {
+        setTestDataPath(dataPath);
+    }
+    initReferenceData(argc, argv);
+    try
+    {
+        boost::scoped_ptr<std::vector<std::string> > commandLine(
+                new std::vector<std::string>());
+        for (int i = 0; i < *argc; ++i)
+        {
+            commandLine->push_back(argv[i]);
+        }
+        swap(commandLine, s_commandLine);
+    }
+    catch (const std::bad_alloc &)
+    {
+        std::fprintf(stderr, "Out of memory\n");
+        std::exit(1);
+    }
+    ::gmx::setFatalErrorHandler(NULL);
+}
+
+void parseTestOptions(Options *options)
+{
+    GMX_RELEASE_ASSERT(s_commandLine.get() != NULL,
+                       "Test options not initialized");
+    try
+    {
+        CommandLineParser(options).parse(s_commandLine.get());
+        options->finish();
+    }
+    catch (const GromacsException &ex)
+    {
+        GMX_THROW_WRAPPER_TESTEXCEPTION(ex);
+    }
+}
+
+} // namespace test
+} // namespace gmx
diff --git a/src/testutils/testoptions.h b/src/testutils/testoptions.h
new file mode 100644 (file)
index 0000000..93fcce6
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ *
+ *                This source code is part of
+ *
+ *                 G   R   O   M   A   C   S
+ *
+ *          GROningen MAchine for Chemical Simulations
+ *
+ * Written by David van der Spoel, Erik Lindahl, Berk Hess, and others.
+ * Copyright (c) 1991-2000, University of Groningen, The Netherlands.
+ * Copyright (c) 2001-2009, The GROMACS development team,
+ * check out http://www.gromacs.org for more information.
+
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * If you want to redistribute modifications, 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 www.gromacs.org.
+ *
+ * To help us fund GROMACS development, we humbly ask that you cite
+ * the papers on the package - you can find them in the top README file.
+ *
+ * For more info, check our website at http://www.gromacs.org
+ */
+/*! \libinternal \file
+ * \brief
+ * Functions for accessing test command-line options.
+ *
+ * Functions in this header allow accessing command-line options passed to the
+ * test executable from tests.  This can be used to, e.g., enable additional
+ * output for debugging purposes.
+ *
+ * \author Teemu Murtola <teemu.murtola@cbr.su.se>
+ * \inlibraryapi
+ * \ingroup module_testutils
+ */
+#ifndef GMX_TESTUTILS_TESTOPTIONS_H
+#define GMX_TESTUTILS_TESTOPTIONS_H
+
+namespace gmx
+{
+
+class Options;
+
+namespace test
+{
+
+/*! \libinternal \brief
+ * Initializes the test utilities library.
+ *
+ * Does not throw.  Terminates the program with a non-zero error code if an
+ * error occurs.
+ *
+ * This function is automatically called by test_main_gtest.cpp and
+ * test_main_gmock.cpp.
+ */
+void initTestUtils(const char *dataPath, int *argc, char *argv[]);
+/*! \libinternal \brief
+ * Parses given options from the command line.
+ *
+ * \param[in] options  Definition of options to parse.
+ * \throws  std::bad_alloc if out of memory.
+ * \throws  TestException if an error occurs in the parsing.
+ *
+ * This can be used from test or test fixture setup functions to initialize
+ * local variables.  Although this means that the parameters are potentially
+ * parsed multiple times, the performance impact should not be significant.
+ *
+ * \inlibraryapi
+ */
+void parseTestOptions(Options *options);
+
+} // namespace test
+} // namespace gmx
+
+#endif