ab2b4c0b2eb9a8dc75d0fd4d784e657d4c2d2b49
[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, 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),
271             path(path),
272             matcher(move(matcher))
273         {
274         }
275
276         std::string        option;
277         std::string        path;
278         FileMatcherPointer matcher;
279     };
280
281     typedef std::vector<OutputFileInfo> OutputFileList;
282
283     explicit Impl(TestFileManager* fileManager) : fileManager_(*fileManager) {}
284
285     TestFileManager& fileManager_;
286     OutputFileList   outputFiles_;
287 };
288
289 /********************************************************************
290  * CommandLineTestHelper
291  */
292
293 // static
294 int CommandLineTestHelper::runModuleDirect(ICommandLineModule* module, CommandLine* commandLine)
295 {
296     CommandLineModuleSettings settings;
297     module->init(&settings);
298     return module->run(commandLine->argc(), commandLine->argv());
299 }
300
301 // static
302 int CommandLineTestHelper::runModuleDirect(std::unique_ptr<ICommandLineOptionsModule> module,
303                                            CommandLine*                               commandLine)
304 {
305     // The name and description are not used in the tests, so they can be NULL.
306     const std::unique_ptr<ICommandLineModule> wrapperModule(
307             ICommandLineOptionsModule::createModule(nullptr, nullptr, std::move(module)));
308     return runModuleDirect(wrapperModule.get(), commandLine);
309 }
310
311 // static
312 int CommandLineTestHelper::runModuleFactory(
313         const std::function<std::unique_ptr<ICommandLineOptionsModule>()>& factory,
314         CommandLine*                                                       commandLine)
315 {
316     return runModuleDirect(factory(), commandLine);
317 }
318
319 CommandLineTestHelper::CommandLineTestHelper(TestFileManager* fileManager) :
320     impl_(new Impl(fileManager))
321 {
322 }
323
324 CommandLineTestHelper::~CommandLineTestHelper() {}
325
326 void CommandLineTestHelper::setInputFileContents(CommandLine*       args,
327                                                  const char*        option,
328                                                  const char*        extension,
329                                                  const std::string& contents)
330 {
331     GMX_ASSERT(extension[0] != '.', "Extension should not contain a dot");
332     std::string fullFilename =
333             impl_->fileManager_.getTemporaryFilePath(formatString("%d.%s", args->argc(), extension));
334     TextWriter::writeFileFromString(fullFilename, contents);
335     args->addOption(option, fullFilename);
336 }
337
338 void CommandLineTestHelper::setInputFileContents(CommandLine*                       args,
339                                                  const char*                        option,
340                                                  const char*                        extension,
341                                                  const ArrayRef<const char* const>& contents)
342 {
343     GMX_ASSERT(extension[0] != '.', "Extension should not contain a dot");
344     std::string fullFilename =
345             impl_->fileManager_.getTemporaryFilePath(formatString("%d.%s", args->argc(), extension));
346     TextWriter                                  file(fullFilename);
347     ArrayRef<const char* const>::const_iterator i;
348     for (i = contents.begin(); i != contents.end(); ++i)
349     {
350         file.writeLine(*i);
351     }
352     file.close();
353     args->addOption(option, fullFilename);
354 }
355
356 void CommandLineTestHelper::setOutputFile(CommandLine*                     args,
357                                           const char*                      option,
358                                           const char*                      filename,
359                                           const ITextBlockMatcherSettings& matcher)
360 {
361     setOutputFile(args, option, filename, TextFileMatch(matcher));
362 }
363
364 void CommandLineTestHelper::setOutputFile(CommandLine*                args,
365                                           const char*                 option,
366                                           const char*                 filename,
367                                           const IFileMatcherSettings& matcher)
368 {
369     std::string suffix(filename);
370     if (startsWith(filename, "."))
371     {
372         suffix = formatString("%d.%s", args->argc(), filename);
373     }
374     std::string fullFilename = impl_->fileManager_.getTemporaryFilePath(suffix);
375     args->addOption(option, fullFilename);
376     impl_->outputFiles_.emplace_back(option, fullFilename, matcher.createFileMatcher());
377 }
378
379 void CommandLineTestHelper::checkOutputFiles(TestReferenceChecker checker) const
380 {
381     if (!impl_->outputFiles_.empty())
382     {
383         TestReferenceChecker outputChecker(checker.checkCompound("OutputFiles", "Files"));
384         for (const auto& outfile : impl_->outputFiles_)
385         {
386             TestReferenceChecker fileChecker(outputChecker.checkCompound("File", outfile.option.c_str()));
387             outfile.matcher->checkFile(outfile.path, &fileChecker);
388         }
389     }
390 }
391
392 /********************************************************************
393  * CommandLineTestBase::Impl
394  */
395
396 class CommandLineTestBase::Impl
397 {
398 public:
399     Impl() : helper_(&tempFiles_) { cmdline_.append("module"); }
400
401     TestReferenceData     data_;
402     TestFileManager       tempFiles_;
403     CommandLineTestHelper helper_;
404     CommandLine           cmdline_;
405 };
406
407 /********************************************************************
408  * CommandLineTestBase
409  */
410
411 CommandLineTestBase::CommandLineTestBase() : impl_(new Impl) {}
412
413 CommandLineTestBase::~CommandLineTestBase() {}
414
415 void CommandLineTestBase::setInputFile(const char* option, const char* filename)
416 {
417     impl_->cmdline_.addOption(option, TestFileManager::getInputFilePath(filename));
418 }
419
420 void CommandLineTestBase::setInputFile(const char* option, const std::string& filename)
421 {
422     setInputFile(option, filename.c_str());
423 }
424
425 void CommandLineTestBase::setModifiableInputFile(const char* option, const std::string& filename)
426 {
427     setModifiableInputFile(option, filename.c_str());
428 }
429
430 void CommandLineTestBase::setModifiableInputFile(const char* option, const char* filename)
431 {
432     std::string originalFileName   = gmx::test::TestFileManager::getInputFilePath(filename);
433     std::string modifiableFileName = fileManager().getTemporaryFilePath(filename);
434     gmx_file_copy(originalFileName.c_str(), modifiableFileName.c_str(), true);
435     impl_->cmdline_.addOption(option, modifiableFileName);
436 }
437
438 void CommandLineTestBase::setInputFileContents(const char*        option,
439                                                const char*        extension,
440                                                const std::string& contents)
441 {
442     impl_->helper_.setInputFileContents(&impl_->cmdline_, option, extension, contents);
443 }
444
445 void CommandLineTestBase::setInputFileContents(const char*                        option,
446                                                const char*                        extension,
447                                                const ArrayRef<const char* const>& contents)
448 {
449     impl_->helper_.setInputFileContents(&impl_->cmdline_, option, extension, contents);
450 }
451
452 void CommandLineTestBase::setOutputFile(const char*                      option,
453                                         const char*                      filename,
454                                         const ITextBlockMatcherSettings& matcher)
455 {
456     impl_->helper_.setOutputFile(&impl_->cmdline_, option, filename, matcher);
457 }
458
459 void CommandLineTestBase::setOutputFile(const char*                 option,
460                                         const char*                 filename,
461                                         const IFileMatcherSettings& matcher)
462 {
463     impl_->helper_.setOutputFile(&impl_->cmdline_, option, filename, matcher);
464 }
465
466 void CommandLineTestBase::setInputAndOutputFile(const char*                      option,
467                                                 const char*                      filename,
468                                                 const ITextBlockMatcherSettings& matcher)
469 {
470     std::string originalFileName   = gmx::test::TestFileManager::getInputFilePath(filename);
471     std::string modifiableFileName = fileManager().getTemporaryFilePath(filename);
472     gmx_file_copy(originalFileName.c_str(), modifiableFileName.c_str(), true);
473     impl_->helper_.setOutputFile(&impl_->cmdline_, option, filename, matcher);
474 }
475
476 void CommandLineTestBase::setInputAndOutputFile(const char*                 option,
477                                                 const char*                 filename,
478                                                 const IFileMatcherSettings& matcher)
479 {
480     std::string originalFileName   = gmx::test::TestFileManager::getInputFilePath(filename);
481     std::string modifiableFileName = fileManager().getTemporaryFilePath(filename);
482     gmx_file_copy(originalFileName.c_str(), modifiableFileName.c_str(), true);
483     impl_->helper_.setOutputFile(&impl_->cmdline_, option, filename, matcher);
484 }
485
486 CommandLine& CommandLineTestBase::commandLine()
487 {
488     return impl_->cmdline_;
489 }
490
491 TestFileManager& CommandLineTestBase::fileManager()
492 {
493     return impl_->tempFiles_;
494 }
495
496 TestReferenceChecker CommandLineTestBase::rootChecker()
497 {
498     return impl_->data_.rootChecker();
499 }
500
501 void CommandLineTestBase::setDefaultTolerance(const FloatingPointTolerance& tolerance)
502 {
503     impl_->data_.rootChecker().setDefaultTolerance(tolerance);
504 }
505
506 void CommandLineTestBase::testWriteHelp(ICommandLineModule* module)
507 {
508     StringOutputStream     stream;
509     TextWriter             writer(&stream);
510     CommandLineHelpContext context(&writer, eHelpOutputFormat_Console, nullptr, "test");
511     context.setModuleDisplayName(formatString("%s %s", "test", module->name()));
512     module->writeHelp(context);
513     TestReferenceChecker checker(rootChecker());
514     checker.checkTextBlock(stream.toString(), "HelpOutput");
515 }
516
517 void CommandLineTestBase::checkOutputFiles()
518 {
519     impl_->helper_.checkOutputFiles(rootChecker());
520 }
521
522 } // namespace test
523 } // namespace gmx