Some tests for 'gmx help -export rst'
authorTeemu Murtola <teemu.murtola@gmail.com>
Sat, 16 May 2015 04:16:04 +0000 (07:16 +0300)
committerGerrit Code Review <gerrit@gerrit.gromacs.org>
Wed, 20 May 2015 08:10:15 +0000 (10:10 +0200)
Add basic test that checks 'gmx help -export rst' for regression.
The mechanism also makes it easier to test additional work in this area.

Minor changes in the command-line module to support testing:
 - Make all output from 'gmx help -export' capturable with a new
   FileOutputRedirectorInterface interface.
 - Make it easier to mock a CommandLineOptionsModuleInterface

Change-Id: I58982b3de7e946c373ffb737ef994b4e527418bc

15 files changed:
src/gromacs/commandline/cmdlinehelpmodule.cpp
src/gromacs/commandline/cmdlinehelpmodule.h
src/gromacs/commandline/cmdlinemodulemanager.cpp
src/gromacs/commandline/cmdlinemodulemanager.h
src/gromacs/commandline/cmdlineoptionsmodule.cpp
src/gromacs/commandline/cmdlineoptionsmodule.h
src/gromacs/commandline/tests/cmdlinemodulemanager.cpp
src/gromacs/commandline/tests/refdata/CommandLineModuleManagerTest_ExportsHelp.xml [new file with mode: 0644]
src/gromacs/commandline/tests/refdata/CommandLineModuleManagerTest_RunsGeneralHelp.xml [new file with mode: 0644]
src/gromacs/utility/file.cpp
src/gromacs/utility/file.h
src/gromacs/utility/fileredirector.cpp [new file with mode: 0644]
src/gromacs/utility/fileredirector.h [new file with mode: 0644]
src/testutils/stringtest.cpp
src/testutils/stringtest.h

index 51241de071d09c1b05b6ddc8c7625a3b99e797da..17233eba548cdb64ba051179f94541742c33bb82 100644 (file)
@@ -60,6 +60,7 @@
 #include "gromacs/utility/baseversion.h"
 #include "gromacs/utility/exceptions.h"
 #include "gromacs/utility/file.h"
+#include "gromacs/utility/fileredirector.h"
 #include "gromacs/utility/gmxassert.h"
 #include "gromacs/utility/programcontext.h"
 #include "gromacs/utility/stringutil.h"
@@ -99,7 +100,7 @@ class CommandLineHelpModuleImpl
         const CommandLineModuleInterface *moduleOverride_;
         bool                              bHidden_;
 
-        File                             *outputOverride_;
+        FileOutputRedirectorInterface    *outputRedirector_;
 
         GMX_DISALLOW_COPY_AND_ASSIGN(CommandLineHelpModuleImpl);
 };
@@ -171,6 +172,7 @@ void RootHelpTopic::writeHelp(const HelpWriterContext &context) const
     // to determine such a set...
     writeSubTopicList(context,
                       "Additional help is available on the following topics:");
+    // TODO: Make these respect the binary name passed in, to make tests work better.
     context.writeTextBlock("To access the help, use '[PROGRAM] help <topic>'.");
     context.writeTextBlock("For help on a command, use '[PROGRAM] help <command>'.");
 }
@@ -434,14 +436,16 @@ class HelpExportReStructuredText : public HelpExportInterface
         virtual void finishModuleGroupExport();
 
     private:
-        HelpLinks                links_;
-        boost::scoped_ptr<File>  indexFile_;
-        boost::scoped_ptr<File>  manPagesFile_;
+        FileOutputRedirectorInterface  *outputRedirector_;
+        HelpLinks                       links_;
+        boost::scoped_ptr<File>         indexFile_;
+        boost::scoped_ptr<File>         manPagesFile_;
 };
 
 HelpExportReStructuredText::HelpExportReStructuredText(
         const CommandLineHelpModuleImpl &helpModule)
