a03b895abb3cf4de20cd90889f74cf0543ba7fe0
[alexxy/gromacs.git] / src / testutils / cmdlinetest.cpp
1 /*
2  * This file is part of the GROMACS molecular simulation package.
3  *
4  * Copyright (c) 2012,2013,2014,2015,2016 by the GROMACS development team.
5  * Copyright (c) 2017,2018,2019,2020,2021, by the GROMACS development team, led by
6  * Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
7  * and including many others, as listed in the AUTHORS file in the
8  * top-level source directory and at http://www.gromacs.org.
9  *
10  * GROMACS is free software; you can redistribute it and/or
11  * modify it under the terms of the GNU Lesser General Public License
12  * as published by the Free Software Foundation; either version 2.1
13  * of the License, or (at your option) any later version.
14  *
15  * GROMACS is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18  * Lesser General Public License for more details.
19  *
20  * You should have received a copy of the GNU Lesser General Public
21  * License along with GROMACS; if not, see
22  * http://www.gnu.org/licenses, or write to the Free Software Foundation,
23  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
24  *
25  * If you want to redistribute modifications to GROMACS, please
26  * consider that scientific software is very special. Version
27  * control is crucial - bugs must be traceable. We will be happy to
28  * consider code for inclusion in the official distribution, but
29  * derived work must not be called official GROMACS. Details are found
30  * in the README & COPYING files - if they are missing, get the
31  * official version at http://www.gromacs.org.
32  *
33  * To help us fund GROMACS development, we humbly ask that you cite
34  * the research papers on the package. Check out http://www.gromacs.org.
35  */
36 /*! \internal \file
37  * \brief
38  * Implements classes from cmdlinetest.h.
39  *
40  * \author Teemu Murtola <teemu.murtola@gmail.com>
41  * \ingroup module_testutils
42  */
43 #include "gmxpre.h"
44
45 #include "testutils/cmdlinetest.h"
46
47 #include <cstdlib>
48 #include <cstring>
49
50 #include <memory>
51 #include <new>
52 #include <sstream>
53 #include <vector>
54
55 #include "gromacs/commandline/cmdlinehelpcontext.h"
56 #include "gromacs/commandline/cmdlineoptionsmodule.h"
57 #include "gromacs/commandline/cmdlineprogramcontext.h"
58 #include "gromacs/utility/arrayref.h"
59 #include "gromacs/utility/futil.h"
60 #include "gromacs/utility/gmxassert.h"
61 #include "gromacs/utility/strconvert.h"
62 #include "gromacs/utility/stringstream.h"
63 #include "gromacs/utility/stringutil.h"
64 #include "gromacs/utility/textreader.h"
65 #include "gromacs/utility/textwriter.h"
66
67 #include "testutils/filematchers.h"
68 #include "testutils/refdata.h"
69 #include "testutils/testfilemanager.h"
70
71 namespace gmx
72 {
73 namespace test
74 {
75
76 /********************************************************************
77  * CommandLine::Impl
78  */
79
80 class CommandLine::Impl
81 {
82 public:
83     Impl(const ArrayRef<const char* const>& cmdline);
84     Impl(const ArrayRef<const std::string>& cmdline);
85     ~Impl();
86
87     std::vector<char*> args_;
88     std::vector<char*> argv_;
89     int                argc_;
90 };
91
92 CommandLine::Impl::Impl(const ArrayRef<const char* const>& cmdline)
93 {
94     args_.reserve(cmdline.size());
95     argv_.reserve(cmdline.size() + 1);
96     argc_ = ssize(cmdline);
97     for (const auto& arg : cmdline)
98     {
99         char* argCopy = strdup(arg);
100         if (argCopy == nullptr)
101         {
102             throw std::bad_alloc();
103         }
104         args_.push_back(argCopy);
105         argv_.push_back(argCopy);
106     }
107     argv_.push_back(nullptr);
108 }
109
110 namespace
111 {
112
113 //! Helper function so we can delegate from the std::string constructor to the const char * one.
114 std::vector<const char*> convertFromStringArrayRef(const ArrayRef<const std::string>& cmdline)
115 {
116     std::vector<const char*> v(cmdline.size());
117     std::transform(
118             cmdline.begin(), cmdline.end(), v.begin(), [](const std::string& s) { return s.c_str(); });
119     return v;
120 }
121
122 } // namespace
123
124 // This makes a new temporary vector of views of the const char * in
125 // the view passed in. Those are then deep copied in the constructor
126 // delegated to.
127 CommandLine::Impl::Impl(const ArrayRef<const std::string>& cmdline) :
128     Impl(convertFromStringArrayRef(cmdline))
129 {
130 }
131
132 CommandLine::Impl::~Impl()
133 {
134     for (size_t i = 0; i < args_.size(); ++i)
135     {
136         std::free(args_[i]);
137     }
138 }
139
140 /********************************************************************
141  * CommandLine
142  */
143
144 CommandLine::CommandLine() : impl_(new Impl(ArrayRef<const char*>{})) {}
145
146 CommandLine::CommandLine(const ArrayRef<const char* const>& cmdline) : impl_(new Impl(cmdline)) {}
147
148 CommandLine::CommandLine(const ArrayRef<const std::string>& cmdline) : impl_(new Impl(cmdline)) {}
149
150 CommandLine::CommandLine(const CommandLine& other) :
151     impl_(new Impl(arrayRefFromArray(other.argv(), other.argc())))
152 {
153 }
154
155 CommandLine::~CommandLine() {}
156
157 void CommandLine::initFromArray(const ArrayRef<const char* const>& cmdline)
158 {
159     impl_.reset(new Impl(cmdline));
160 }
161
162 void CommandLine::append(const char* arg)
163 {
164     GMX_RELEASE_ASSERT(impl_->argc_ == ssize(impl_->args_),
165                        "Command-line has been modified externally");
166     size_t newSize = impl_->args_.size() + 1;
167     impl_->args_.reserve(newSize);
168     impl_->argv_.reserve(newSize + 1);
169     char* newArg = strdup(arg);
170     if (newArg == nullptr)
171     {
172         throw std::bad_alloc();
173     }
174     impl_->args_.push_back(newArg);
175     impl_->argv_.pop_back(); // Remove the trailing NULL.
176     impl_->argv_.push_back(newArg);
177     impl_->argv_.push_back(nullptr);
178     impl_->argc_ = static_cast<int>(newSize);
179 }
180
181 void CommandLine::addOption(const char* name)
182 {
183     append(name);
184 }
185
186 void CommandLine::addOption(const char* name, const char* value)
187 {
188     append(name);
189     append(value);
190 }
191
192 void CommandLine::addOption(const char* name, const std::string& value)
193 {
194     addOption(name, value.c_str());
195 }
196
197 void CommandLine::addOption(const char* name, int value)
198 {
199     append(name);
200     append(gmx::toString(value));
201 }
202
203 void CommandLine::addOption(const char* name, double value)
204 {
205     append(name);
206     append(gmx::toString(value));
207 }
208
209 void CommandLine::merge(const CommandLine& args)
210 {
211     if (args.argc() > 0)
212     {
213         // Skip first argument if it is the module name.
214         const int firstArg = (args.arg(0)[0] == '-' ? 0 : 1);
215         for (int i = firstArg; i < args.argc(); ++i)
216         {
217             append(args.arg(i));
218         }
219     }
220 }
221
222 int& CommandLine::argc()
223 {
224     return impl_->argc_;
225 }
226 char** CommandLine::argv()
227 {
228     return &impl_->argv_[0];
229 }
230 int CommandLine::argc() const
231 {
232     return impl_->argc_;
233 }
234 const char* const* CommandLine::argv() const
235 {
236     return &impl_->argv_[0];
237 }
238 const char* CommandLine::arg(int i) const
239 {
240     return impl_->argv_[i];
241 }
242
243 std::string CommandLine::toString() const
244 {
245     return CommandLineProgramContext(argc(), argv()).commandLine();
246 }
247
248 bool CommandLine::contains(const char* name) const
249 {
250     for (int i = 0; i < impl_->argc_; ++i)
251     {
252         if (std::strcmp(arg(i), name) == 0)
253         {
254             return true;
255         }
256     }
257     return false;
258 }
259
260 /********************************************************************
261  * CommandLineTestHelper::Impl
262  */
263
264 class CommandLineTestHelper::Impl
265 {
266 public:
267     struct OutputFileInfo
268     {
269         OutputFileInfo(const char* option, const std::string& path, FileMatcherPointer matcher) :
270             option(option), path(path), matcher(move(matcher))
271         {
272         }
273
274         std::string        option;
275         std::string        path;
276         FileMatcherPointer matcher;
277     };
278
279     typedef std::vector<OutputFileInfo> OutputFileList;
280
281     explicit Impl(TestFileManager* fileManager) : fileManager_(*fileManager) {}
282
283     TestFileManager& fileManager_;
284     OutputFileList   outputFiles_;
285 };
286
287 /********************************************************************
288  * CommandLineTestHelper
289  */
290
291 // static
292 int CommandLineTestHelper::runModuleDirect(ICommandLineModule* module, CommandLine* commandLine)
293 {
294     CommandLineModuleSettings settings;
295     module->init(&settings);
296     return module->run(commandLine->argc(), commandLine->argv());
297 }
298
299 // static
300 int CommandLineTestHelper::runModuleDirect(std::unique_ptr<ICommandLineOptionsModule> module,
301                                            CommandLine*                               commandLine)
302 {
303     // The name and description are not used in the tests, so they can be NULL.
304     const std::unique_ptr<ICommandLineModule> wrapperModule(
305             ICommandLineOptionsModule::createModule(nullptr, nullptr, std::move(module)));
306     return runModuleDirect(wrapperModule.get(), commandLine);
307 }
308
309 // static
310 int CommandLineTestHelper::runModuleFactory(
311         const std::function<std::unique_ptr<ICommandLineOptionsModule>()>& factory,
312         CommandLine*                                                       commandLine)
313 {
314     return runModuleDirect(factory(), commandLine);
315 }
316
317 CommandLineTestHelper::CommandLineTestHelper(TestFileManager* fileManager) :
318     impl_(new Impl(fileManager))
319 {
320 }
321
322 CommandLineTestHelper::~CommandLineTestHelper() {}
323
324 void CommandLineTestHelper::setInputFileContents(CommandLine*       args,
325                                                  const char*        option,
326                                                  const char*        extension,
327                                                  const std::string& contents)
328 {
329     GMX_ASSERT(extension[0] != '.', "Extension should not contain a dot");
330     std::string fullFilename =
331             impl_->fileManager_.getTemporaryFilePath(formatString("%d.%s", args->argc(), extension));
332     TextWriter::writeFileFromString(fullFilename, contents);
333     args->addOption(option, fullFilename);
334 }
335
336 void CommandLineTestHelper::setInputFileContents(CommandLine*                       args,
337                                                  const char*                        option,
338                                                  const char*                        extension,
339                                                  const ArrayRef<const char* const>& contents)
340 {
341     GMX_ASSERT(extension[0] != '.', "Extension should not contain a dot");
342     std::string fullFilename =
343             impl_->fileManager_.getTemporaryFilePath(formatString("%d.%s", args->argc(), extension));
344     TextWriter                                  file(fullFilename);
345     ArrayRef<const char* const>::const_iterator i;
346     for (i = contents.begin(); i != contents.end(); ++i)
347     {
348         file.writeLine(*i);
349     }
350     file.close();
351     args->addOption(option, fullFilename);
352 }
353
354 void CommandLineTestHelper::setOutputFile(CommandLine*                     args,
355                                           const char*                      option,
356                                           const char*                      filename,
357                                           const ITextBlockMatcherSettings& matcher)
358 {
359     setOutputFile(args, option, filename, TextFileMatch(matcher));
360 }
361
362 void CommandLineTestHelper::setOutputFile(CommandLine*                args,
363                                           const char*                 option,
364                                           const char*                 filename,
365                                           const IFileMatcherSettings& matcher)
366 {
367     std::string suffix(filename);
368     if (startsWith(filename, "."))
369     {
370         suffix = formatString("%d.%s", args->argc(), filename);
371     }
372     std::string fullFilename = impl_->fileManager_.getTemporaryFilePath(suffix);
373     args->addOption(option, fullFilename);
374     impl_->outputFiles_.emplace_back(option, fullFilename, matcher.createFileMatcher());
375 }
376
377 void CommandLineTestHelper::checkOutputFiles(TestReferenceChecker checker) const
378 {
379     if (!impl_->outputFiles_.empty())
380     {
381         TestReferenceChecker outputChecker(checker.checkCompound("OutputFiles", "Files"));
382         for (const auto& outfile : impl_->outputFiles_)
383         {
384             TestReferenceChecker fileChecker(outputChecker.checkCompound("File", outfile.option.c_str()));
385             outfile.matcher->checkFile(outfile.path, &fileChecker);
386         }
387     }
388 }
389
390 /********************************************************************
391  * CommandLineTestBase::Impl
392  */
393
394 class CommandLineTestBase::Impl
395 {
396 public:
397     Impl() : helper_(&tempFiles_) { cmdline_.append("module"); }
398
399     TestReferenceData     data_;
400     TestFileManager       tempFiles_;
401     CommandLineTestHelper helper_;
402     CommandLine           cmdline_;
403 };
404
405 /********************************************************************
406  * CommandLineTestBase
407  */
408
409 CommandLineTestBase::CommandLineTestBase() : impl_(new Impl) {}
410
411 CommandLineTestBase::~CommandLineTestBase() {}
412
413 void CommandLineTestBase::setInputFile(const char* option, const char* filename)
414 {
415     impl_->cmdline_.addOption(option, TestFileManager::getInputFilePath(filename));
416 }
417
418 void CommandLineTestBase::setInputFile(const char* option, const std::string& filename)
419 {
420     setInputFile(option, filename.c_str());
421 }
422
423 void CommandLineTestBase::setModifiableInputFile(const char* option, const std::string& filename)
424 {
425     setModifiableInputFile(option, filename.c_str());
426 }
427
428 void CommandLineTestBase::setModifiableInputFile(const char* option, const char* filename)
429 {
430     std::string originalFileName   = gmx::test::TestFileManager::getInputFilePath(filename);
431     std::string modifiableFileName = fileManager().getTemporaryFilePath(filename);
432     gmx_file_copy(originalFileName.c_str(), modifiableFileName.c_str(), true);
433     impl_->cmdline_.addOption(option, modifiableFileName);
434 }
435
436 void CommandLineTestBase::setInputFileContents(const char*        option,
437                                                const char*        extension,
438                                                const std::string& contents)
439 {
440     impl_->helper_.setInputFileContents(&impl_->cmdline_, option, extension, contents);
441 }
442
443 void CommandLineTestBase::setInputFileContents(const char*                        option,
444                                                const char*                        extension,
445                                                const ArrayRef<const char* const>& contents)
446 {
447     impl_->helper_.setInputFileContents(&impl_->cmdline_, option, extension, contents);
448 }
449
450 void CommandLineTestBase::setOutputFile(const char*                      option,
451                                         const char*                      filename,
452                                         const ITextBlockMatcherSettings& matcher)
453 {
454     impl_->helper_.setOutputFile(&impl_->cmdline_, option, filename, matcher);
455 }
456
457 void CommandLineTestBase::setOutputFile(const char*                 option,
458                                         const char*                 filename,
459                                         const IFileMatcherSettings& matcher)
460 {
461     impl_->helper_.setOutputFile(&impl_->cmdline_, option, filename, matcher);
462 }
463
464 void CommandLineTestBase::setInputAndOutputFile(const char*                      option,
465                                                 const char*                      filename,
466                                                 const ITextBlockMatcherSettings& matcher)
467 {
468     std::string originalFileName   = gmx::test::TestFileManager::getInputFilePath(filename);
469     std::string modifiableFileName = fileManager().getTemporaryFilePath(filename);
470     gmx_file_copy(originalFileName.c_str(), modifiableFileName.c_str(), true);
471     impl_->helper_.setOutputFile(&impl_->cmdline_, option, filename, matcher);
472 }
473
474 void CommandLineTestBase::setInputAndOutputFile(const char*                 option,
475                                                 const char*                 filename,
476                                                 const IFileMatcherSettings& matcher)
477 {
478     std::string originalFileName   = gmx::test::TestFileManager::getInputFilePath(filename);
479     std::string modifiableFileName = fileManager().getTemporaryFilePath(filename);
480     gmx_file_copy(originalFileName.c_str(), modifiableFileName.c_str(), true);
481     impl_->helper_.setOutputFile(&impl_->cmdline_, option, filename, matcher);
482 }
483
484 CommandLine& CommandLineTestBase::commandLine()
485 {
486     return impl_->cmdline_;
487 }
488
489 TestFileManager& CommandLineTestBase::fileManager()
490 {
491     return impl_->tempFiles_;
492 }
493
494 TestReferenceChecker CommandLineTestBase::rootChecker()
495 {
496     return impl_->data_.rootChecker();
497 }
498
499 void CommandLineTestBase::setDefaultTolerance(const FloatingPointTolerance& tolerance)
500 {
501     impl_->data_.rootChecker().setDefaultTolerance(tolerance);
502 }
503
504 void CommandLineTestBase::testWriteHelp(ICommandLineModule* module)
505 {
506     StringOutputStream     stream;
507     TextWriter             writer(&stream);
508     CommandLineHelpContext context(&writer, eHelpOutputFormat_Console, nullptr, "test");
509     context.setModuleDisplayName(formatString("%s %s", "test", module->name()));
510     module->writeHelp(context);
511     TestReferenceChecker checker(rootChecker());
512     checker.checkTextBlock(stream.toString(), "HelpOutput");
513 }
514
515 void CommandLineTestBase::checkOutputFiles()
516 {
517     impl_->helper_.checkOutputFiles(rootChecker());
518 }
519
520 } // namespace test
521 } // namespace gmx