C++ Utilities 5.22.0
Useful C++ classes and routines such as argument parser, IO and conversion utilities
Loading...
Searching...
No Matches
testutils.cpp
Go to the documentation of this file.
1#include "./testutils.h"
2
3#include "../conversion/stringbuilder.h"
4#include "../conversion/stringconversion.h"
5#include "../io/ansiescapecodes.h"
6#include "../io/misc.h"
7#include "../io/nativefilestream.h"
8#include "../io/path.h"
9#include "../misc/parseerror.h"
10
11#include <cerrno>
12#include <cstdio>
13#include <cstdlib>
14#include <cstring>
15#include <fstream>
16#include <initializer_list>
17#include <iostream>
18#include <limits>
19
20#ifdef PLATFORM_UNIX
21#ifdef CPP_UTILITIES_USE_STANDARD_FILESYSTEM
22#include <filesystem>
23#endif
24#include <poll.h>
25#include <sys/stat.h>
26#include <sys/wait.h>
27#include <unistd.h>
28#endif
29
30#ifdef PLATFORM_WINDOWS
31#include <windows.h>
32#endif
33
34using namespace std;
35using namespace CppUtilities::EscapeCodes;
36
40namespace CppUtilities {
41
43static bool fileSystemItemExists(const string &path)
44{
45#ifdef PLATFORM_UNIX
46 struct stat res;
47 return stat(path.data(), &res) == 0;
48#else
49 const auto widePath(convertMultiByteToWide(path));
50 if (!widePath.first) {
51 return false;
52 }
53 const auto fileType(GetFileAttributesW(widePath.first.get()));
54 return fileType != INVALID_FILE_ATTRIBUTES;
55#endif
56}
57
58static bool fileExists(const string &path)
59{
60#ifdef PLATFORM_UNIX
61 struct stat res;
62 return stat(path.data(), &res) == 0 && !S_ISDIR(res.st_mode);
63#else
64 const auto widePath(convertMultiByteToWide(path));
65 if (!widePath.first) {
66 return false;
67 }
68 const auto fileType(GetFileAttributesW(widePath.first.get()));
69 return (fileType != INVALID_FILE_ATTRIBUTES) && !(fileType & FILE_ATTRIBUTE_DIRECTORY) && !(fileType & FILE_ATTRIBUTE_DEVICE);
70#endif
71}
72
73static bool dirExists(const string &path)
74{
75#ifdef PLATFORM_UNIX
76 struct stat res;
77 return stat(path.data(), &res) == 0 && S_ISDIR(res.st_mode);
78#else
79 const auto widePath(convertMultiByteToWide(path));
80 if (!widePath.first) {
81 return false;
82 }
83 const auto fileType(GetFileAttributesW(widePath.first.get()));
84 return (fileType != INVALID_FILE_ATTRIBUTES) && (fileType & FILE_ATTRIBUTE_DIRECTORY);
85#endif
86}
87
88static bool makeDir(const string &path)
89{
90#ifdef PLATFORM_UNIX
91 return mkdir(path.data(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) == 0;
92#else
93 const auto widePath(convertMultiByteToWide(path));
94 if (!widePath.first) {
95 return false;
96 }
97 return CreateDirectoryW(widePath.first.get(), nullptr) || GetLastError() == ERROR_ALREADY_EXISTS;
98#endif
99}
101
102TestApplication *TestApplication::s_instance = nullptr;
103
117 : TestApplication(0, nullptr)
118{
119}
120
125TestApplication::TestApplication(int argc, const char *const *argv)
126 : m_listArg("list", 'l', "lists available test units")
127 , m_runArg("run", 'r', "runs the tests")
128 , m_testFilesPathArg("test-files-path", 'p', "specifies the path of the directory with test files", { "path" })
129 , m_applicationPathArg("app-path", 'a', "specifies the path of the application to be tested", { "path" })
130 , m_workingDirArg("working-dir", 'w', "specifies the directory to store working copies of test files", { "path" })
131 , m_unitsArg("units", 'u', "specifies the units to test; omit to test all units", { "unit1", "unit2", "unit3" })
132{
133 // check whether there is already an instance
134 if (s_instance) {
135 throw runtime_error("only one TestApplication instance allowed at a time");
136 }
137 s_instance = this;
138
139 // handle specified arguments (if present)
140 if (argc && argv) {
141 // setup argument parser
142 m_testFilesPathArg.setRequiredValueCount(Argument::varValueCount);
143 m_unitsArg.setRequiredValueCount(Argument::varValueCount);
144 m_runArg.setImplicit(true);
145 m_runArg.setSubArguments({ &m_testFilesPathArg, &m_applicationPathArg, &m_workingDirArg, &m_unitsArg });
146 m_parser.setMainArguments({ &m_runArg, &m_listArg, &m_parser.noColorArg(), &m_parser.helpArg() });
147
148 // parse arguments
149 try {
151 } catch (const ParseError &failure) {
152 cerr << failure;
153 m_valid = false;
154 return;
155 }
156
157 // print help
158 if (m_parser.helpArg().isPresent()) {
159 exit(0);
160 }
161 }
162
163 // set paths for testfiles
164 // -> set paths set via CLI argument
165 if (m_testFilesPathArg.isPresent()) {
166 for (const char *const testFilesPath : m_testFilesPathArg.values()) {
167 if (*testFilesPath) {
168 m_testFilesPaths.emplace_back(argsToString(testFilesPath, '/'));
169 } else {
170 m_testFilesPaths.emplace_back("./");
171 }
172 }
173 }
174 // -> read TEST_FILE_PATH environment variable
175 bool hasTestFilePathFromEnv;
176 if (auto testFilePathFromEnv = readTestfilePathFromEnv(); (hasTestFilePathFromEnv = !testFilePathFromEnv.empty())) {
177 m_testFilesPaths.emplace_back(std::move(testFilePathFromEnv));
178 }
179 // -> find source directory
180 if (auto testFilePathFromSrcDirRef = readTestfilePathFromSrcRef(); !testFilePathFromSrcDirRef.empty()) {
181 m_testFilesPaths.insert(m_testFilesPaths.end(), std::make_move_iterator(testFilePathFromSrcDirRef.begin()),
182 std::make_move_iterator(testFilePathFromSrcDirRef.end()));
183 }
184 // -> try testfiles directory in working directory
185 m_testFilesPaths.emplace_back("./testfiles/");
186 for (const auto &testFilesPath : m_testFilesPaths) {
187 cerr << testFilesPath << '\n';
188 }
189
190 // set path for working-copy
191 if (m_workingDirArg.isPresent()) {
192 if (*m_workingDirArg.values().front()) {
193 (m_workingDir = m_workingDirArg.values().front()) += '/';
194 } else {
195 m_workingDir = "./";
196 }
197 } else if (const char *const workingDirEnv = getenv("WORKING_DIR")) {
198 if (*workingDirEnv) {
199 m_workingDir = argsToString(workingDirEnv, '/');
200 }
201 } else {
202 if ((m_testFilesPathArg.isPresent() && !m_testFilesPathArg.values().empty()) || hasTestFilePathFromEnv) {
203 m_workingDir = m_testFilesPaths.front() + "workingdir/";
204 } else {
205 m_workingDir = "./testfiles/workingdir/";
206 }
207 }
208 cerr << "Directory used to store working copies:\n" << m_workingDir << '\n';
209
210 // clear list of all additional profiling files created when forking the test application
211 if (const char *const profrawListFile = getenv("LLVM_PROFILE_LIST_FILE")) {
212 ofstream(profrawListFile, ios_base::trunc);
213 }
214
215 m_valid = true;
216}
217
222{
223 s_instance = nullptr;
224}
225
238std::string TestApplication::testFilePath(const std::string &relativeTestFilePath) const
239{
240 std::string path;
241 for (const auto &testFilesPath : m_testFilesPaths) {
242 if (fileExists(path = testFilesPath + relativeTestFilePath)) {
243 return path;
244 }
245 }
246 throw std::runtime_error("The test file \"" % relativeTestFilePath % "\" can not be located. Was looking under:\n"
247 + joinStrings(m_testFilesPaths, "\n", false, " - ", relativeTestFilePath));
248}
249
256std::string TestApplication::testDirPath(const std::string &relativeTestDirPath) const
257{
258 std::string path;
259 for (const auto &testFilesPath : m_testFilesPaths) {
260 if (dirExists(path = testFilesPath + relativeTestDirPath)) {
261 return path;
262 }
263 }
264 throw std::runtime_error("The test directory \"" % relativeTestDirPath % "\" can not be located. Was looking under:\n"
265 + joinStrings(m_testFilesPaths, "\n", false, " - ", relativeTestDirPath));
266}
267
275string TestApplication::workingCopyPath(const string &relativeTestFilePath, WorkingCopyMode mode) const
276{
277 return workingCopyPathAs(relativeTestFilePath, relativeTestFilePath, mode);
278}
279
295 const std::string &relativeTestFilePath, const std::string &relativeWorkingCopyPath, WorkingCopyMode mode) const
296{
297 // ensure working directory is present
298 auto workingCopyPath = std::string();
299 if (!dirExists(m_workingDir) && !makeDir(m_workingDir)) {
300 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath << "\": can't create working directory \""
301 << m_workingDir << "\"." << Phrases::EndFlush;
302 return workingCopyPath;
303 }
304
305 // ensure subdirectory exists
306 const auto parts = splitString<vector<string>>(relativeWorkingCopyPath, "/", EmptyPartsTreat::Omit);
307 if (!parts.empty()) {
308 // create subdirectory level by level
309 string currentLevel;
310 currentLevel.reserve(m_workingDir.size() + relativeWorkingCopyPath.size() + 1);
311 currentLevel.assign(m_workingDir);
312 for (auto i = parts.cbegin(), end = parts.end() - 1; i != end; ++i) {
313 if (currentLevel.back() != '/') {
314 currentLevel += '/';
315 }
316 currentLevel += *i;
317
318 // continue if subdirectory level already exists or we can successfully create the directory
319 if (dirExists(currentLevel) || makeDir(currentLevel)) {
320 continue;
321 }
322 // fail otherwise
323 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeWorkingCopyPath << "\": can't create directory \""
324 << currentLevel << "\" (inside working directory)." << Phrases::EndFlush;
325 return workingCopyPath;
326 }
327 }
328
329 workingCopyPath = m_workingDir + relativeWorkingCopyPath;
330 switch (mode) {
332 // just return the path if we don't want to actually create a copy
333 return workingCopyPath;
335 // ensure the file does not exist in cleanup mode
336 if (std::remove(workingCopyPath.data()) != 0 && errno != ENOENT) {
337 const auto error = std::strerror(errno);
338 cerr << Phrases::Error << "Unable to delete \"" << workingCopyPath << "\": " << error << Phrases::EndFlush;
339 workingCopyPath.clear();
340 }
341 return workingCopyPath;
342 default:;
343 }
344
345 // copy the file
346 const auto origFilePath = testFilePath(relativeTestFilePath);
347 size_t workingCopyPathAttempt = 0;
348 NativeFileStream origFile, workingCopy;
349 origFile.open(origFilePath, ios_base::in | ios_base::binary);
350 if (origFile.fail()) {
351 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath
352 << "\": an IO error occurred when opening original file \"" << origFilePath << "\"." << Phrases::EndFlush;
353 cerr << "error: " << std::strerror(errno) << endl;
354 workingCopyPath.clear();
355 return workingCopyPath;
356 }
357 workingCopy.open(workingCopyPath, ios_base::out | ios_base::binary | ios_base::trunc);
358 while (workingCopy.fail() && fileSystemItemExists(workingCopyPath)) {
359 // adjust the working copy path if the target file already exists and can not be truncated
360 workingCopyPath = argsToString(m_workingDir, relativeWorkingCopyPath, '.', ++workingCopyPathAttempt);
361 workingCopy.clear();
362 workingCopy.open(workingCopyPath, ios_base::out | ios_base::binary | ios_base::trunc);
363 }
364 if (workingCopy.fail()) {
365 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath
366 << "\": an IO error occurred when opening target file \"" << workingCopyPath << "\"." << Phrases::EndFlush;
367 cerr << "error: " << strerror(errno) << endl;
368 workingCopyPath.clear();
369 return workingCopyPath;
370 }
371 workingCopy << origFile.rdbuf();
372 workingCopy.close();
373 if (!origFile.fail() && !workingCopy.fail()) {
374 return workingCopyPath;
375 }
376
377 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath << "\": ";
378 if (origFile.fail()) {
379 cerr << "an IO error occurred when reading original file \"" << origFilePath << "\"";
380 workingCopyPath.clear();
381 return workingCopyPath;
382 }
383 if (workingCopy.fail()) {
384 if (origFile.fail()) {
385 cerr << " and ";
386 }
387 cerr << " an IO error occurred when writing to target file \"" << workingCopyPath << "\".";
388 }
389 cerr << "error: " << strerror(errno) << endl;
390 workingCopyPath.clear();
391 return workingCopyPath;
392}
393
394#ifdef PLATFORM_UNIX
399static int execAppInternal(const char *appPath, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout,
400 const std::string &newProfilingPath, bool enableSearchPath = false)
401{
402 // print log message
403 if (!suppressLogging) {
404 // print actual appPath and skip first argument instead
405 cout << '-' << ' ' << appPath;
406 if (*args) {
407 for (const char *const *i = args + 1; *i; ++i) {
408 cout << ' ' << *i;
409 }
410 }
411 cout << endl;
412 }
413
414 // create pipes
415 int coutPipes[2], cerrPipes[2];
416 pipe(coutPipes);
417 pipe(cerrPipes);
418 const auto readCoutPipe = coutPipes[0], writeCoutPipe = coutPipes[1];
419 const auto readCerrPipe = cerrPipes[0], writeCerrPipe = cerrPipes[1];
420
421 // create child process
422 if (const auto child = fork()) {
423 // parent process: read stdout and stderr from child
424 close(writeCoutPipe);
425 close(writeCerrPipe);
426
427 try {
428 if (child == -1) {
429 throw runtime_error("Unable to create fork");
430 }
431
432 // init file descriptor set for poll
433 struct pollfd fileDescriptorSet[2];
434 fileDescriptorSet[0].fd = readCoutPipe;
435 fileDescriptorSet[1].fd = readCerrPipe;
436 fileDescriptorSet[0].events = fileDescriptorSet[1].events = POLLIN;
437
438 // init variables for reading
439 char buffer[512];
440 output.clear();
441 errors.clear();
442
443 // poll as long as at least one pipe is open
444 do {
445 const auto retpoll = poll(fileDescriptorSet, 2, timeout);
446 if (retpoll == 0) {
447 throw runtime_error("Poll time-out");
448 }
449 if (retpoll < 0) {
450 throw runtime_error("Poll failed");
451 }
452 if (fileDescriptorSet[0].revents & POLLIN) {
453 const auto count = read(readCoutPipe, buffer, sizeof(buffer));
454 if (count > 0) {
455 output.append(buffer, static_cast<size_t>(count));
456 }
457 } else if (fileDescriptorSet[0].revents & POLLHUP) {
458 close(readCoutPipe);
459 fileDescriptorSet[0].fd = -1;
460 }
461 if (fileDescriptorSet[1].revents & POLLIN) {
462 const auto count = read(readCerrPipe, buffer, sizeof(buffer));
463 if (count > 0) {
464 errors.append(buffer, static_cast<size_t>(count));
465 }
466 } else if (fileDescriptorSet[1].revents & POLLHUP) {
467 close(readCerrPipe);
468 fileDescriptorSet[1].fd = -1;
469 }
470 } while (fileDescriptorSet[0].fd >= 0 || fileDescriptorSet[1].fd >= 0);
471 } catch (...) {
472 // ensure all pipes are closed in the error case
473 close(readCoutPipe);
474 close(readCerrPipe);
475 throw;
476 }
477
478 // get return code
479 int childReturnCode;
480 waitpid(child, &childReturnCode, 0);
481 return childReturnCode;
482 } else {
483 // child process
484 // -> set pipes to be used for stdout/stderr
485 dup2(writeCoutPipe, STDOUT_FILENO);
486 dup2(writeCerrPipe, STDERR_FILENO);
487 close(readCoutPipe);
488 close(writeCoutPipe);
489 close(readCerrPipe);
490 close(writeCerrPipe);
491
492 // -> modify environment variable LLVM_PROFILE_FILE to apply new path for profiling output
493 if (!newProfilingPath.empty()) {
494 setenv("LLVM_PROFILE_FILE", newProfilingPath.data(), true);
495 }
496
497 // -> execute application
498 if (enableSearchPath) {
499 execvp(appPath, const_cast<char *const *>(args));
500
501 } else {
502 execv(appPath, const_cast<char *const *>(args));
503 }
504 cerr << Phrases::Error << "Unable to execute \"" << appPath << "\": execv() failed" << Phrases::EndFlush;
505 exit(-101);
506 }
507}
508
518int TestApplication::execApp(const char *const *args, string &output, string &errors, bool suppressLogging, int timeout) const
519{
520 // increase counter used for giving profiling files unique names
521 static unsigned int invocationCount = 0;
522 ++invocationCount;
523
524 // determine the path of the application to be tested
525 const char *appPath = m_applicationPathArg.firstValue();
526 auto fallbackAppPath = string();
527 if (!appPath || !*appPath) {
528 // try to find the path by removing "_tests"-suffix from own executable path
529 // (the own executable path is the path of the test application and its name is usually the name of the application
530 // to be tested with "_tests"-suffix)
531 const char *const testAppPath = m_parser.executable();
532 const auto testAppPathLength = strlen(testAppPath);
533 if (testAppPathLength > 6 && !strcmp(testAppPath + testAppPathLength - 6, "_tests")) {
534 fallbackAppPath.assign(testAppPath, testAppPathLength - 6);
535 appPath = fallbackAppPath.data();
536 // TODO: it would not hurt to verify whether "fallbackAppPath" actually exists and is executable
537 } else {
538 throw runtime_error("Unable to execute application to be tested: no application path specified");
539 }
540 }
541
542 // determine new path for profiling output (to not override profiling output of parent and previous invocations)
543 const auto newProfilingPath = [appPath] {
544 auto path = string();
545 const char *const llvmProfileFile = getenv("LLVM_PROFILE_FILE");
546 if (!llvmProfileFile) {
547 return path;
548 }
549 // replace eg. "/some/path/tageditor_tests.profraw" with "/some/path/tageditor0.profraw"
550 const char *const llvmProfileFileEnd = strstr(llvmProfileFile, ".profraw");
551 if (!llvmProfileFileEnd) {
552 return path;
553 }
554 const auto llvmProfileFileWithoutExtension = string(llvmProfileFile, llvmProfileFileEnd);
555 // extract application name from path
556 const char *appName = strrchr(appPath, '/');
557 appName = appName ? appName + 1 : appPath;
558 // concat new path
559 path = argsToString(llvmProfileFileWithoutExtension, '_', appName, invocationCount, ".profraw");
560 // append path to profiling list file
561 if (const char *const profrawListFile = getenv("LLVM_PROFILE_LIST_FILE")) {
562 ofstream(profrawListFile, ios_base::app) << path << endl;
563 }
564 return path;
565 }();
566
567 return execAppInternal(appPath, args, output, errors, suppressLogging, timeout, newProfilingPath);
568}
569
577int execHelperApp(const char *appPath, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout)
578{
579 return execAppInternal(appPath, args, output, errors, suppressLogging, timeout, string());
580}
581
592int execHelperAppInSearchPath(
593 const char *appName, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout)
594{
595 return execAppInternal(appName, args, output, errors, suppressLogging, timeout, string(), true);
596}
597#endif // PLATFORM_UNIX
598
602string TestApplication::readTestfilePathFromEnv()
603{
604 const char *const testFilesPathEnv = getenv("TEST_FILE_PATH");
605 if (!testFilesPathEnv || !*testFilesPathEnv) {
606 return string();
607 }
608 return argsToString(testFilesPathEnv, '/');
609}
610
616std::vector<std::string> TestApplication::readTestfilePathFromSrcRef()
617{
618 // find the path of the current executable on platforms supporting "/proc/self/exe"; otherwise assume the current working directory
619 // is the executable path
620 auto res = std::vector<std::string>();
621 auto binaryPath = std::string();
622#if defined(CPP_UTILITIES_USE_STANDARD_FILESYSTEM) && defined(PLATFORM_UNIX)
623 try {
624 binaryPath = std::filesystem::read_symlink("/proc/self/exe").parent_path();
625 binaryPath += '/';
626 } catch (const std::filesystem::filesystem_error &e) {
627 cerr << Phrases::Warning << "Unable to detect binary path for finding \"srcdirref\": " << e.what() << Phrases::EndFlush;
628 }
629#endif
630 const auto srcdirrefPath = binaryPath + "srcdirref";
631 try {
632 // read "srcdirref" file which should contain the path of the source directory
633 const auto srcDirContent = readFile(srcdirrefPath, 2 * 1024);
634 if (srcDirContent.empty()) {
635 cerr << Phrases::Warning << "The file \"srcdirref\" is empty." << Phrases::EndFlush;
636 return res;
637 }
638
639 // check whether the referenced source directories contain a "testfiles" directory
640 const auto srcPaths = splitStringSimple<std::vector<std::string_view>>(srcDirContent, "\n");
641 for (const auto &srcPath : srcPaths) {
642 auto testfilesPath = argsToString(srcPath, "/testfiles/");
643 if (dirExists(testfilesPath)) {
644 res.emplace_back(std::move(testfilesPath));
645 } else {
646 cerr << Phrases::Warning
647 << "The source directory referenced by the file \"srcdirref\" does not contain a \"testfiles\" directory or does not exist."
648 << Phrases::End << "Referenced source directory: " << testfilesPath << endl;
649 }
650 }
651 return res;
652
653 } catch (const std::ios_base::failure &e) {
654 cerr << Phrases::Warning << "The file \"" << srcdirrefPath << "\" can not be opened: " << e.what() << Phrases::EndFlush;
655 }
656 return res;
657}
658} // namespace CppUtilities
const char * executable() const
Returns the name of the current executable.
static constexpr std::size_t varValueCount
Denotes a variable number of values.
const char * firstValue() const
Returns the first parameter value of the first occurrence of the argument.
The ParseError class is thrown by an ArgumentParser when a parsing error occurs.
Definition: parseerror.h:11
The TestApplication class simplifies writing test applications that require opening test files.
Definition: testutils.h:23
std::string workingCopyPath(const std::string &relativeTestFilePath, WorkingCopyMode mode=WorkingCopyMode::CreateCopy) const
Returns the full path to a working copy of the test file with the specified relativeTestFilePath.
Definition: testutils.cpp:275
std::string testFilePath(const std::string &relativeTestFilePath) const
Returns the full path of the test file with the specified relativeTestFilePath.
Definition: testutils.cpp:238
static const char * appPath()
Returns the application path or an empty string if no application path has been set.
Definition: testutils.h:92
TestApplication()
Constructs a TestApplication instance without further arguments.
Definition: testutils.cpp:116
std::string workingCopyPathAs(const std::string &relativeTestFilePath, const std::string &relativeWorkingCopyPath, WorkingCopyMode mode=WorkingCopyMode::CreateCopy) const
Returns the full path to a working copy of the test file with the specified relativeTestFilePath.
Definition: testutils.cpp:294
std::string testDirPath(const std::string &relativeTestDirPath) const
Returns the full path of the test directory with the specified relativeTestDirPath.
Definition: testutils.cpp:256
~TestApplication()
Destroys the TestApplication.
Definition: testutils.cpp:221
Encapsulates functions for formatted terminal output using ANSI escape codes.
Contains all utilities provides by the c++utilities library.
CPP_UTILITIES_EXPORT std::string readFile(const std::string &path, std::string::size_type maxSize=std::string::npos)
Reads all contents of the specified file in a single call.
Definition: misc.cpp:17
WorkingCopyMode
The WorkingCopyMode enum specifies additional options to influence behavior of TestApplication::worki...
Definition: testutils.h:17
ReturnType joinStrings(const Container &strings, Detail::StringParamForContainer< Container > delimiter=Detail::StringParamForContainer< Container >(), bool omitEmpty=false, Detail::StringParamForContainer< Container > leftClosure=Detail::StringParamForContainer< Container >(), Detail::StringParamForContainer< Container > rightClosure=Detail::StringParamForContainer< Container >())
Joins the given strings using the specified delimiter.
StringType argsToString(Args &&...args)
std::fstream NativeFileStream
STL namespace.
constexpr int i