-    : links_(eHelpOutputFormat_Rst)
+    : outputRedirector_(helpModule.outputRedirector_),
+      links_(eHelpOutputFormat_Rst)
 {
     File             linksFile("links.dat", "r");
     std::string      line;
@@ -458,10 +462,12 @@ HelpExportReStructuredText::HelpExportReStructuredText(
 
 void HelpExportReStructuredText::startModuleExport()
 {
-    indexFile_.reset(new File("programs/byname.rst", "w"));
+    indexFile_.reset(
+            new File(outputRedirector_->openFileForWriting("programs/byname.rst")));
     indexFile_->writeLine("Tools by Name");
     indexFile_->writeLine("=============");
-    manPagesFile_.reset(new File("conf-man.py", "w"));
+    manPagesFile_.reset(
+            new File(outputRedirector_->openFileForWriting("conf-man.py")));
     manPagesFile_->writeLine("man_pages = [");
 }
 
@@ -472,7 +478,7 @@ void HelpExportReStructuredText::exportModuleHelp(
 {
     // TODO: Ideally, the file would only be touched if it really changes.
     // This would make Sphinx reruns much faster.
-    File file("programs/" + tag + ".rst", "w");
+    File file(outputRedirector_->openFileForWriting("programs/" + tag + ".rst"));
     file.writeLine(formatString(".. _%s:", displayName.c_str()));
     if (0 == displayName.compare("gmx mdrun"))
     {
@@ -518,10 +524,12 @@ void HelpExportReStructuredText::finishModuleExport()
 
 void HelpExportReStructuredText::startModuleGroupExport()
 {
-    indexFile_.reset(new File("programs/bytopic.rst", "w"));
+    indexFile_.reset(
+            new File(outputRedirector_->openFileForWriting("programs/bytopic.rst")));
     indexFile_->writeLine("Tools by Topic");
     indexFile_->writeLine("==============");
-    manPagesFile_.reset(new File("man/bytopic.rst", "w"));
+    manPagesFile_.reset(
+            new File(outputRedirector_->openFileForWriting("man/bytopic.rst")));
 }
 
 void HelpExportReStructuredText::exportModuleGroup(
@@ -642,7 +650,7 @@ CommandLineHelpModuleImpl::CommandLineHelpModuleImpl(
     : rootTopic_(new RootHelpTopic(*this)), programContext_(programContext),
       binaryName_(binaryName), modules_(modules), groups_(groups),
       context_(NULL), moduleOverride_(NULL), bHidden_(false),
-      outputOverride_(NULL)
+      outputRedirector_(&defaultFileOutputRedirector())
 {
 }
 
@@ -714,9 +722,10 @@ void CommandLineHelpModule::setModuleOverride(
     impl_->moduleOverride_ = &module;
 }
 
-void CommandLineHelpModule::setOutputRedirect(File *output)
+void CommandLineHelpModule::setOutputRedirector(
+        FileOutputRedirectorInterface *output)
 {
-    impl_->outputOverride_ = output;
+    impl_->outputRedirector_ = output;
 }
 
 int CommandLineHelpModule::run(int argc, char *argv[])
@@ -749,15 +758,11 @@ int CommandLineHelpModule::run(int argc, char *argv[])
         return 0;
     }
 
-    File *outputFile = &File::standardOutput();
-    if (impl_->outputOverride_ != NULL)
-    {
-        outputFile = impl_->outputOverride_;
-    }
+    File &outputFile = impl_->outputRedirector_->standardOutput();
     HelpLinks                                 links(eHelpOutputFormat_Console);
     initProgramLinks(&links, *impl_);
     boost::scoped_ptr<CommandLineHelpContext> context(
-            new CommandLineHelpContext(outputFile,
+            new CommandLineHelpContext(&outputFile,
                                        eHelpOutputFormat_Console, &links));
     context->setShowHidden(impl_->bHidden_);
     if (impl_->moduleOverride_ != NULL)
index 25eb2376b29021285a63c97cee0fb86ba6238bdd..42f25078d6fc190e703cfd808dc535f4eb96fefc 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * This file is part of the GROMACS molecular simulation package.
  *
- * Copyright (c) 2012,2013,2014, by the GROMACS development team, led by
+ * Copyright (c) 2012,2013,2014,2015, 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.
@@ -52,7 +52,7 @@ namespace gmx
 {
 
 class CommandLineHelpContext;
-class File;
+class FileOutputRedirectorInterface;
 class ProgramContextInterface;
 
 class CommandLineHelpModuleImpl;
@@ -115,12 +115,12 @@ class CommandLineHelpModule : public CommandLineModuleInterface
         void setModuleOverride(const CommandLineModuleInterface &module);
 
         /*! \brief
-         * Sets a file to write help output to instead of default `stdout`.
+         * Sets a file redirector for writing help output.
          *
          * Used for unit testing; see
-         * CommandLineModuleManager::setOutputRedirect() for more details.
+         * CommandLineModuleManager::setOutputRedirector() for more details.
          */
-        void setOutputRedirect(File *output);
+        void setOutputRedirector(FileOutputRedirectorInterface *output);
 
         virtual const char *name() const { return "help"; }
         virtual const char *shortDescription() const
index d29a8212afe67c511256b1f9a8e409171660e315..cd2c65957569a30d5922b163519ea03876f0d566 100644 (file)
@@ -436,10 +436,11 @@ void CommandLineModuleManager::setQuiet(bool bQuiet)
     impl_->bQuiet_ = bQuiet;
 }
 
-void CommandLineModuleManager::setOutputRedirect(File *output)
+void CommandLineModuleManager::setOutputRedirector(
+        FileOutputRedirectorInterface *output)
 {
     impl_->ensureHelpModuleExists();
-    impl_->helpModule_->setOutputRedirect(output);
+    impl_->helpModule_->setOutputRedirector(output);
 }
 
 void CommandLineModuleManager::setSingleModule(CommandLineModuleInterface *module)
index 8a0522b595c7545989e7ca47a43d18adc10cce26..4214ab1f1b6ff592908c748c0955bfb303ff0289 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * This file is part of the GROMACS molecular simulation package.
  *
- * Copyright (c) 2012,2013,2014, by the GROMACS development team, led by
+ * Copyright (c) 2012,2013,2014,2015, 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.
@@ -54,7 +54,7 @@ class CommandLineModuleGroup;
 class CommandLineModuleGroupData;
 class CommandLineModuleInterface;
 class CommandLineProgramContext;
-class File;
+class FileOutputRedirectorInterface;
 
 //! \addtogroup module_commandline
 //! \{
@@ -192,17 +192,21 @@ class CommandLineModuleManager
         /*! \brief
          * Redirects the output of the module manager to a file.
          *
-         * \param[in] output  File to write the output to.
+         * \param[in] output  File redirector to use for output.
          *
          * Normally, the module manager prints explicitly requested text such
          * as help output to `stdout`, but this method can be used to redirect
-         * that output to a file.  This is used for unit tests, either to keep
-         * them quiet or to verify that output.  To keep implementation options
-         * open, behavior with `output == NULL` is undefined and should not be
-         * relied on.  For tests, there should only be need to call this a
-         * single time, right after creating the manager.
+         * that output to a file.  For exporting help from the module manager,
+         * several files are written, and can be redirected with this method as
+         * well.
+         *
+         * This is used for unit tests, either to keep them quiet or to verify
+         * that output.  To keep implementation options open, behavior with
+         * `output == NULL` is undefined and should not be relied on.
+         * For tests, there should only be need to call this a single time,
+         * right after creating the manager.
          */
-        void setOutputRedirect(File *output);
+        void setOutputRedirector(FileOutputRedirectorInterface *output);
 
         /*! \brief
          * Makes the manager always run a single module.
index 7ea4c2511b5a07a936ba09a70100436c4372c2dd..a0cb556813a2f037fdc27945644160e94c89a829 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * This file is part of the GROMACS molecular simulation package.
  *
- * Copyright (c) 2014, by the GROMACS development team, led by
+ * Copyright (c) 2014,2015, 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.
@@ -73,6 +73,12 @@ class CommandLineOptionsModule : public CommandLineModuleInterface
             : name_(name), description_(description), factory_(factory)
         {
         }
+        CommandLineOptionsModule(const char *name, const char *description,
+                                 CommandLineOptionsModuleInterface *module)
+            : name_(name), description_(description), factory_(NULL),
+              module_(module)
+        {
+        }
         virtual const char *name() const { return name_; }
         virtual const char *shortDescription() const { return description_; }
 
@@ -91,7 +97,11 @@ class CommandLineOptionsModule : public CommandLineModuleInterface
 
 void CommandLineOptionsModule::init(CommandLineModuleSettings *settings)
 {
-    module_.reset(factory_());
+    if (!module_)
+    {
+        GMX_RELEASE_ASSERT(factory_ != NULL, "Neither factory nor module provided");
+        module_.reset(factory_());
+    }
     module_->init(settings);
 }
 
@@ -104,7 +114,14 @@ int CommandLineOptionsModule::run(int argc, char *argv[])
 
 void CommandLineOptionsModule::writeHelp(const CommandLineHelpContext &context) const
 {
-    boost::scoped_ptr<CommandLineOptionsModuleInterface> module(factory_());
+    boost::scoped_ptr<CommandLineOptionsModuleInterface> moduleGuard;
+    CommandLineOptionsModuleInterface                   *module = module_.get();
+    if (!module)
+    {
+        GMX_RELEASE_ASSERT(factory_ != NULL, "Neither factory nor module provided");
+        moduleGuard.reset(factory_());
+        module = moduleGuard.get();
+    }
     Options options(name(), shortDescription());
     module->initOptions(&options);
     CommandLineHelpWriter(options)
@@ -164,4 +181,14 @@ void CommandLineOptionsModuleInterface::registerModule(
     manager->addModule(move(module));
 }
 
+// static
+void CommandLineOptionsModuleInterface::registerModule(
+        CommandLineModuleManager *manager, const char *name,
+        const char *description, CommandLineOptionsModuleInterface *module)
+{
+    CommandLineModulePointer wrapperModule(
+            new CommandLineOptionsModule(name, description, module));
+    manager->addModule(move(wrapperModule));
+}
+
 } // namespace gmx
index 5958806576020fe2c653e4c75ef32034b7f6559b..f44022fb081ed8ce30b411e5b5c6e6918585b475 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * This file is part of the GROMACS molecular simulation package.
  *
- * Copyright (c) 2014, by the GROMACS development team, led by
+ * Copyright (c) 2014,2015, 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.
@@ -108,7 +108,7 @@ class CommandLineOptionsModuleInterface
          *     returned by \p factory.  Caller must `delete` the object.
          * \throws  std::bad_alloc if out of memory.
          *
-         * This is mainly used by unit tests that want to bypass
+         * This is mainly used by tests that want to bypass
          * CommandLineModuleManager.
          */
         static CommandLineModuleInterface *
@@ -152,6 +152,27 @@ class CommandLineOptionsModuleInterface
         registerModule(CommandLineModuleManager *manager,
                        const char *name, const char *description,
                        FactoryMethod factory);
+        /*! \brief
+         * Registers a module to this manager.
+         *
+         * \param     manager     Manager to register to.
+         * \param[in] name        Name for the module.
+         * \param[in] description Short description for the module.
+         * \param[in] module      Module to register.
+         *     The method takes ownership (must have been allocated with `new`).
+         * \throws  std::bad_alloc if out of memory.
+         *
+         * This method internally creates a CommandLineModuleInterface module
+         * with the given \p name and \p description, and adds that to
+         * \p manager.
+         *
+         * This method is mainly used by tests that need to have a reference to
+         * the CommandLineOptionsModuleInterface instance (e.g., for mocking).
+         */
+        static void
+        registerModule(CommandLineModuleManager *manager,
+                       const char *name, const char *description,
+                       CommandLineOptionsModuleInterface *module);
 
         virtual ~CommandLineOptionsModuleInterface();
 
index d680cfef6e435964c72e22101944ecf4d58055c3..6ff23d78ece228d7bc6da9adb16d54788189eeea 100644 (file)
 
 #include "gromacs/commandline/cmdlinemodulemanager.h"
 
-#include <vector>
+#include <cstdio>
 
 #include <gmock/gmock.h>
 
 #include "gromacs/commandline/cmdlinehelpcontext.h"
 #include "gromacs/commandline/cmdlinemodule.h"
+#include "gromacs/commandline/cmdlineoptionsmodule.h"
 #include "gromacs/commandline/cmdlineprogramcontext.h"
+#include "gromacs/options/basicoptions.h"
+#include "gromacs/options/options.h"
 #include "gromacs/utility/file.h"
 
 #include "gromacs/onlinehelp/tests/mock_helptopic.h"
 #include "testutils/cmdlinetest.h"
+#include "testutils/stringtest.h"
 #include "testutils/testasserts.h"
 #include "testutils/testfilemanager.h"
 
@@ -63,6 +67,16 @@ namespace
 using gmx::test::CommandLine;
 using gmx::test::MockHelpTopic;
 
+/*! \brief
+ * Helper method to disable nice() calls from CommandLineModuleManager.
+ *
+ * \ingroup module_commandline
+ */
+void disableNice(gmx::CommandLineModuleSettings *settings)
+{
+    settings->setDefaultNiceLevel(0);
+}
+
 /********************************************************************
  * MockModule
  */
@@ -92,11 +106,6 @@ class MockModule : public gmx::CommandLineModuleInterface
         }
 
     private:
-        //! Disable nice() calls for tests.
-        void disableNice(gmx::CommandLineModuleSettings *settings)
-        {
-            settings->setDefaultNiceLevel(0);
-        }
         //! Checks the context passed to writeHelp().
         void checkHelpContext(const gmx::CommandLineHelpContext &context) const;
 
@@ -110,11 +119,10 @@ MockModule::MockModule(const char *name, const char *description)
 {
     using ::testing::_;
     using ::testing::Invoke;
-    using ::testing::WithArg;
     ON_CALL(*this, init(_))
-        .WillByDefault(WithArg<0>(Invoke(this, &MockModule::disableNice)));
+        .WillByDefault(Invoke(&disableNice));
     ON_CALL(*this, writeHelp(_))
-        .WillByDefault(WithArg<0>(Invoke(this, &MockModule::checkHelpContext)));
+        .WillByDefault(Invoke(this, &MockModule::checkHelpContext));
 }
 
 void MockModule::checkHelpContext(const gmx::CommandLineHelpContext &context) const
@@ -128,26 +136,57 @@ void MockModule::checkHelpContext(const gmx::CommandLineHelpContext &context) co
     EXPECT_EQ(expectedDisplayName_, moduleName);
 }
 
+/********************************************************************
+ * MockOptionsModule
+ */
+
+/*! \internal \brief
+ * Mock implementation of gmx::CommandLineOptionsModuleInterface.
+ *
+ * \ingroup module_commandline
+ */
+class MockOptionsModule : public gmx::CommandLineOptionsModuleInterface
+{
+    public:
+        MockOptionsModule();
+
+        MOCK_METHOD1(init, void(gmx::CommandLineModuleSettings *settings));
+        MOCK_METHOD1(initOptions, void(gmx::Options *options));
+        MOCK_METHOD1(optionsFinished, void(gmx::Options *options));
+        MOCK_METHOD0(run, int());
+};
+
+MockOptionsModule::MockOptionsModule()
+{
+    using ::testing::_;
+    using ::testing::Invoke;
+    ON_CALL(*this, init(_))
+        .WillByDefault(Invoke(&disableNice));
+}
+
 /********************************************************************
  * Test fixture for the tests
  */
 
-class CommandLineModuleManagerTest : public ::testing::Test
+class CommandLineModuleManagerTest : public gmx::test::StringTestBase
 {
     public:
         void initManager(const CommandLine &args, const char *realBinaryName);
-        MockModule    &addModule(const char *name, const char *description);
-        MockHelpTopic &addHelpTopic(const char *name, const char *title);
+        MockModule        &addModule(const char *name, const char *description);
+        MockOptionsModule &addOptionsModule(const char *name, const char *description);
+        MockHelpTopic     &addHelpTopic(const char *name, const char *title);
 
         gmx::CommandLineModuleManager &manager() { return *manager_; }
 
-        void ignoreManagerOutput();
+        void redirectManagerOutput()
+        {
+            manager_->setOutputRedirector(&initOutputRedirector(&fileManager_));
+        }
 
     private:
         boost::scoped_ptr<gmx::CommandLineProgramContext> programContext_;
         boost::scoped_ptr<gmx::CommandLineModuleManager>  manager_;
         gmx::test::TestFileManager                        fileManager_;
-        boost::scoped_ptr<gmx::File>                      outputFile_;
 };
 
 void CommandLineModuleManagerTest::initManager(
@@ -169,6 +208,15 @@ CommandLineModuleManagerTest::addModule(const char *name, const char *descriptio
     return *module;
 }
 
+MockOptionsModule &
+CommandLineModuleManagerTest::addOptionsModule(const char *name, const char *description)
+{
+    MockOptionsModule *module = new MockOptionsModule();
+    gmx::CommandLineOptionsModuleInterface::registerModule(
+            &manager(), name, description, module);
+    return *module;
+}
+
 MockHelpTopic &
 CommandLineModuleManagerTest::addHelpTopic(const char *name, const char *title)
 {
@@ -177,13 +225,6 @@ CommandLineModuleManagerTest::addHelpTopic(const char *name, const char *title)
     return *topic;
 }
 
-void CommandLineModuleManagerTest::ignoreManagerOutput()
-{
-    outputFile_.reset(
-            new gmx::File(fileManager_.getTemporaryFilePath("out.txt"), "w"));
-    manager().setOutputRedirect(outputFile_.get());
-}
-
 /********************************************************************
  * Actual tests
  */
@@ -195,12 +236,13 @@ TEST_F(CommandLineModuleManagerTest, RunsGeneralHelp)
     };
     CommandLine       args(cmdline);
     initManager(args, "test");
-    ignoreManagerOutput();
+    redirectManagerOutput();
     addModule("module", "First module");
     addModule("other", "Second module");
     int rc = 0;
     ASSERT_NO_THROW_GMX(rc = manager().run(args.argc(), args.argv()));
     ASSERT_EQ(0, rc);
+    checkRedirectedOutputFiles();
 }
 
 TEST_F(CommandLineModuleManagerTest, RunsModule)
@@ -310,4 +352,50 @@ TEST_F(CommandLineModuleManagerTest, HandlesConflictingBinaryAndModuleNames)
     ASSERT_EQ(0, rc);
 }
 
+/*! \brief
+ * Initializes Options for help export tests.
+ *
+ * \ingroup module_commandline
+ */
+void initOptionsBasic(gmx::Options *options)
+{
+    const char *const desc[] = {
+        "Sample description",
+        "for testing [THISMODULE]."
+    };
+    options->setDescription(desc);
+    options->addOption(gmx::IntegerOption("int"));
+}
+
+TEST_F(CommandLineModuleManagerTest, ExportsHelp)
+{
+    const char *const cmdline[] = {
+        "test", "help", "-export", "rst"
+    };
+    // TODO: Find a more elegant solution, or get rid of the links.dat altogether.
+    gmx::File::writeFileFromString("links.dat", "");
+    CommandLine       args(cmdline);
+    initManager(args, "test");
+    redirectManagerOutput();
+    MockOptionsModule &mod1 = addOptionsModule("module", "First module");
+    MockOptionsModule &mod2 = addOptionsModule("other", "Second module");
+    {
+        gmx::CommandLineModuleGroup group = manager().addModuleGroup("Group 1");
+        group.addModule("module");
+    }
+    {
+        gmx::CommandLineModuleGroup group = manager().addModuleGroup("Group 2");
+        group.addModule("other");
+    }
+    using ::testing::_;
+    using ::testing::Invoke;
+    EXPECT_CALL(mod1, initOptions(_)).WillOnce(Invoke(&initOptionsBasic));
+    EXPECT_CALL(mod2, initOptions(_));
+    int rc = 0;
+    ASSERT_NO_THROW_GMX(rc = manager().run(args.argc(), args.argv()));
+    ASSERT_EQ(0, rc);
+    checkRedirectedOutputFiles();
+    std::remove("links.dat");
+}
+
 } // namespace
diff --git a/src/gromacs/commandline/tests/refdata/CommandLineModuleManagerTest_ExportsHelp.xml b/src/gromacs/commandline/tests/refdata/CommandLineModuleManagerTest_ExportsHelp.xml
new file mode 100644 (file)
index 0000000..d8b6126
--- /dev/null
@@ -0,0 +1,110 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/xsl" href="referencedata.xsl"?>
+<ReferenceData>
+  <String Name="programs/byname.rst"><![CDATA[
+Tools by Name
+=============
+* :doc:`test help <test-help>` - Print help information
+* :doc:`test module <test-module>` - First module
+* :doc:`test other <test-other>` - Second module
+]]></String>
+  <String Name="conf-man.py"><![CDATA[
+man_pages = [
+    ('programs/test-help', 'test-help', "Print help information", '', 1),
+    ('programs/test-module', 'test-module', "First module", '', 1),
+    ('programs/test-other', 'test-other', "Second module", '', 1),
+    ('man/gromacs.7', 'gromacs', 'molecular dynamics simulation suite', '', 7)
+]
+]]></String>
+  <String Name="programs/test-help.rst"><![CDATA[
+.. _test help:
+
+test help
+=========
+
+.. only:: man
+
+   See also
+   --------
+
+   :manpage:`gromacs(7)`
+
+   More information about |Gromacs| is available at <http://www.gromacs.org/>.
+]]></String>
+  <String Name="programs/test-module.rst"><![CDATA[
+.. _test module:
+
+test module
+===========
+Synopsis
+--------
+::
+
+    test module [-int <int>]
+
+Description
+-----------
+Sample description
+for testing ``test module``.
+
+Options
+-------
+Other options:
+
+``-int`` <int>
+
+
+
+.. only:: man
+
+   See also
+   --------
+
+   :manpage:`gromacs(7)`
+
+   More information about |Gromacs| is available at <http://www.gromacs.org/>.
+]]></String>
+  <String Name="programs/test-other.rst"><![CDATA[
+.. _test other:
+
+test other
+==========
+Synopsis
+--------
+::
+
+    test other
+
+
+.. only:: man
+
+   See also
+   --------
+
+   :manpage:`gromacs(7)`
+
+   More information about |Gromacs| is available at <http://www.gromacs.org/>.
+]]></String>
+  <String Name="programs/bytopic.rst"><![CDATA[
+Tools by Topic
+==============
+Group 1
+-------
+| :doc:`test module <test-module>` - First module
+
+Group 2
+-------
+| :doc:`test other <test-other>` - Second module
+
+]]></String>
+  <String Name="man/bytopic.rst"><![CDATA[
+Group 1
++++++++
+| ``test module`` - First module
+
+Group 2
++++++++
+| ``test other`` - Second module
+
+]]></String>
+</ReferenceData>
diff --git a/src/gromacs/commandline/tests/refdata/CommandLineModuleManagerTest_RunsGeneralHelp.xml b/src/gromacs/commandline/tests/refdata/CommandLineModuleManagerTest_RunsGeneralHelp.xml
new file mode 100644 (file)
index 0000000..54c0bfc
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/xsl" href="referencedata.xsl"?>
+<ReferenceData>
+  <String Name="&lt;stdout&gt;"><![CDATA[
+SYNOPSIS
+
+test [-[no]h] [-[no]quiet] [-[no]version] [-[no]copyright] [-nice <int>]
+     [-[no]backup]
+
+OPTIONS
+
+Other options:
+
+ -[no]h          (no)       Print help and quit
+ -[no]quiet      (no)       Do not print common startup info or quotes
+ -[no]version    (no)       Print extended version information and quit
+ -[no]copyright  (yes)      Print copyright information on startup
+ -nice  <int>    (19)       Set the nicelevel (default depends on command)
+ -[no]backup     (yes)      Write backups if output files exist
+
+Additional help is available on the following topics:
+    commands  List of available commands
+To access the help, use 'commandline-test help <topic>'.
+For help on a command, use 'commandline-test help <command>'.
+]]></String>
+</ReferenceData>
index dbd809c38b2bf5a4f1c56737c9a8e88ab8c14871..76147344aa7dbbcccde11c132b842aa92e651140 100644 (file)
@@ -140,6 +140,12 @@ File::File(const std::string &filename, const char *mode)
     open(filename, mode);
 }
 
+File::File(const FileInitializer &initializer)
+    : impl_(new Impl(NULL, true))
+{
+    open(initializer.filename_, initializer.mode_);
+}
+
 File::File(FILE *fp, bool bClose)
     : impl_(new Impl(fp, bClose))
 {
index d1f354c9a44b20745ae3c68587fbe96c35e208cd..5e457360760098321180d4f8c3665dc0b115d44e 100644 (file)
 namespace gmx
 {
 
+class File;
+
+/*! \brief
+ * Parameters for creating a File object.
+ *
+ * This class (mostly) replaces the ability to return a File object from a
+ * function (since File is not copyable): returning a FileInitializer instead
+ * allows the caller to construct the File object.
+ *
+ * \inpublicapi
+ * \ingroup module_utility
+ */
+class FileInitializer
+{
+    public:
+        /*! \brief
+         * Creates the initializer with given parameters.
+         *
+         * The passed strings must remain valid until the initializer is used
+         * to construct a File object.
+         */
+        FileInitializer(const char *filename, const char *mode)
+            : filename_(filename), mode_(mode)
+        {
+        }
+
+    private:
+        const char *filename_;
+        const char *mode_;
+
+        /*! \brief
+         * Needed to allow access to the parameters without otherwise
+         * unnecessary accessors.
+         */
+        friend class File;
+};
+
 /*! \brief
  * Basic file object.
  *
@@ -90,6 +127,14 @@ class File
         File(const char *filename, const char *mode);
         //! \copydoc File(const char *, const char *)
         File(const std::string &filename, const char *mode);
+        /*! \brief
+         * Creates a file object and opens a file.
+         *
+         * \param[in] initializer  Parameters to open the file.
+         * \throws    std::bad_alloc if out of memory.
+         * \throws    FileIOError on any I/O error.
+         */
+        File(const FileInitializer &initializer);
         /*! \brief
          * Destroys the file object.
          *
diff --git a/src/gromacs/utility/fileredirector.cpp b/src/gromacs/utility/fileredirector.cpp
new file mode 100644 (file)
index 0000000..9a14061
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * This file is part of the GROMACS molecular simulation package.
+ *
+ * Copyright (c) 2015, 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 classes and functions from fileredirector.h.
+ *
+ * \author Teemu Murtola <teemu.murtola@gmail.com>
+ * \ingroup module_utility
+ */
+#include "gmxpre.h"
+
+#include "fileredirector.h"
+
+#include "gromacs/utility/file.h"
+
+namespace gmx
+{
+
+FileOutputRedirectorInterface::~FileOutputRedirectorInterface()
+{
+}
+
+namespace
+{
+
+/*! \internal
+ * \brief
+ * Implements the redirector returned by defaultFileOutputRedirector().
+ *
+ * Does not redirect anything, but instead opens the files exactly as
+ * requested.
+ *
+ * \ingroup module_utility
+ */
+class DefaultOutputRedirector : public FileOutputRedirectorInterface
+{
+    public:
+        virtual File &standardOutput()
+        {
+            return File::standardOutput();
+        }
+        virtual FileInitializer openFileForWriting(const char *filename)
+        {
+            return FileInitializer(filename, "w");
+        }
+};
+
+}   // namespace
+
+//! \cond libapi
+FileOutputRedirectorInterface &defaultFileOutputRedirector()
+{
+    static DefaultOutputRedirector instance;
+    return instance;
+}
+//! \endcond
+
+} // namespace gmx
diff --git a/src/gromacs/utility/fileredirector.h b/src/gromacs/utility/fileredirector.h
new file mode 100644 (file)
index 0000000..f65ce72
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * This file is part of the GROMACS molecular simulation package.
+ *
+ * Copyright (c) 2015, 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::FileOutputRedirectorInterface.
+ *
+ * \author Teemu Murtola <teemu.murtola@gmail.com>
+ * \inlibraryapi
+ * \ingroup module_utility
+ */
+#ifndef GMX_UTILITY_FILEREDIRECTOR_H
+#define GMX_UTILITY_FILEREDIRECTOR_H
+
+#include <string>
+
+#include "gromacs/utility/file.h"
+
+namespace gmx
+{
+
+/*! \libinternal \brief
+ * Allows capturing `stdout` and file output from code that supports it.
+ *
+ * The calling code should take in this interface and use the File objects
+ * it returns for all output that needs to support this redirection.
+ * By default, the code can then use defaultFileOutputRedirector() in case no
+ * redirection is needed.
+ *
+ * This allows tests to capture the file output without duplicating the
+ * knowledge of which files are actually produced.  With some further
+ * refactoring of the File class, this could support capturing the output into
+ * in-memory buffers as well, but for now the current capabilities are
+ * sufficient.
+ *
+ * \inlibraryapi
+ * \ingroup module_utility
+ */
+class FileOutputRedirectorInterface
+{
+    public:
+        virtual ~FileOutputRedirectorInterface();
+
+        /*! \brief
+         * Returns a File object to use for `stdout` output.
+         */
+        virtual File &standardOutput() = 0;
+        /*! \brief
+         * Returns a File object to use for output to a given file.
+         *
+         * \param[in] filename  Requested file name.
+         */
+        virtual FileInitializer openFileForWriting(const char *filename) = 0;
+
+        //! Convenience method to open a file using an std::string path.
+        FileInitializer openFileForWriting(const std::string &filename)
+        {
+            return openFileForWriting(filename.c_str());
+        }
+};
+
+//! \cond libapi
+/*! \brief
+ * Returns default implementation for FileOutputRedirectorInterface.
+ *
+ * The returned implementation does not redirect anything, but just opens the
+ * files at requested locations.
+ *
+ * \ingroup module_utility
+ */
+FileOutputRedirectorInterface &defaultFileOutputRedirector();
+//! \endcond
+
+} // namespace gmx
+
+#endif
index 93b8bf9c757d4be8d4c02090f95a7b096f9d38ad..7ce6008345f5ee323fcd8ee11bf075a04f09d39c 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * This file is part of the GROMACS molecular simulation package.
  *
- * Copyright (c) 2012,2013,2014, by the GROMACS development team, led by
+ * Copyright (c) 2012,2013,2014,2015, 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.
 
 #include "stringtest.h"
 
+#include <algorithm>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <boost/scoped_ptr.hpp>
+
 #include "gromacs/options/basicoptions.h"
 #include "gromacs/options/options.h"
+#include "gromacs/utility/exceptions.h"
 #include "gromacs/utility/file.h"
+#include "gromacs/utility/fileredirector.h"
 
+#include "testutils/refdata.h"
+#include "testutils/testexceptions.h"
+#include "testutils/testfilemanager.h"
 #include "testutils/testoptions.h"
 
 namespace gmx
@@ -58,6 +70,27 @@ namespace
 {
 //! Stores the -stdout flag value to print out values instead of checking them.
 bool g_bWriteToStdOut = false;
+
+/*! \brief
+ * Helper for checking a block of text, e.g., implementing the `-stdout`
+ * option.
+ *
+ * \ingroup module_testutils
+ */
+void checkTextImpl(TestReferenceChecker *checker, const std::string &text,
+                   const char *id)
+{
+    if (g_bWriteToStdOut)
+    {
+        printf("%s:\n", id);
+        printf("%s[END]\n", text.c_str());
+    }
+    else
+    {
+        checker->checkStringBlock(text, id);
+    }
+}
+
 }
 
 // TODO: Only add this option to those test binaries that actually need it
@@ -73,7 +106,92 @@ GMX_TEST_OPTIONS(StringTestOptions, options)
 }
 //! \endcond
 
+/********************************************************************
+ * TestFileOutputRedirector
+ */
+
+/*! \internal
+ * \brief
+ * Implementation of FileOutputRedirectorInterface for tests.
+ *
+ * This class redirects all output files to temporary files managed by a
+ * TestFileManager, and supports checking the contents of these files using the
+ * reference data framework.
+ *
+ * \ingroup module_testutils
+ */
+class TestFileOutputRedirector : public FileOutputRedirectorInterface
+{
+    public:
+        //! Initializes the redirector with the given file manager.
+        explicit TestFileOutputRedirector(TestFileManager *fileManager)
+            : fileManager_(*fileManager)
+        {
+        }
+
+        virtual File &standardOutput()
+        {
+            if (!stdoutFile_)
+            {
+                const std::string path = fileManager_.getTemporaryFilePath("stdout.txt");
+                stdoutFile_.reset(new File(path, "w"));
+                fileList_.push_back(FileListEntry("<stdout>", path));
+            }
+            return *stdoutFile_;
+        }
+        virtual FileInitializer openFileForWriting(const char *filename)
+        {
+            std::string       suffix = filename;
+            std::replace(suffix.begin(), suffix.end(), '/', '_');
+            const std::string path = fileManager_.getTemporaryFilePath(suffix);
+            fileList_.push_back(FileListEntry(filename, path));
+            return FileInitializer(fileList_.back().second.c_str(), "w");
+        }
+
+        /*! \brief
+         * Checks the contents of all redirected files.
+         */
+        void checkRedirectedFiles(TestReferenceChecker *checker)
+        {
+            if (stdoutFile_)
+            {
+                stdoutFile_->close();
+                stdoutFile_.reset();
+            }
+            std::vector<FileListEntry>::const_iterator i;
+            for (i = fileList_.begin(); i != fileList_.end(); ++i)
+            {
+                const std::string text = File::readToString(i->second);
+                checkTextImpl(checker, text, i->first.c_str());
+            }
+        }
+
+    private:
+        typedef std::pair<std::string, std::string> FileListEntry;
+
+        TestFileManager            &fileManager_;
+        boost::scoped_ptr<File>     stdoutFile_;
+        std::vector<FileListEntry>  fileList_;
+};
+
+/********************************************************************
+ * StringTestBase::Impl
+ */
+
+class StringTestBase::Impl
+{
+    public:
+        TestReferenceData                           data_;
+        boost::scoped_ptr<TestReferenceChecker>     checker_;
+        boost::scoped_ptr<TestFileOutputRedirector> redirector_;
+};
+
+/********************************************************************
+ * StringTestBase
+ */
+
 StringTestBase::StringTestBase()
+    : impl_(new Impl)
 {
 }
 
@@ -81,36 +199,49 @@ StringTestBase::~StringTestBase()
 {
 }
 
+FileOutputRedirectorInterface &
+StringTestBase::initOutputRedirector(TestFileManager *fileManager)
+{
+    if (impl_->redirector_)
+    {
+        GMX_THROW(TestException("initOutputRedirector() called more than once"));
+    }
+    impl_->redirector_.reset(new TestFileOutputRedirector(fileManager));
+    return *impl_->redirector_;
+}
+
 TestReferenceChecker &
 StringTestBase::checker()
 {
-    if (checker_.get() == NULL)
+    if (!impl_->checker_)
     {
-        checker_.reset(new TestReferenceChecker(data_.rootChecker()));
+        impl_->checker_.reset(new TestReferenceChecker(impl_->data_.rootChecker()));
     }
-    return *checker_;
+    return *impl_->checker_;
 }
 
 void
 StringTestBase::checkText(const std::string &text, const char *id)
 {
-    if (g_bWriteToStdOut)
-    {
-        printf("%s:\n", id);
-        printf("%s[END]\n", text.c_str());
-    }
-    else
-    {
-        checker().checkStringBlock(text, id);
-    }
+    checkTextImpl(&checker(), text, id);
 }
 
 void
 StringTestBase::checkFileContents(const std::string &filename, const char *id)
 {
-    std::string text = File::readToString(filename);
+    const std::string text = File::readToString(filename);
     checkText(text, id);
 }
 
+void
+StringTestBase::checkRedirectedOutputFiles()
+{
+    if (!impl_->redirector_)
+    {
+        GMX_THROW(TestException("initOutputRedirector() not called"));
+    }
+    impl_->redirector_->checkRedirectedFiles(&checker());
+}
+
 } // namespace test
 } // namespace gmx
