C++ Utilities 5.21.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()), std::make_move_iterator(testFilePathFromSrcDirRef.end()));
182 }
183 // -> try testfiles directory in working directory
184 m_testFilesPaths.emplace_back("./testfiles/");
185 for (const auto &testFilesPath : m_testFilesPaths) {
186 cerr << testFilesPath << '\n';
187 }
188
189 // set path for working-copy
190 if (m_workingDirArg.isPresent()) {
191 if (*m_workingDirArg.values().front()) {
192 (m_workingDir = m_workingDirArg.values().front()) += '/';
193 } else {
194 m_workingDir = "./";
195 }
196 } else if (const char *const workingDirEnv = getenv("WORKING_DIR")) {
197 if (*workingDirEnv) {
198 m_workingDir = argsToString(workingDirEnv, '/');
199 }
200 } else {
201 if ((m_testFilesPathArg.isPresent() && !m_testFilesPathArg.values().empty()) || hasTestFilePathFromEnv) {
202 m_workingDir = m_testFilesPaths.front() + "workingdir/";
203 } else {
204 m_workingDir = "./testfiles/workingdir/";
205 }
206 }
207 cerr << "Directory used to store working copies:\n" << m_workingDir << '\n';
208
209 // clear list of all additional profiling files created when forking the test application
210 if (const char *const profrawListFile = getenv("LLVM_PROFILE_LIST_FILE")) {
211 ofstream(profrawListFile, ios_base::trunc);
212 }
213
214 m_valid = true;
215}
216
221{
222 s_instance = nullptr;
223}
224
237std::string TestApplication::testFilePath(const std::string &relativeTestFilePath) const
238{
239 std::string path;
240 for (const auto &testFilesPath : m_testFilesPaths) {
241 if (fileExists(path = testFilesPath + relativeTestFilePath)) {
242 return path;
243 }
244 }
245 throw std::runtime_error("The test file \"" % relativeTestFilePath % "\" can not be located. Was looking under:\n"
246 + joinStrings(m_testFilesPaths, "\n", false, " - ", relativeTestFilePath));
247}
248
255std::string TestApplication::testDirPath(const std::string &relativeTestDirPath) const
256{
257 std::string path;
258 for (const auto &testFilesPath : m_testFilesPaths) {
259 if (dirExists(path = testFilesPath + relativeTestDirPath)) {
260 return path;
261 }
262 }
263 throw std::runtime_error("The test directory \"" % relativeTestDirPath % "\" can not be located. Was looking under:\n"
264 + joinStrings(m_testFilesPaths, "\n", false, " - ", relativeTestDirPath));
265}
266
274string TestApplication::workingCopyPath(const string &relativeTestFilePath, WorkingCopyMode mode) const
275{
276 return workingCopyPathAs(relativeTestFilePath, relativeTestFilePath, mode);
277}
278
294 const std::string &relativeTestFilePath, const std::string &relativeWorkingCopyPath, WorkingCopyMode mode) const
295{
296 // ensure working directory is present
297 auto workingCopyPath = std::string();
298 if (!dirExists(m_workingDir) && !makeDir(m_workingDir)) {
299 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath << "\": can't create working directory \""
300 << m_workingDir << "\"." << Phrases::EndFlush;
301 return workingCopyPath;
302 }
303
304 // ensure subdirectory exists
305 const auto parts = splitString<vector<string>>(relativeWorkingCopyPath, "/", EmptyPartsTreat::Omit);
306 if (!parts.empty()) {
307 // create subdirectory level by level
308 string currentLevel;
309 currentLevel.reserve(m_workingDir.size() + relativeWorkingCopyPath.size() + 1);
310 currentLevel.assign(m_workingDir);
311 for (auto i = parts.cbegin(), end = parts.end() - 1; i != end; ++i) {
312 if (currentLevel.back() != '/') {
313 currentLevel += '/';
314 }
315 currentLevel += *i;
316
317 // continue if subdirectory level already exists or we can successfully create the directory
318 if (dirExists(currentLevel) || makeDir(currentLevel)) {
319 continue;
320 }
321 // fail otherwise
322 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeWorkingCopyPath << "\": can't create directory \""
323 << currentLevel << "\" (inside working directory)." << Phrases::EndFlush;
324 return workingCopyPath;
325 }
326 }
327
328 workingCopyPath = m_workingDir + relativeWorkingCopyPath;
329 switch (mode) {
331 // just return the path if we don't want to actually create a copy
332 return workingCopyPath;
334 // ensure the file does not exist in cleanup mode
335 if (std::remove(workingCopyPath.data()) != 0 && errno != ENOENT) {
336 const auto error = std::strerror(errno);
337 cerr << Phrases::Error << "Unable to delete \"" << workingCopyPath << "\": " << error << Phrases::EndFlush;
338 workingCopyPath.clear();
339 }
340 return workingCopyPath;
341 default:;
342 }
343
344 // copy the file
345 const auto origFilePath = testFilePath(relativeTestFilePath);
346 size_t workingCopyPathAttempt = 0;
347 NativeFileStream origFile, workingCopy;
348 origFile.open(origFilePath, ios_base::in | ios_base::binary);
349 if (origFile.fail()) {
350 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath
351 << "\": an IO error occurred when opening original file \"" << origFilePath << "\"." << Phrases::EndFlush;
352 cerr << "error: " << std::strerror(errno) << endl;
353 workingCopyPath.clear();
354 return workingCopyPath;
355 }
356 workingCopy.open(workingCopyPath, ios_base::out | ios_base::binary | ios_base::trunc);
357 while (workingCopy.fail() && fileSystemItemExists(workingCopyPath)) {
358 // adjust the working copy path if the target file already exists and can not be truncated
359 workingCopyPath = argsToString(m_workingDir, relativeWorkingCopyPath, '.', ++workingCopyPathAttempt);
360 workingCopy.clear();
361 workingCopy.open(workingCopyPath, ios_base::out | ios_base::binary | ios_base::trunc);
362 }
363 if (workingCopy.fail()) {
364 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath
365 << "\": an IO error occurred when opening target file \"" << workingCopyPath << "\"." << Phrases::EndFlush;
366 cerr << "error: " << strerror(errno) << endl;
367 workingCopyPath.clear();
368 return workingCopyPath;
369 }
370 workingCopy << origFile.rdbuf();
371 workingCopy.close();
372 if (!origFile.fail() && !workingCopy.fail()) {
373 return workingCopyPath;
374 }
375
376 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath << "\": ";
377 if (origFile.fail()) {
378 cerr << "an IO error occurred when reading original file \"" << origFilePath << "\"";
379 workingCopyPath.clear();
380 return workingCopyPath;
381 }
382 if (workingCopy.fail()) {
383 if (origFile.fail()) {
384 cerr << " and ";
385 }
386 cerr << " an IO error occurred when writing to target file \"" << workingCopyPath << "\".";
387 }
388 cerr << "error: " << strerror(errno) << endl;
389 workingCopyPath.clear();
390 return workingCopyPath;
391}
392
393#ifdef PLATFORM_UNIX
398static int execAppInternal(const char *appPath, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout,
399 const std::string &newProfilingPath, bool enableSearchPath = false)
400{
401 // print log message
402 if (!suppressLogging) {
403 // print actual appPath and skip first argument instead
404 cout << '-' << ' ' << appPath;
405 if (*args) {
406 for (const char *const *i = args + 1; *i; ++i) {
407 cout << ' ' << *i;
408 }
409 }
410 cout << endl;
411 }
412
413 // create pipes
414 int coutPipes[2], cerrPipes[2];
415 pipe(coutPipes);
416 pipe(cerrPipes);
417 const auto readCoutPipe = coutPipes[0], writeCoutPipe = coutPipes[1];
418 const auto readCerrPipe = cerrPipes[0], writeCerrPipe = cerrPipes[1];
419
420 // create child process
421 if (const auto child = fork()) {
422 // parent process: read stdout and stderr from child
423 close(writeCoutPipe);
424 close(writeCerrPipe);
425
426 try {
427 if (child == -1) {
428 throw runtime_error("Unable to create fork");
429 }
430
431 // init file descriptor set for poll
432 struct pollfd fileDescriptorSet[2];
433 fileDescriptorSet[0].fd = readCoutPipe;
434 fileDescriptorSet[1].fd = readCerrPipe;
435 fileDescriptorSet[0].events = fileDescriptorSet[1].events = POLLIN;
436
437 // init variables for reading
438 char buffer[512];
439 output.clear();
440 errors.clear();
441
442 // poll as long as at least one pipe is open
443 do {
444 const auto retpoll = poll(fileDescriptorSet, 2, timeout);
445 if (retpoll == 0) {
446 throw runtime_error("Poll time-out");
447 }
448 if (retpoll < 0) {
449 throw runtime_error("Poll failed");
450 }
451 if (fileDescriptorSet[0].revents & POLLIN) {
452 const auto count = read(readCoutPipe, buffer, sizeof(buffer));
453 if (count > 0) {
454 output.append(buffer, static_cast<size_t>(count));
455 }
456 } else if (fileDescriptorSet[0].revents & POLLHUP) {
457 close(readCoutPipe);
458 fileDescriptorSet[0].fd = -1;
459 }
460 if (fileDescriptorSet[1].revents & POLLIN) {
461 const auto count = read(readCerrPipe, buffer, sizeof(buffer));
462 if (count > 0) {
463 errors.append(buffer, static_cast<size_t>(count));
464 }
465 } else if (fileDescriptorSet[1].revents & POLLHUP) {
466 close(readCerrPipe);
467 fileDescriptorSet[1].fd = -1;
468 }
469 } while (fileDescriptorSet[0].fd >= 0 || fileDescriptorSet[1].fd >= 0);
470 } catch (...) {
471 // ensure all pipes are closed in the error case
472 close(readCoutPipe);
473 close(readCerrPipe);
474 throw;
475 }
476
477 // get return code
478 int childReturnCode;
479 waitpid(child, &childReturnCode, 0);
480 return childReturnCode;
481 } else {
482 // child process
483 // -> set pipes to be used for stdout/stderr
484 dup2(writeCoutPipe, STDOUT_FILENO);
485 dup2(writeCerrPipe, STDERR_FILENO);
486 close(readCoutPipe);
487 close(writeCoutPipe);
488 close(readCerrPipe);
489 close(writeCerrPipe);
490
491 // -> modify environment variable LLVM_PROFILE_FILE to apply new path for profiling output
492 if (!newProfilingPath.empty()) {
493 setenv("LLVM_PROFILE_FILE", newProfilingPath.data(), true);
494 }
495
496 // -> execute application
497 if (enableSearchPath) {
498 execvp(appPath, const_cast<char *const *>(args));
499
500 } else {
501 execv(appPath, const_cast<char *const *>(args));
502 }
503 cerr << Phrases::Error << "Unable to execute \"" << appPath << "\": execv() failed" << Phrases::EndFlush;
504 exit(-101);
505 }
506}
507
517int TestApplication::execApp(const char *const *args, string &output, string &errors, bool suppressLogging, int timeout) const
518{
519 // increase counter used for giving profiling files unique names
520 static unsigned int invocationCount = 0;
521 ++invocationCount;
522
523 // determine the path of the application to be tested
524 const char *appPath = m_applicationPathArg.firstValue();
525 auto fallbackAppPath = string();
526 if (!appPath || !*appPath) {
527 // try to find the path by removing "_tests"-suffix from own executable path
528 // (the own executable path is the path of the test application and its name is usually the name of the application
529 // to be tested with "_tests"-suffix)
530 const char *const testAppPath = m_parser.executable();
531 const auto testAppPathLength = strlen(testAppPath);
532 if (testAppPathLength > 6 && !strcmp(testAppPath + testAppPathLength - 6, "_tests")) {
533 fallbackAppPath.assign(testAppPath, testAppPathLength - 6);
534 appPath = fallbackAppPath.data();
535 // TODO: it would not hurt to verify whether "fallbackAppPath" actually exists and is executable
536 } else {
537 throw runtime_error("Unable to execute application to be tested: no application path specified");
538 }
539 }
540
541 // determine new path for profiling output (to not override profiling output of parent and previous invocations)
542 const auto newProfilingPath = [appPath] {
543 auto path = string();
544 const char *const llvmProfileFile = getenv("LLVM_PROFILE_FILE");
545 if (!llvmProfileFile) {
546 return path;
547 }
548 // replace eg. "/some/path/tageditor_tests.profraw" with "/some/path/tageditor0.profraw"
549 const char *const llvmProfileFileEnd = strstr(llvmProfileFile, ".profraw");
550 if (!llvmProfileFileEnd) {
551 return path;
552 }
553 const auto llvmProfileFileWithoutExtension = string(llvmProfileFile, llvmProfileFileEnd);
554 // extract application name from path
555 const char *appName = strrchr(appPath, '/');
556 appName = appName ? appName + 1 : appPath;
557 // concat new path
558 path = argsToString(llvmProfileFileWithoutExtension, '_', appName, invocationCount, ".profraw");
559 // append path to profiling list file
560 if (const char *const profrawListFile = getenv("LLVM_PROFILE_LIST_FILE")) {
561 ofstream(profrawListFile, ios_base::app) << path << endl;
562 }
563 return path;
564 }();
565
566 return execAppInternal(appPath, args, output, errors, suppressLogging, timeout, newProfilingPath);
567}
568
576int execHelperApp(const char *appPath, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout)
577{
578 return execAppInternal(appPath, args, output, errors, suppressLogging, timeout, string());
579}
580
591int execHelperAppInSearchPath(
592 const char *appName, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout)
593{
594 return execAppInternal(appName, args, output, errors, suppressLogging, timeout, string(), true);
595}
596#endif // PLATFORM_UNIX
597
601string TestApplication::readTestfilePathFromEnv()
602{
603 const char *const testFilesPathEnv = getenv("TEST_FILE_PATH");
604 if (!testFilesPathEnv || !*testFilesPathEnv) {
605 return string();
606 }
607 return argsToString(testFilesPathEnv, '/');
608}
609
615std::vector<std::string> TestApplication::readTestfilePathFromSrcRef()
616{
617 // find the path of the current executable on platforms supporting "/proc/self/exe"; otherwise assume the current working directory
618 // is the executable path
619 auto res = std::vector<std::string>();
620 auto binaryPath = std::string();
621#if defined(CPP_UTILITIES_USE_STANDARD_FILESYSTEM) && defined(PLATFORM_UNIX)
622 try {
623 binaryPath = std::filesystem::read_symlink("/proc/self/exe").parent_path();
624 binaryPath += '/';
625 } catch (const std::filesystem::filesystem_error &e) {
626 cerr << Phrases::Warning << "Unable to detect binary path for finding \"srcdirref\": " << e.what() << Phrases::EndFlush;
627 }
628#endif
629 const auto srcdirrefPath = binaryPath + "srcdirref";
630 try {
631 // read "srcdirref" file which should contain the path of the source directory
632 const auto srcDirContent = readFile(srcdirrefPath, 2 * 1024);
633 if (srcDirContent.empty()) {
634 cerr << Phrases::Warning << "The file \"srcdirref\" is empty." << Phrases::EndFlush;
635 return res;
636 }
637
638 // check whether the referenced source directories contain a "testfiles" directory
639 const auto srcPaths = splitStringSimple<std::vector<std::string_view>>(srcDirContent, "\n");
640 for (const auto &srcPath : srcPaths) {
641 auto testfilesPath = argsToString(srcPath, "/testfiles/");
642 if (dirExists(testfilesPath)) {
643 res.emplace_back(std::move(testfilesPath));
644 } else {
645 cerr << Phrases::Warning
646 << "The source directory referenced by the file \"srcdirref\" does not contain a \"testfiles\" directory or does not exist."
647 << Phrases::End << "Referenced source directory: " << testfilesPath << endl;
648 }
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:274
std::string testFilePath(const std::string &relativeTestFilePath) const
Returns the full path of the test file with the specified relativeTestFilePath.
Definition: testutils.cpp:237
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:293
std::string testDirPath(const std::string &relativeTestDirPath) const
Returns the full path of the test directory with the specified relativeTestDirPath.
Definition: testutils.cpp:255
~TestApplication()
Destroys the TestApplication.
Definition: testutils.cpp:220
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