OGS
TestDefinition.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: Copyright (c) OpenGeoSys Community (opengeosys.org)
2// SPDX-License-Identifier: BSD-3-Clause
3
4#include "TestDefinition.h"
5
6#include <spdlog/fmt/fmt.h>
7
8#include <algorithm>
9#include <cmath>
10#include <cstdlib>
11#include <filesystem>
12#include <iomanip>
13#include <regex>
14#include <sstream>
15#include <string>
16#include <vector>
17
18#include "BaseLib/ConfigTree.h"
19#include "BaseLib/Error.h"
20#include "BaseLib/FileTools.h"
21#include "BaseLib/MPI.h"
22
23#ifdef USE_PETSC
24#include "MeshLib/IO/VtkIO/VtuInterface.h" // For petsc file name conversion.
25#endif
26
27namespace
28{
30bool isConvertibleToDouble(std::string const& s)
31{
32 std::size_t pos = 0;
33 double value;
34 try
35 {
36 value = std::stod(s, &pos);
37 }
38 catch (...)
39 {
40 ERR("The given string '{:s}' is not convertible to double.", s);
41 return false;
42 }
43 if (pos != s.size())
44 {
45 ERR("Only {:d} characters were used for double conversion of string "
46 "'{:s}'",
47 pos, s);
48 return false;
49 }
50
51 if (std::isnan(value))
52 {
53 ERR("The given string '{:s}' results in a NaN value.", s);
54 return false;
55 }
56 return true;
57}
58
60std::string safeString(std::string const& s)
61{
62 std::stringstream ss;
63 ss << std::quoted(s);
64 return ss.str();
65}
66
69std::string findDiffTool(std::string const& executable_name,
70 std::string const& environment_variable_name)
71{
72 // Try to read the environment variable.
73 if (const char* diff_tool_exe_environment_variable =
74 std::getenv(environment_variable_name.c_str()))
75 {
76 std::string const diff_tool_exe{diff_tool_exe_environment_variable};
77 DBUG("{:s} set to {:s}.", environment_variable_name, diff_tool_exe);
78
79 //
80 // Sanity checks.
81 //
82 { // Check the base name.
83 auto const& base_name =
85 if (base_name != executable_name)
86 {
88 "The {:s} environment variable does not point to '{:s}'. "
89 "{:s}='{:s}'",
90 environment_variable_name, executable_name,
91 environment_variable_name, diff_tool_exe);
92 }
93 }
94 { // Diff tool must exist.
95 if (!BaseLib::IsFileExisting(diff_tool_exe))
96 {
97 OGS_FATAL("The {:s} points to a non-existing file. {:s}='{:s}'",
98 environment_variable_name, environment_variable_name,
99 diff_tool_exe);
100 }
101 }
102
103 //
104 // Test the actual call.
105 //
106 int const return_value =
107 // TODO (naumov) replace system call with output consuming call
108 // (fork + execl seems to be more safe), and extract the vtkdiff
109 // call to common function. Also properly escape all strings in
110 // command lines.
111 // Reference for POSIX and Windows:
112 // https://wiki.sei.cmu.edu/confluence/pages/viewpage.action?pageId=87152177
113 // Take care when using fork, which might copy resources.
114 std::system((diff_tool_exe + " --version").c_str());
115 if (return_value == 0)
116 {
117 return diff_tool_exe;
118 }
119 WARN(
120 "Calling {:s} from the {:s} environment variable didn't work as "
121 "expected. Return value was {:d}.",
122 diff_tool_exe, environment_variable_name, return_value);
123 }
124
125 std::vector<std::string> const paths = {"", "bin"};
126 auto const path =
127 find_if(begin(paths), end(paths),
128 [&executable_name](std::string const& path)
129 {
130 int const return_value =
131 // TODO (naumov) replace system call with output
132 // consuming call as in an above todo comment.
133 std::system((BaseLib::joinPaths(path, executable_name) +
134 " --version")
135 .c_str());
136 return return_value == 0;
137 });
138 if (path == end(paths))
139 {
140 OGS_FATAL("{:s} not found.", executable_name);
141 }
142 return BaseLib::joinPaths(*path, executable_name);
143}
144
145std::string formatPathForDiffTool(std::string const& filename)
146{
147#if _WIN32
148 // VTK does not handle Windows long paths:
149 // https://gitlab.kitware.com/vtk/vtk/-/blob/master/Utilities/KWSys/vtksys/SystemTools.cxx#L1519-1521
150 // Workaround is to make the path absolute and prefix with a special
151 // marker and put everything in quotes, see
152 // https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=powershell
153 auto const& long_path_indicator = R"(\\?\)";
154
155 auto const absolute_filename = std::filesystem::absolute(filename).string();
156 return fmt::format("\"{}{}\"", long_path_indicator, absolute_filename);
157#else
158 return safeString(filename);
159#endif
160}
161
162void checkTolerance(std::string const& tolerance_name,
163 std::string const& tolerance)
164{
165 if (!tolerance.empty() && !isConvertibleToDouble(tolerance))
166 {
167 OGS_FATAL(
168 "The {:s} tolerance value '{:s}' is not convertible to double.",
169 tolerance_name, tolerance);
170 }
171}
172
174using FilePairs = std::vector<std::pair<std::string, std::string>>;
175
177FilePairs findFilesMatchingRegex(std::string const& regex_string,
178 std::string const& reference_path)
179{
180 DBUG("regex is '{}'.", regex_string);
181 FilePairs filenames;
182 auto const regex = std::regex(regex_string);
183 for (auto const& p : std::filesystem::directory_iterator(
184 std::filesystem::path(reference_path)))
185 {
186 auto const filename = p.path().filename().string();
187 if (std::regex_match(filename, regex))
188 {
189 DBUG(" -> matched '{}'", filename);
190 filenames.emplace_back(filename, filename);
191 }
192 }
193 return filenames;
194}
195
196} // namespace
197
198namespace ApplicationsLib
199{
201 std::string const& reference_path,
202 std::string const& output_directory)
203{
204 if (reference_path.empty())
205 {
206 OGS_FATAL(
207 "Reference path containing expected result files can not be "
208 "empty.");
209 }
210
211 // Construct command lines for each entry.
213 auto const& vtkdiff_configs = config_tree.getConfigSubtreeList("vtkdiff");
215 auto const& xdmfdiff_configs = config_tree.getConfigSubtreeList("xdmfdiff");
216 _command_lines.reserve(vtkdiff_configs.size() + xdmfdiff_configs.size());
217
218 // Constructs one command line per file name pair from the entries common
219 // to all diff tools and appends it together with the corresponding output
220 // file.
221 auto const append_tests =
222 [&](std::string const& tool, std::string const& tool_path,
223 std::size_t const number_of_configs, FilePairs const& filenames,
224 std::string const& field_name,
225 std::string const& reference_field_name,
226 std::string const& absolute_tolerance,
227 std::string const& relative_tolerance,
228 std::string const& extra_options)
229 {
230 if (filenames.empty())
231 {
232 OGS_FATAL(
233 "No files from test definitions were added for tests but {} "
234 "{:s} specified.",
235 number_of_configs,
236 (number_of_configs == 1 ? "test was" : "tests were"));
237 }
238 checkTolerance("absolute", absolute_tolerance);
239 checkTolerance("relative", relative_tolerance);
240
241 for (auto const& [reference_file, output_file] : filenames)
242 {
243 auto const output_filename =
244 BaseLib::joinPaths(output_directory, output_file);
245 _output_files.push_back(output_filename);
246
247 std::string command_line = fmt::format(
248 "{} -a {} -b {} {} {} --abs {} --rel {}{}", tool_path,
249 safeString(reference_field_name), safeString(field_name),
250 formatPathForDiffTool(
251 BaseLib::joinPaths(reference_path, reference_file)),
252 formatPathForDiffTool(output_filename), absolute_tolerance,
253 relative_tolerance, extra_options);
254 INFO("Will run '{:s}'", command_line);
255 _command_lines.push_back({tool, std::move(command_line)});
256 }
257 };
258
259 std::string const vtkdiff =
260 vtkdiff_configs.empty() ? "" : findDiffTool("vtkdiff", "VTKDIFF_EXE");
261 for (auto const& vtkdiff_config : vtkdiff_configs)
262 {
263 std::string const& field_name =
265 vtkdiff_config.getConfigParameter<std::string>("field");
266 DBUG("vtkdiff will compare field '{:s}'.", field_name);
267 std::string const reference_field_name =
268 vtkdiff_config
270 .getConfigParameterOptional<std::string>("reference_field")
271 .value_or(field_name);
272
273 FilePairs filenames;
274 if (auto const regex_string =
276 vtkdiff_config.getConfigParameterOptional<std::string>("regex"))
277 {
278 // TODO: insert rank into regex for mpi case
279 filenames = findFilesMatchingRegex(*regex_string, reference_path);
280 }
281 else
282 {
283 std::string filename =
285 vtkdiff_config.getConfigParameter<std::string>("file");
286 std::string reference_filename =
287 vtkdiff_config
289 .getConfigParameterOptional<std::string>("reference_file")
290 .value_or(filename);
291#ifdef USE_PETSC
293 if (mpi.size > 1)
294 {
295 filename =
297 filename) +
298 "_" + std::to_string(mpi.rank) + ".vtu";
299 reference_filename =
301 reference_filename) +
302 "_" + std::to_string(mpi.rank) + ".vtu";
303 }
304#endif // OGS_USE_PETSC
305 filenames.emplace_back(reference_filename, filename);
306 }
307
308 auto const absolute_tolerance =
310 vtkdiff_config.getConfigParameter<std::string>("absolute_tolerance",
311 "");
312 auto const relative_tolerance =
314 vtkdiff_config.getConfigParameter<std::string>("relative_tolerance",
315 "");
316
317 append_tests("vtkdiff", vtkdiff, vtkdiff_configs.size(), filenames,
318 field_name, reference_field_name, absolute_tolerance,
319 relative_tolerance, "");
320 }
321
322 std::string const xdmfdiff = xdmfdiff_configs.empty()
323 ? ""
324 : findDiffTool("xdmfdiff", "XDMFDIFF_EXE");
325 for (auto const& xdmfdiff_config : xdmfdiff_configs)
326 {
327 std::string const& field_name =
329 xdmfdiff_config.getConfigParameter<std::string>("field");
330 DBUG("xdmfdiff will compare field '{:s}'.", field_name);
331 std::string const reference_field_name =
332 xdmfdiff_config
334 .getConfigParameterOptional<std::string>("reference_field")
335 .value_or(field_name);
336
337 FilePairs filenames;
338 if (auto const regex_string =
340 xdmfdiff_config.getConfigParameterOptional<std::string>("regex"))
341 {
342 filenames = findFilesMatchingRegex(*regex_string, reference_path);
343 }
344 else
345 {
346 std::string const filename =
348 xdmfdiff_config.getConfigParameter<std::string>("file");
349 std::string const reference_filename =
350 xdmfdiff_config
352 .getConfigParameterOptional<std::string>("reference_file")
353 .value_or(filename);
354 filenames.emplace_back(reference_filename, filename);
355 }
356
357 auto const absolute_tolerance =
359 xdmfdiff_config.getConfigParameter<std::string>(
360 "absolute_tolerance", "");
361 auto const relative_tolerance =
363 xdmfdiff_config.getConfigParameter<std::string>(
364 "relative_tolerance", "");
365
366 std::string const timestep =
368 xdmfdiff_config.getConfigParameter<std::string>("timestep");
369 std::string const reference_timestep =
370 xdmfdiff_config
372 .getConfigParameterOptional<std::string>("reference_timestep")
373 .value_or(timestep);
374
375 append_tests("xdmfdiff", xdmfdiff, xdmfdiff_configs.size(), filenames,
376 field_name, reference_field_name, absolute_tolerance,
377 relative_tolerance,
378 fmt::format(" --timestep-a {} --timestep-b {}",
379 reference_timestep, timestep));
380 }
381}
382
384{
385 return runCommandLines(_command_lines, true);
386}
387
388bool TestDefinition::runTests(std::string_view const diff_tool_name) const
389{
390 std::vector<CommandLine> command_lines;
391 copy_if(begin(_command_lines), end(_command_lines),
392 back_inserter(command_lines),
393 [diff_tool_name](CommandLine const& command_line)
394 { return command_line.diff_tool_name == diff_tool_name; });
395
396 return runCommandLines(command_lines, true);
397}
398
400 std::string_view const diff_tool_name) const
401{
402 std::vector<CommandLine> command_lines;
403 copy_if(begin(_command_lines), end(_command_lines),
404 back_inserter(command_lines),
405 [diff_tool_name](CommandLine const& command_line)
406 { return command_line.diff_tool_name != diff_tool_name; });
407
408 return runCommandLines(command_lines, false);
409}
410
411bool TestDefinition::hasTests(std::string_view const diff_tool_name) const
412{
413 return any_of(begin(_command_lines), end(_command_lines),
414 [diff_tool_name](CommandLine const& command_line)
415 { return command_line.diff_tool_name == diff_tool_name; });
416}
417
419 std::vector<CommandLine> const& command_lines, bool const fail_on_empty)
420{
421 std::vector<int> return_values;
422 transform(begin(command_lines), end(command_lines),
423 back_inserter(return_values),
424 [](CommandLine const& command_line)
425 {
426 INFO("---------- {:s} begin ----------",
427 command_line.diff_tool_name);
428 int const return_value =
429 std::system(command_line.command_line.c_str());
430 if (return_value != 0)
431 {
432 WARN("Value {:d} was returned by '{:s}'.", return_value,
433 command_line.command_line);
434 }
435 INFO("---------- {:s} end ----------\n",
436 command_line.diff_tool_name);
437 return return_value;
438 });
439 return (!return_values.empty() || !fail_on_empty) &&
440 all_of(begin(return_values), end(return_values),
441 [](int const return_value) { return return_value == 0; });
442}
443
444std::vector<std::string> const& TestDefinition::getOutputFiles() const
445{
446 return _output_files;
447}
448
450{
451 return size(_command_lines);
452}
453} // namespace ApplicationsLib
#define OGS_FATAL(...)
Definition Error.h:10
void INFO(fmt::format_string< Args... > fmt, Args &&... args)
Definition Logging.h:28
void DBUG(fmt::format_string< Args... > fmt, Args &&... args)
Definition Logging.h:22
void ERR(fmt::format_string< Args... > fmt, Args &&... args)
Definition Logging.h:40
void WARN(fmt::format_string< Args... > fmt, Args &&... args)
Definition Logging.h:34
OGS_EXPORT_SYMBOL bool runTests() const
Runs all configured test command lines.
std::vector< std::string > _output_files
static bool runCommandLines(std::vector< CommandLine > const &command_lines, bool fail_on_empty)
std::vector< CommandLine > _command_lines
TestDefinition(BaseLib::ConfigTree const &config_tree, std::string const &reference_path, std::string const &output_directory)
std::size_t numberOfTests() const
Returns the number of configured test command lines.
OGS_EXPORT_SYMBOL bool runTestsExcluding(std::string_view diff_tool_name) const
Runs all test command lines except those for the given diff tool.
std::vector< std::string > const & getOutputFiles() const
Returns all output files referenced by the configured test definitions.
OGS_EXPORT_SYMBOL bool hasTests(std::string_view diff_tool_name) const
Returns true if at least one test command line uses the given diff tool.
Range< SubtreeIterator > getConfigSubtreeList(std::string const &root) const
bool IsFileExisting(const std::string &strFilename)
Returns true if given file exists.
Definition FileTools.cpp:23
std::string extractBaseNameWithoutExtension(std::string const &pathname)
std::string joinPaths(std::string const &pathA, std::string const &pathB)
std::string getVtuFileNameForPetscOutputWithoutExtension(std::string const &file_name)
bool isConvertibleToDouble(std::string const &s)
Test if the given string is convertible to a valid double value, not a NaN.
std::string findDiffTool(std::string const &executable_name, std::string const &environment_variable_name)
std::string formatPathForDiffTool(std::string const &filename)
void checkTolerance(std::string const &tolerance_name, std::string const &tolerance)
std::vector< std::pair< std::string, std::string > > FilePairs
Pairs of reference and output file names to be compared.
std::string safeString(std::string const &s)
Wraps a string into double ticks.
FilePairs findFilesMatchingRegex(std::string const &regex_string, std::string const &reference_path)
Collects all file names in reference_path matching the regex.