Visual Servoing Platform  version 3.6.1 under development (2025-02-01)
catchJsonArgumentParser.cpp
1 /*
2  * ViSP, open source Visual Servoing Platform software.
3  * Copyright (C) 2005 - 2024 by Inria. All rights reserved.
4  *
5  * This software is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  * See the file LICENSE.txt at the root directory of this source
10  * distribution for additional information about the GNU GPL.
11  *
12  * For using ViSP with software that can not be combined with the GNU
13  * GPL, please contact Inria about acquiring a ViSP Professional
14  * Edition License.
15  *
16  * See https://visp.inria.fr for more information.
17  *
18  * This software was developed at:
19  * Inria Rennes - Bretagne Atlantique
20  * Campus Universitaire de Beaulieu
21  * 35042 Rennes Cedex
22  * France
23  *
24  * If you have questions regarding the use of this file, please contact
25  * Inria at visp@inria.fr
26  *
27  * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
28  * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
29  *
30  * Description:
31  * Test vpJsonArgumentParser
32  */
33 
40 #include <visp3/core/vpIoTools.h>
41 #include <visp3/io/vpJsonArgumentParser.h>
42 
43 #if defined(VISP_HAVE_NLOHMANN_JSON) && defined(VISP_HAVE_CATCH2)
44 #include VISP_NLOHMANN_JSON(json.hpp)
45 using json = nlohmann::json;
46 
47 #include <catch_amalgamated.hpp>
48 
49 #ifdef ENABLE_VISP_NAMESPACE
50 using namespace VISP_NAMESPACE_NAME;
51 #endif
52 
53 std::pair<int, std::vector<char *>> convertToArgcAndArgv(const std::vector<std::string> &args)
54 {
55  std::vector<char *> argvs;
56  argvs.reserve(args.size());
57  int argc = static_cast<int>(args.size());
58  for (unsigned i = 0; i < args.size(); ++i) {
59  argvs.push_back(const_cast<char *>(args[i].c_str()));
60  }
61  return std::make_pair(argc, argvs);
62 }
63 
64 json loadJson(const std::string &path)
65 {
66  std::ifstream json_file(path);
67  if (!json_file.good()) {
68  throw vpException(vpException::ioError, "Could not open JSON settings file");
69  }
70  json j = json::parse(json_file);
71  json_file.close();
72  return j;
73 }
74 
75 void saveJson(const json &j, const std::string &path)
76 {
77  std::ofstream json_file(path);
78  if (!json_file.good()) {
79  throw vpException(vpException::ioError, "Could not open JSON settings file to write modifications");
80  }
81  json_file << j.dump();
82  json_file.close();
83 }
84 
85 SCENARIO("Parsing arguments from JSON file", "[json]")
86 {
87  // setup test dir
88  // Get the user login name
89 
90  std::string tmp_dir = vpIoTools::makeTempDirectory(vpIoTools::getTempPath() + vpIoTools::path("/") + "visp_test_json_argument_parsing");
91  const std::string jsonPath = tmp_dir + "/" + "arguments.json";
92 
93  const auto modifyJson = [&jsonPath](std::function<void(json &)> modify) -> void {
94  json j = loadJson(jsonPath);
95  modify(j);
96  saveJson(j, jsonPath);
97  };
98 
99  GIVEN("Some specific arguments")
100  {
101  const std::string s = "hello";
102  WHEN("Converting a string to a json rep")
103  {
104  const json js = convertCommandLineArgument<std::string>(s);
105  const json truejs = s;
106  THEN("Conversion is correct")
107  {
108  REQUIRE(js == truejs);
109  }
110  }
111  }
112 
113  GIVEN("Some JSON parameters saved in a file, and some C++ variables")
114  {
115  json j = json {
116  {"a", 2},
117  {"b", 2.0},
118  {"c", "a string"},
119  {"d", true},
120  {"flag", true},
121  {"flagDefaultTrue", true},
122  {"e", {{"a", 5} }
123  }
124  };
125  saveJson(j, jsonPath);
126 
127  int a = 1;
128  double b = 1.0;
129  std::string c = "";
130  bool d = false;
131  int ea = 4;
132  bool flag = false;
133  bool flagInitialValue = flag;
134 
135  bool invertedFlag = true;
136  WHEN("Declaring a parser with all parameters required")
137  {
138  vpJsonArgumentParser parser("A program", "--config", "/");
139  parser.addArgument("a", a, true)
140  .addArgument("b", b, true)
141  .addArgument("c", c, true)
142  .addArgument("d", d, true)
143  .addArgument("e/a", ea, true)
144  .addFlag("flag", flag)
145  .addFlag("flagDefaultTrue", invertedFlag);
146 
147  THEN("Calling the parser without any argument fails")
148  {
149  const int argc = 1;
150  const char *argv[] = {
151  "program"
152  };
153 
154  REQUIRE_THROWS(parser.parse(argc, argv));
155  }
156 
157  THEN("Calling the parser with only the JSON file works")
158  {
159  const int argc = 3;
160  const char *argv[] = {
161  "program",
162  "--config",
163  jsonPath.c_str()
164  };
165  REQUIRE_NOTHROW(parser.parse(argc, argv));
166  REQUIRE(a == j["a"]);
167  REQUIRE(b == j["b"]);
168  REQUIRE(c == j["c"]);
169  REQUIRE(d == j["d"]);
170  REQUIRE(ea == j["e"]["a"]);
171  REQUIRE(flag != flagInitialValue);
172  REQUIRE(invertedFlag != true);
173  }
174  THEN("Calling the parser by specifying the json argument but leaving the file path empty throws an error")
175  {
176  const int argc = 2;
177  const char *argv[] = {
178  "program",
179  "--config",
180  };
181  REQUIRE_THROWS(parser.parse(argc, argv));
182  }
183  THEN("Calling the parser with only the json file but deleting a random field throws an error")
184  {
185  const int argc = 3;
186  for (const auto &jsonElem : j.items()) {
187  if (jsonElem.key().rfind("flag", 0) != 0) {
188  modifyJson([&jsonElem](json &j) { j.erase(jsonElem.key()); });
189  const char *argv[] = {
190  "program",
191  "--config",
192  jsonPath.c_str()
193  };
194  REQUIRE_THROWS(parser.parse(argc, argv));
195  }
196  }
197  }
198  THEN("Calling the parser with only the json file but setting a random field to null throws an error")
199  {
200  const int argc = 3;
201  for (const auto &jsonElem : j.items()) {
202  if (jsonElem.key().rfind("flag", 0) != 0) {
203  modifyJson([&jsonElem](json &j) { j[jsonElem.key()] = nullptr; });
204  const char *argv[] = {
205  "program",
206  "--config",
207  jsonPath.c_str()
208  };
209  REQUIRE_THROWS(parser.parse(argc, argv));
210  }
211  }
212  }
213  THEN("Calling the parser with an invalid json file path throws an error")
214  {
215  const int argc = 3;
216  const char *argv[] = {
217  "program",
218  "--config",
219  "some_invalid_json/file/path.json"
220  };
221  REQUIRE_THROWS(parser.parse(argc, argv));
222  }
223  THEN("Calling the parser with only the command line arguments works")
224  {
225  const int newa = a + 1, newea = ea + 6;
226  const double newb = b + 2.0;
227  const std::string newc = c + "hello";
228  const bool newd = !d;
229 
230  const std::string newdstr(newd ? "true" : "false");
231  std::vector<std::string> args = {
232  "program",
233  "a", std::to_string(newa),
234  "b", std::to_string(newb),
235  "c", newc,
236  "d", newdstr,
237  "e/a", std::to_string(newea),
238  "flag"
239  };
240  int argc;
241  std::vector<char *> argv;
242  std::tie(argc, argv) = convertToArgcAndArgv(args);
243  REQUIRE_NOTHROW(parser.parse(argc, (const char **)(&argv[0])));
244  REQUIRE(a == newa);
245  REQUIRE(b == newb);
246  REQUIRE(c == newc);
247  REQUIRE(d == newd);
248  REQUIRE(ea == newea);
249  REQUIRE(flag != flagInitialValue);
250 
251  }
252  THEN("Calling the parser with JSON and command line argument works")
253  {
254  const int newa = a + 1;
255  const double newb = b + 2.0;
256  std::vector<std::string> args = {
257  "program",
258  "--config", jsonPath,
259  "a", std::to_string(newa),
260  "b", std::to_string(newb),
261  "flagDefaultTrue"
262  };
263  int argc;
264  std::vector<char *> argv;
265  std::tie(argc, argv) = convertToArgcAndArgv(args);
266  REQUIRE_NOTHROW(parser.parse(argc, (const char **)(&argv[0])));
267  REQUIRE(a == newa);
268  REQUIRE(b == newb);
269  REQUIRE(c == j["c"]);
270  REQUIRE(d == j["d"]);
271  REQUIRE(ea == j["e"]["a"]);
272  REQUIRE(invertedFlag == false);
273  }
274  THEN("Calling the parser with a missing argument value throws an error")
275  {
276 
277  std::vector<std::string> args = {
278  "program",
279  "--config", jsonPath,
280  "a"
281  };
282  int argc;
283  std::vector<char *> argv;
284  std::tie(argc, argv) = convertToArgcAndArgv(args);
285  REQUIRE_THROWS(parser.parse(argc, (const char **)(&argv[0])));
286  }
287  }
288  }
289  THEN("Declaring a parser with an undefined nesting delimiter fails")
290  {
291  REQUIRE_THROWS(vpJsonArgumentParser("A program", "--config", ""));
292  }
293  THEN("Declaring a parser with an invalid JSON file argument fails")
294  {
295  REQUIRE_THROWS(vpJsonArgumentParser("A program", "", "/"));
296  }
297 
298  WHEN("Instantiating a parser with some optional fields")
299  {
300  vpJsonArgumentParser parser("A program", "--config", "/");
301  float b = 0.0;
302  parser.addArgument("b", b, false);
303 
304  THEN("Calling the parser without any argument works and does not modify the default value")
305  {
306  float bcopy = b;
307  const int argc = 1;
308  const char *argv[] = {
309  "program"
310  };
311 
312  REQUIRE_NOTHROW(parser.parse(argc, argv));
313  REQUIRE(b == bcopy);
314 
315  }
316  }
317  WHEN("Instantiating a parser with nested parameters")
318  {
319  vpJsonArgumentParser parser("A program", "--config", "/");
320  float b = 0.0;
321  parser.addArgument("b", b, false);
322 
323  THEN("Calling the parser without any argument works and does not modify the default value")
324  {
325  float bcopy = b;
326  const int argc = 1;
327  const char *argv[] = {
328  "program"
329  };
330 
331  REQUIRE_NOTHROW(parser.parse(argc, argv));
332  REQUIRE(b == bcopy);
333 
334  }
335  }
336 
337 
338  WHEN("Instantiating a parser with some documentation")
339  {
340  const std::string programString = "ProgramString";
341  const std::string firstArg = "FirstArgName", firstArgDescription = "FirstArgDescription";
342  const std::string secondArg = "secondArgName", secondArgDescription = "secondArgDescription";
343  vpJsonArgumentParser parser(programString, "--config", "/");
344  std::string a = "DefaultFirstArg", b = "DefaultSecondArg";
345  parser.addArgument(firstArg, a, true, firstArgDescription);
346  parser.addArgument(secondArg, b, false, secondArgDescription);
347  WHEN("Getting the help string")
348  {
349  const std::string help = parser.help();
350  THEN("Output should contain the basic program description")
351  {
352  REQUIRE(help.find(programString) < help.size());
353  }
354  THEN("Output should contain the json argument")
355  {
356  REQUIRE(help.find("--config") < help.size());
357  }
358  THEN("Output should contain the arguments, their description and their default value")
359  {
360  const std::vector<std::string> requireds = { firstArg, secondArg, firstArgDescription, secondArgDescription, a, b };
361  for (const auto &required : requireds) {
362  REQUIRE(help.find(required) < help.size());
363  }
364  }
365  }
366 
367 
368  }
369 }
370 
371 int main(int argc, char *argv[])
372 {
373  Catch::Session session; // There must be exactly one instance
374  session.applyCommandLine(argc, argv);
375 
376  int numFailed = session.run();
377  return numFailed;
378 }
379 
380 #else
381 
382 int main()
383 {
384  return EXIT_SUCCESS;
385 }
386 
387 #endif
error that can be emitted by ViSP classes.
Definition: vpException.h:60
@ ioError
I/O error.
Definition: vpException.h:67
static std::string path(const std::string &pathname)
Definition: vpIoTools.cpp:1005
static std::string getTempPath()
Definition: vpIoTools.cpp:189
static std::string makeTempDirectory(const std::string &dirname)
Definition: vpIoTools.cpp:708
Command line argument parsing with support for JSON files. If a JSON file is supplied,...