Prev | Table of Contents | Next
The FLiT library has a number of utility functionality that may be useful to those writing tests.
FLiT provides two classes for reading and writing CSV files, flit::CsvReader
and flit::CsvWriter
. These classes are used within FLiT to create the test
result files, but are usable by users.
The details can be seen in the FLiT source tree in src/FlitCsv.h
and
src/FlitCsv.cpp
. Here, we give example usage of these classes.
This is the basis of the usage with flit::CsvReader
. This class derives from
std::vector<std::string>
so you can use any function defined on that
interface.
Additionally, this class provides
flit::CsvRow::header()
returning theCsvRow
instance that represents the header row.flit::CsvRow::setHeader()
: sets the header rowflit::CsvRow::operator[](std::string)
: allowing you to index based on the column header name.
Reads in from a stream and returns instances of CsvRow
(which derive from
std::vector<std::string>
). Here is an example of its usage:
std::istringstream input(
"#,Cost,Description\n"
"1,-3,Candy bar\n"
"2,-5,Garage parking\n"
"3,315,Got paid!\n"
);
Flit::CsvReader reader(input);
int balance = 0;
for (flit::CsvRow row; reader >> row;) {
balance += std::stoi(row["Cost"]);
// could also do
// balance += std::stoi(row[1]);
}
assert(balance == 307);
There are other functions not demonstrated in this example:
flit::CsvReader::header()
: returns aCsvRow
containing the header rowflit::CsvReader::stream()
: returns the stream that was passed in and is being used- casting
flit::CsvReader
to abool
evaluates tofalse
if there are no more rows to parse and evaluates totrue
otherwise.
Writes to a stream in CSV format. Here is an example of its usage:
std::ofstream out("output.csv");
flit::CsvWriter writer(out);
std::vector<std::vector<double>> data {
{1.0, 2.0, 3.3},
{2.0, 2.0, 5.2},
{3.0, 2.5, 7.7},
};
writer << "x" << "y" << "z";
writer.new_row(); // start a new row
writer << data[0][0]
<< data[0][1]
<< data[0][2];
writer.new_row(); // start a new row
writer.write_row(data[1]); // write the whole row all at once
writer.write_row(data[2]); // write the whole row all at once
Supported types with operator<<()
are
int
long
long long
unsigned int
unsigned long
unsigned long long
unsigned __int128
float
double
long double
std::string
To implement your own types, you will need to convert to one of these types.
The std::string
type will probably be the most common route.
FLiT provides a way to output debugging information from tests that will only be printed when the --verbose
flag is given to the compiled FLiT tests. This is through a singleton object called flit::info_stream
. This object is an instance of the flit::InfoStream
class that derives from std::ostream
, so any object that can be printed to std::ostream
can be printed with flit::info_stream
. This stream is thread-safe.
Here is an example usage from within a test:
virtual flit::Variant run_impl(const std::vector<T>& ti) override {
auto &info = flit::info_stream;
info << id << ": beginning test\n";
// test implementation ...
info << id << ": ending test\n";
info << id << ": returned value = " << retval << "\n";
return retval;
}
This class has a few more methods of interest:
flit::InfoStream::show()
: turns on the debugging messages. This is the same as calling the tests with the--verbose
flag. Note: each thread has their own local copy offlit::info_stream
, so this would only affect the current thread.flit::InfoStream::hide()
: the opposite ofshow()
flit::InfoStream::flushout()
: flushes the stream
It is common to work with files within tests, so many of these functionalities
are made easier if desired. These are defined in src/fsutil.h
and
src/fsutil.cpp
.
Open files for in and out with error checking. By default, if you open a file
with std::ifstream
and the file does not exist, or you open a file with
std::ofstream
with permission errors, then the default behavior is to
silently ignore those errors. Since that is not too useful to a programmer,
these utility functions open the files with error checking turned on.
std::ifstream in;
flit::ifopen(in, "in-file.txt"); // will throw a std::ios::failure if it fails
std::ofstream out;
flit::ofopen(out, "out-file.txt"); // will throw if it fails
Also, the flit::ofopen()
function sets the precision of the std::ofstream
to a very high number to ensure outputting does not cause floating-point
truncation.
Joins two parts of a path together. Currently, the only supported separator is the Unix one, "/", but if Windows is supported in the future, this function would join path elements properly.
Note, you can pass in as many arguments to this function as you want.
auto full_path = flit::join("/home", "user", "git", "FLiT", "src", "flit", "fsutil.h");
Reads the entire content of the given filename into a std::string
.
std::string contents = flit::readfile(flit::join("data", "setup.txt"));
Lists the contents of a directory into a std::vector<std::string>
.
auto listing = flit::listdir(".");
Prints the contents of a directory to the console.
flit::printdir("."); // prints all files and folders in the current directory
Creates an empty file if it doesn't exist, or update the modification time if
it does exist. Similar to the touch
command-line utility.
flit::touch("empty.txt"); // create a new empty file
Make a directory (optionally with a unix permission mode). Throws an exception on failure (std::ios_base::failure
).
flit::mkdir(flit::join("output", "directory"), 0777);
The above will attempt to make the directory "output/directory" with read, write, and execute permissions for all.
Remove an empty directory. Throws a std::ios_base::failure
exception on
failure (which will happen if the directory is not empty, you do not have
permission to delete it, or it doesn't exist).
flit::rmdir(flit::join("output", "directory"));
Recursively delete everything under a particular directory. This is the same
as rm -rf <directory>
in the console. Throws a std::ios_base::failure
exception upon failure (mostly for permission issues).
using flit::join;
flit::mkdir("output");
flit::mkdir(join("output", "directory"));
flit::touch(join("output", "directory", "file1.txt"));
flit::touch(join("output", "file2.txt"));
flit::rec_rmdir("output"); // delete it all
Simply returns the absolute path to the current working directory.
auto current_path = flit::curdir();
Change the to given directory (i.e. cd
on the command-line)
auto prev_path = flit::curdir();
flit::mkdir("output");
flit::chdir("output");
// do some work in output
flit::chdir(prev_path);
Finds the given filename from a list of paths (given as a colon-separated
list). If the path is not given, then the PATH
environment variable is used.
auto system_echo = flit::which("echo");
auto my_echo = flit::which("echo",
"/home/user/bin:/home/user/sys/bin:/opt/bin");
The first one uses the system PATH
variable for lookup (returning the same
thing as which echo
from the command-line). The second is given a
user-specified path string with colon separated search paths.
If no such file is found, then std::ios_base::failure
is thrown.
Returns the cononical absolute path after resolving symbolic links of the given path. The given path need not exist. For those elements that do exist and are symbolic links, they are resolved.
auto absolute = flit::realpath(os.path.join(".", "output", "..", "output",
"test.txt"));
This will resolve as expected.
Returns the parent directory path of the given path.
auto parent = flit::dirname("a/b/c");
The above will return "a/b"
as the parent.
Returns the last component of a path, which may be a file or a directory.
auto filename = flit::basename("dir/subdir/file.txt");
The above will result in "file.txt"
.
An RAII-style class that creates a temporary directory. When the class goes out of scope (i.e., is deleted), then the directory, along with all of its contents, are deleted.
{
flit::TempDir tmpdir;
flit::mkdir(flit::join(tmpdir.name(), "data"));
std::ofstream out;
flit::ofopen(out, flit::join(tmpdir.name(), "data", "output.txt"));
// output to out
// do some work ...
}
// when we go out of scope, tmpdir and its contents will be deleted
An RAII-style class that creates a temporary file. When the class goes out of scope (i.e., is deleted), then teh file is deleted. Optionally, you can give a directory where to create the temporary file.
{
flit::TempDir tmpdir;
flit::TempFile tmpfile(tmpdir.name());
tmpfile.out << "hello there";
std::cout << "Created " << tmpfile.name << std::endl;
}
An RAII-style class that calls flit::chdir()
in the constructor to the given
directory, and calls flit::chdir()
back to the directory it was in before.
You can also query what the previous directory was.
{
flit::TempDir temporaryDirectory;
flit::PushDir pusher(temporaryDirectory.name());
// now in the temporary directory
// do some work ...
}
// first pusher goes out of scope and we cd out of temporaryDirectory
// then temporaryDirectory goes out of scope and the directory is deleted
An RAII-style class for handling the closing of a FILE*
pointer (created
using the std::fopen()
-family of functions). When this class goes out of scope,
it will close the FILE*
pointer, preventing resource leaks in the case of
exceptions or programmer negligence.
{
flit::FileCloser handler(std::fopen("hello.txt", "w"));
fprintf(handler.file, "Value: %02f\n", 3.14159);
}
Also gives access to the integer fd
file descriptor. This can be useful as
shown in the next section with the FdReplace
class.
An RAII-style class for replacing the file descriptor of one FILE*
handle
with the file descriptor of another FILE*
handle. When this object goes out
of scope and is deleted, then the original file descriptor is restored.
An example usage of this is to capture output that would go to stdout. If you
use std::cout
, then there are ways to redirect that output to a stream buffer
for another stream, such as a std::stringstream
and capture it that way. But
what about code that calls printf()
and other functions that act on the
stdout
or stderr
global pointers? Here is a trick to grab that output.
And it seems most implementations of std::cout
end up feeding into the
stdout
pointer, so capturing it here gets all of it.
{
flit::FileCloser stdout_handler(std::fopen("stdout-captured.txt", "w"));
flit::FdReplace stdout_capturer(stdout, stdout_handler.file);
printf("hello world!\n"); // will print instead to "stdout-captured.txt"
std::cout << "hello world!\n"; // will also be captured
}
// stdout is now restored
An RAII-style class for replacing stream buffers. Suppose you do not want your
function to output to std::cout
during the test, but at runtime, you do. In
your test, you can capture the output using this class, and restore it at the
end of your test.
{
std::ostringstream capturer;
flit::StreamBufReplace replacer(std::cout, capturer);
std::cout << "hello world!\n"; // will be captured by capturer
capturer.str(); // will contain "hello world!\n"
}
// std::cout is now restored
std::cout << "hello again\n"; // will be printed to the screen
Many tests may wish to call subprocesses. There are already utilities such as
the system()
call. These are a little hard to use, especially if you want to
capture the standard output of the child process. Fortunately, FLiT has some
useful helper functions for you.
These utilities are defined in src/subprocess.h
and src/subprocess.cpp
.
Calls a command as though from the command-line and returns a
flit::ProcResult
struct containing the returncode, stdout, and stderr of the
child process.
auto result = flit::call_with_output("curl localhost:8000/data.html");
if (result.ret != 0) {
throw std::logic_failure("curl command failed");
}
flit::info_stream << "curl stdout:\n" << result.out << "\n\n"
<< "curl stderr:\n" << result.err << "\n\n";
This topic is also covered in Writing Test Cases.
It is often the case that you want to test your application as a whole. That
means calling and testing your main()
function directly. Here is an example
to illustrate how to use the functions declared in src/subprocess.h
to run a
main()
function.
#include <flit/flit.h>
#define main my_main
#include "main.cc"
#undef main
FLIT_REGISTER_MAIN(my_main)
template <typename T>
class MainTest : public flit::TestBase<T> {
...
};
REGISTER_TYPE(MainTest);
template<>
flit::Variant MainTest<double> run_impl(const std::vector<double> &ti) {
FLIT_UNUSED(ti);
auto results = flit::call_main(my_main, "myapp", "--input data/file.txt");
std::vector<std::string> vec_results;
if (results.ret != 0) {
throw std::logic_error("my_main() failed in its execution");
}
vec_results.emplace_back(std::move(results.out));
vec_results.emplace_back(std::move(results.err));
return vec_results;
}
More detail is given in
Writing Test Cases.
But, here we will highlight a few points. First, the file containing the user-defined main()
function is in main.cc
, which is included into the test file after the #define main my_main
. That trick renames the name main
to my_main
. It no longer name clashes, our app still has only one main()
, and we can call my_main()
as a normal function now. After including that source file, we call FLIT_REGISTER_MAIN()
on the newly named my_main()
function for later.
In our test, we call flit::call_main()
. In it we give three parameters:
- the function pointer to the
main
-like function. This function pointer needs to be registered withFLIT_REGISTER_MAIN()
. This is so that FLiT can call this function in a child process. - the value of
argv[0]
whenmy_main()
is called (in a child process). Some programs change their behavior when the name of the executable changes (for example,bash
behaves different if the name of the executable issh
). - the rest of the command-line parameters as would be given on the bash prompt.
Afterwards, the standard output and standard error are placed into a vector and
returned. It will be the job of the user's compare()
function that will make
sense of the standard output and standard error when doing the comparison
between two runs.
Similarly, there is a function called call_mpi_main()
that has a very similar
interface to call_main()
. The only difference is that an extra argument is
inserted as the second argument. This extra argument is the command (with
arguments) for the mpirun
executable. Not only can you choose which mpirun
you want to use (or if you want to use srun
or abuse this function to call a
different wrapper than has nothing to do with MPI), but you can specify the
arguments to mpirun
such as the number of processes to
create.
There are many other helper functions that may be useful to test writers. Those are documented here, along with where they are implemented.
Split a string by a delimiter. This is similar to python's split()
function.
auto split_by_space = flit::split("my name is Mike", ' ');
for (auto &item : split_by_space) {
std::cout << "'" << item << "'" << std::endl;
}
This will print
'my'
'name'
'is'
'Mike'
There is an optional parameter of maxsplit
which specifies how many times to
split maximum.
auto split_only_twice = flit::split("flit::CsvReader::operator<<()", ':', 2);
for (auto &item : split_only_twice) {
std::cout << "'" << item << "'" << std::endl;
}
This will print
'flit'
''
'CsvReader::operator<<()'
All three of these functions return new strings so that the original is not modified. This is to prevent unintended consequences in code.
flit::rtrim()
: trims whitespace from the right-hand side of a stringflit::ltrim()
: trims whitespace from the left-hand side of a stringflit::trim()
: trims whitespace from both the left and right
These three are similar to the flit::trim()
functions. These, however, can
strip off any amount of an arbitrary string from the right, left, or both
sides, respectively.
flit::rstrip()
: strip any amount of a given string from the right and returnflit::lstrip()
: strip any amount of a given string from the left and returnflit::strip()
: strip from both sides
auto stripped = flit::rstrip("-*--*- hello -*--*-", "-*-");
This will result in "-*--*- hello "
, since only the right side was stripped.
Computes the L2 norm between two std::vector<T>
instances. The type of T
must implement minus and multiply between themselves, and they must implement
plus with a long double
accumulator. This is intended for floating-point
types, but users can use custom types if they choose.