index 502f207bf6b32620f68d8ba2ce1ba641f89dd75b..67e89113a3b7e6dc0d2b06275ae0b53eae0c241c 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * This file is part of the GROMACS molecular simulation package.
  *
- * Copyright (c) 2012,2013,2014, by the GROMACS development team, led by
+ * Copyright (c) 2012,2013,2014,2015, 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.
 
 #include <string>
 
-#include <boost/scoped_ptr.hpp>
 #include <gtest/gtest.h>
 
-#include "testutils/refdata.h"
+#include "gromacs/utility/classhelpers.h"
 
 namespace gmx
 {
+
+class FileOutputRedirectorInterface;
+
 namespace test
 {
 
+class TestFileManager;
+class TestReferenceChecker;
+
 /*! \libinternal \brief
  * Test fixture for tests that check string formatting.
  *
@@ -72,6 +77,18 @@ class StringTestBase : public ::testing::Test
         StringTestBase();
         ~StringTestBase();
 
+        /*! \brief
+         * Creates a redirector that directs all output to temporary files.
+         *
+         * \param[in] fileManager  File manager to use for temporary files.
+         *
+         * Can only be called once in a test.
+         *
+         * \see checkRedirectedOutputFiles()
+         */
+        FileOutputRedirectorInterface &
+        initOutputRedirector(TestFileManager *fileManager);
+
         /*! \brief
          * Returns the root checker for this test's reference data.
          *
@@ -81,14 +98,14 @@ class StringTestBase : public ::testing::Test
         TestReferenceChecker &checker();
 
         /*! \brief
-         * Check a string.
+         * Checks a string.
          *
          * \param[in] text  String to check.
          * \param[in] id    Unique (within a single test) id for the string.
          */
         void checkText(const std::string &text, const char *id);
         /*! \brief
-         * Check contents of a file as a single string.
+         * Checks contents of a file as a single string.
          *
          * \param[in] filename  Name of the file to check.
          * \param[in] id        Unique (within a single test) id for the string.
@@ -97,10 +114,25 @@ class StringTestBase : public ::testing::Test
          * single string and calls checkText().
          */
         void checkFileContents(const std::string &filename, const char *id);
+        /*! \brief
+         * Checks contents of all files redirected with initOutputRedirector().
+         *
+         * Uses the same logic as checkFileContents() to check each file
+         * (including `stdout`) that has been created using the redirector
+         * returned by initOutputRedirector().
+         *
+         * initOutputRedirector() must have been called.
+         * This method should not be called if the redirector will still be
+         * used for further output in the test.  Behavior is not designed for
+         * checking in the middle of the test, although that could potentially
+         * be changed if necessary.
+         */
+        void checkRedirectedOutputFiles();
 
     private:
-        TestReferenceData                       data_;
-        boost::scoped_ptr<TestReferenceChecker> checker_;
+        class Impl;
+
+        PrivateImplPointer<Impl> impl_;
 };
 
 } // namespace test