diff options
Diffstat (limited to 'ept/utils')
-rw-r--r-- | ept/utils/string.cc | 437 | ||||
-rw-r--r-- | ept/utils/string.h | 301 | ||||
-rw-r--r-- | ept/utils/sys.cc | 786 | ||||
-rw-r--r-- | ept/utils/sys.h | 468 | ||||
-rw-r--r-- | ept/utils/tests-main.cc | 139 | ||||
-rw-r--r-- | ept/utils/tests.cc | 578 | ||||
-rw-r--r-- | ept/utils/tests.h | 798 |
7 files changed, 3507 insertions, 0 deletions
diff --git a/ept/utils/string.cc b/ept/utils/string.cc new file mode 100644 index 0000000..95d24fd --- /dev/null +++ b/ept/utils/string.cc @@ -0,0 +1,437 @@ +#include "string.h" +#include <vector> + +using namespace std; + +namespace ept { +namespace str { + +std::string basename(const std::string& pathname) +{ + size_t pos = pathname.rfind("/"); + if (pos == std::string::npos) + return pathname; + else + return pathname.substr(pos+1); +} + +std::string dirname(const std::string& pathname) +{ + if (pathname.empty()) return "."; + + // Skip trailing separators + size_t end = pathname.size(); + while (end > 0 && pathname[end - 1] == '/') + --end; + + // If the result is empty again, then the string was only / characters + if (!end) return "/"; + + // Find the previous separator + end = pathname.rfind("/", end - 1); + + if (end == std::string::npos) + // No previous separator found, everything should be chopped + return std::string("."); + else + { + while (end > 0 && pathname[end - 1] == '/') + --end; + if (!end) return "/"; + return pathname.substr(0, end); + } +} + +void appendpath(std::string& dest, const char* path2) +{ + if (!*path2) + return; + + if (dest.empty()) + { + dest = path2; + return; + } + + if (dest[dest.size() - 1] == '/') + if (path2[0] == '/') + dest += (path2 + 1); + else + dest += path2; + else + if (path2[0] == '/') + dest += path2; + else + { + dest += '/'; + dest += path2; + } +} + +void appendpath(std::string& dest, const std::string& path2) +{ + if (path2.empty()) + return; + + if (dest.empty()) + { + dest = path2; + return; + } + + if (dest[dest.size() - 1] == '/') + if (path2[0] == '/') + dest += path2.substr(1); + else + dest += path2; + else + if (path2[0] == '/') + dest += path2; + else + { + dest += '/'; + dest += path2; + } +} + +std::string joinpath(const std::string& path1, const std::string& path2) +{ + string res = path1; + appendpath(res, path2); + return res; +} + +std::string normpath(const std::string& pathname) +{ + vector<string> st; + if (pathname[0] == '/') + st.push_back("/"); + + Split split(pathname, "/"); + for (const auto& i: split) + { + if (i == "." || i.empty()) continue; + if (i == "..") + if (st.back() == "..") + st.emplace_back(i); + else if (st.back() == "/") + continue; + else + st.pop_back(); + else + st.emplace_back(i); + } + + if (st.empty()) + return "."; + + string res; + for (const auto& i: st) + appendpath(res, i); + return res; +} + +Split::const_iterator::const_iterator(const Split& split) + : split(&split) +{ + // Ignore leading separators if skip_end is true + if (split.skip_empty) skip_separators(); + ++*this; +} + +Split::const_iterator::~const_iterator() +{ +} + +std::string Split::const_iterator::remainder() const +{ + if (end == std::string::npos) + return std::string(); + else + return split->str.substr(end); +}; + +void Split::const_iterator::skip_separators() +{ + const std::string& str = split->str; + const std::string& sep = split->sep; + + while (end + sep.size() <= str.size()) + { + unsigned i = 0; + for ( ; i < sep.size(); ++i) + if (str[end + i] != sep[i]) + break; + if (i < sep.size()) + break; + else + end += sep.size(); + } +} + +Split::const_iterator& Split::const_iterator::operator++() +{ + if (!split) return *this; + + const std::string& str = split->str; + const std::string& sep = split->sep; + bool skip_empty = split->skip_empty; + + /// Convert into an end iterator + if (end == std::string::npos) + { + split = nullptr; + return *this; + } + + /// The string ended with an iterator, and we do not skip empty tokens: + /// return it + if (end == str.size()) + { + cur = string(); + end = std::string::npos; + return *this; + } + + /// Position of the first character past the token that starts at 'end' + size_t tok_end; + if (sep.empty()) + /// If separator is empty, advance one character at a time + tok_end = end + 1; + else + { + /// The token ends at the next separator + tok_end = str.find(sep, end); + } + + /// No more separators found, return from end to the end of the string + if (tok_end == std::string::npos) + { + cur = str.substr(end); + end = std::string::npos; + return *this; + } + + /// We have the boundaries of the current token + cur = str.substr(end, tok_end - end); + + /// Skip the separator + end = tok_end + sep.size(); + + /// Skip all the following separators if skip_empty is true + if (skip_empty) + { + skip_separators(); + if (end == str.size()) + { + end = std::string::npos; + return *this; + } + } + + return *this; +} + +const std::string& Split::const_iterator::operator*() const { return cur; } +const std::string* Split::const_iterator::operator->() const { return &cur; } + +bool Split::const_iterator::operator==(const const_iterator& ti) const +{ + if (!split && !ti.split) return true; + if (split != ti.split) return false; + return end == ti.end; +} + +bool Split::const_iterator::operator!=(const const_iterator& ti) const +{ + if (!split && !ti.split) return false; + if (split != ti.split) return true; + return end != ti.end; +} + + +std::string encode_cstring(const std::string& str) +{ + string res; + for (string::const_iterator i = str.begin(); i != str.end(); ++i) + if (*i == '\n') + res += "\\n"; + else if (*i == '\t') + res += "\\t"; + else if (*i == 0 || iscntrl(*i)) + { + char buf[5]; + snprintf(buf, 5, "\\x%02x", (unsigned int)*i); + res += buf; + } + else if (*i == '"' || *i == '\\') + { + res += "\\"; + res += *i; + } + else + res += *i; + return res; +} + +std::string decode_cstring(const std::string& str, size_t& lenParsed) +{ + string res; + string::const_iterator i = str.begin(); + for ( ; i != str.end() && *i != '"'; ++i) + if (*i == '\\' && (i+1) != str.end()) + { + switch (*(i+1)) + { + case 'n': res += '\n'; break; + case 't': res += '\t'; break; + case 'x': { + size_t j; + char buf[5] = "0x\0\0"; + // Read up to 2 extra hex digits + for (j = 0; j < 2 && i+2+j != str.end() && isxdigit(*(i+2+j)); ++j) + buf[2+j] = *(i+2+j); + i += j; + res += (char)atoi(buf); + break; + } + default: + res += *(i+1); + break; + } + ++i; + } else + res += *i; + if (i != str.end() && *i == '"') + ++i; + lenParsed = i - str.begin(); + return res; +} + +std::string encode_url(const std::string& str) +{ + string res; + for (string::const_iterator i = str.begin(); i != str.end(); ++i) + { + if ( (*i >= '0' && *i <= '9') || (*i >= 'A' && *i <= 'Z') + || (*i >= 'a' && *i <= 'z') || *i == '-' || *i == '_' + || *i == '!' || *i == '*' || *i == '\'' || *i == '(' || *i == ')') + res += *i; + else { + char buf[4]; + snprintf(buf, 4, "%%%02x", static_cast<unsigned>(static_cast<unsigned char>(*i))); + res += buf; + } + } + return res; +} + +std::string decode_url(const std::string& str) +{ + string res; + for (size_t i = 0; i < str.size(); ++i) + { + if (str[i] == '%') + { + // If there's a partial %something at the end, ignore it + if (i >= str.size() - 2) + return res; + res += static_cast<char>(strtoul(str.substr(i+1, 2).c_str(), 0, 16)); + i += 2; + } + else + res += str[i]; + } + return res; +} + +static const char* base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +template<typename T> +static const char invbase64(const T& idx) +{ + static const char data[] = {62,0,0,0,63,52,53,54,55,56,57,58,59,60,61,0,0,0,0,0,0,0,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,0,0,0,0,0,0,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51}; + if (idx < 43) return 0; + if (static_cast<unsigned>(idx) > 43 + (sizeof(data)/sizeof(data[0]))) return 0; + return data[idx - 43]; +} + +std::string encode_base64(const std::string& str) +{ + std::string res; + + for (size_t i = 0; i < str.size(); i += 3) + { + // Pack every triplet into 24 bits + unsigned int enc; + if (i + 3 < str.size()) + enc = ((unsigned char)str[i] << 16) | ((unsigned char)str[i + 1] << 8) | (unsigned char)str[i + 2]; + else + { + enc = ((unsigned char)str[i] << 16); + if (i + 1 < str.size()) + enc |= (unsigned char)str[i + 1] << 8; + if (i + 2 < str.size()) + enc |= (unsigned char)str[i + 2]; + } + + // Divide in 4 6-bit values and use them as indexes in the base64 char + // array + for (int j = 18; j >= 0; j -= 6) + res += base64[(enc >> j) & 63]; + } + + // Replace padding characters with '=' + if (str.size() % 3) + for (size_t i = 0; i < 3 - (str.size() % 3); ++i) + res[res.size() - i - 1] = '='; + + return res; +} + +std::string decode_base64(const std::string& str) +{ + std::string res; + + for (size_t i = 0; i < str.size(); i += 4) + { + // Pack every quadruplet into 24 bits + unsigned int enc; + if (i+4 < str.size()) + { + enc = (invbase64(str[i]) << 18) + + (invbase64(str[i+1]) << 12) + + (invbase64(str[i+2]) << 6) + + (invbase64(str[i+3])); + } else { + enc = (invbase64(str[i]) << 18); + if (i+1 < str.size()) + enc += (invbase64(str[i+1]) << 12); + if (i+2 < str.size()) + enc += (invbase64(str[i+2]) << 6); + if (i+3 < str.size()) + enc += (invbase64(str[i+3])); + } + + // Divide in 3 8-bit values and append them to the result + res += enc >> 16 & 0xff; + res += enc >> 8 & 0xff; + res += enc & 0xff; + } + + // Remove trailing padding + if (str.size() > 0) + for (size_t i = str.size() - 1; str[i] == '='; --i) + { + if (res.size() > 0) + res.resize(res.size() - 1); + if (i == 0 || res.size() == 0 ) + break; + } + + return res; +} + + +} +} diff --git a/ept/utils/string.h b/ept/utils/string.h new file mode 100644 index 0000000..5988365 --- /dev/null +++ b/ept/utils/string.h @@ -0,0 +1,301 @@ +#ifndef EPT_STRING_H +#define EPT_STRING_H + +/** + * @author Enrico Zini <enrico@enricozini.org> + * @brief String functions + * + * Copyright (C) 2007--2015 Enrico Zini <enrico@debian.org> + */ + +#include <string> +#include <functional> +#include <sstream> +#include <cctype> + +namespace ept { +namespace str { + +/// Check if a string starts with the given substring +inline bool startswith(const std::string& str, const std::string& part) +{ + if (str.size() < part.size()) + return false; + return str.substr(0, part.size()) == part; +} + +/// Check if a string ends with the given substring +inline bool endswith(const std::string& str, const std::string& part) +{ + if (str.size() < part.size()) + return false; + return str.substr(str.size() - part.size()) == part; +} + +/** + * Stringify and join a sequence of objects + */ +template<typename ITER> +std::string join(const std::string& sep, const ITER& begin, const ITER& end) +{ + std::stringstream res; + bool first = true; + for (ITER i = begin; i != end; ++i) + { + if (first) + first = false; + else + res << sep; + res << *i; + } + return res.str(); +} + +/** + * Stringify and join an iterable container + */ +template<typename ITEMS> +std::string join(const std::string& sep, const ITEMS& items) +{ + std::stringstream res; + bool first = true; + for (const auto& i: items) + { + if (first) + first = false; + else + res << sep; + res << i; + } + return res.str(); +} + +/** + * Return the substring of 'str' without all leading characters for which + * 'classifier' returns true. + */ +template<typename FUN> +inline std::string lstrip(const std::string& str, const FUN& classifier) +{ + if (str.empty()) + return str; + + size_t beg = 0; + while (beg < str.size() && classifier(str[beg])) + ++beg; + + return str.substr(beg, str.size() - beg + 1); +} + +/** + * Return the substring of 'str' without all leading spaces. + */ +inline std::string lstrip(const std::string& str) +{ + return lstrip(str, ::isspace); +} + +/** + * Return the substring of 'str' without all trailing characters for which + * 'classifier' returns true. + */ +template<typename FUN> +inline std::string rstrip(const std::string& str, const FUN& classifier) +{ + if (str.empty()) + return str; + + size_t end = str.size(); + while (end > 0 && classifier(str[end - 1])) + --end; + + if (end == 0) + return std::string(); + else + return str.substr(0, end); +} + +/** + * Return the substring of 'str' without all trailing spaces. + */ +inline std::string rstrip(const std::string& str) +{ + return rstrip(str, ::isspace); +} + +/** + * Return the substring of 'str' without all leading and trailing characters + * for which 'classifier' returns true. + */ +template<typename FUN> +inline std::string strip(const std::string& str, const FUN& classifier) +{ + if (str.empty()) + return str; + + size_t beg = 0; + size_t end = str.size() - 1; + while (beg < end && classifier(str[beg])) + ++beg; + while (end >= beg && classifier(str[end])) + --end; + + return str.substr(beg, end-beg+1); +} + +/** + * Return the substring of 'str' without all leading and trailing spaces. + */ +inline std::string strip(const std::string& str) +{ + return strip(str, ::isspace); +} + +/// Return an uppercased copy of str +inline std::string upper(const std::string& str) +{ + std::string res; + res.reserve(str.size()); + for (std::string::const_iterator i = str.begin(); i != str.end(); ++i) + res += ::toupper(*i); + return res; +} + +/// Return a lowercased copy of str +inline std::string lower(const std::string& str) +{ + std::string res; + res.reserve(str.size()); + for (std::string::const_iterator i = str.begin(); i != str.end(); ++i) + res += ::tolower(*i); + return res; +} + +/// Given a pathname, return the file name without its path +std::string basename(const std::string& pathname); + +/// Given a pathname, return the directory name without the file name +std::string dirname(const std::string& pathname); + +/// Append path2 to path1, adding slashes when appropriate +void appendpath(std::string& dest, const char* path2); + +/// Append path2 to path1, adding slashes when appropriate +void appendpath(std::string& dest, const std::string& path2); + +/// Append an arbitrary number of path components to \a dest +template<typename S1, typename S2, typename... Args> +void appendpath(std::string& dest, S1 first, S2 second, Args... next) +{ + appendpath(dest, first); + appendpath(dest, second, next...); +} + +/// Join two or more paths, adding slashes when appropriate +template<typename... Args> +std::string joinpath(Args... components) +{ + std::string res; + appendpath(res, components...); + return res; +} + +/** + * Normalise a pathname. + * + * For example, A//B, A/./B and A/foo/../B all become A/B. + */ +std::string normpath(const std::string& pathname); + +/** + * Split a string where a given substring is found + * + * This does a similar work to the split functions of perl, python and ruby. + * + * Example code: + * \code + * str::Split splitter(my_string, "/"); + * vector<string> split; + * std::copy(splitter.begin(), splitter.end(), back_inserter(split)); + * \endcode + */ +struct Split +{ + /// String to split + std::string str; + /// Separator + std::string sep; + /** + * If true, skip empty tokens, effectively grouping consecutive separators + * as if they were a single one + */ + bool skip_empty; + + Split(const std::string& str, const std::string& sep, bool skip_empty=false) + : str(str), sep(sep), skip_empty(skip_empty) {} + + class const_iterator : public std::iterator<std::input_iterator_tag, std::string> + { + protected: + const Split* split = nullptr; + /// Current token + std::string cur; + /// Position of the first character of the next token + size_t end = 0; + + /// Move end past all the consecutive separators that start at its position + void skip_separators(); + + public: + /// Begin iterator + const_iterator(const Split& split); + /// End iterator + const_iterator() {} + ~const_iterator(); + + const_iterator& operator++(); + const std::string& operator*() const; + const std::string* operator->() const; + + std::string remainder() const; + + bool operator==(const const_iterator& ti) const; + bool operator!=(const const_iterator& ti) const; + }; + + /// Return the begin iterator to split a string on instances of sep + const_iterator begin() { return const_iterator(*this); } + + /// Return the end iterator to string split + const_iterator end() { return const_iterator(); } +}; + +/** + * Escape the string so it can safely used as a C string inside double quotes + */ +std::string encode_cstring(const std::string& str); + +/** + * Unescape a C string, stopping at the first double quotes or at the end of + * the string. + * + * lenParsed is set to the number of characters that were pased (which can be + * greather than the size of the resulting string in case escapes were found) + */ +std::string decode_cstring(const std::string& str, size_t& lenParsed); + +/// Urlencode a string +std::string encode_url(const std::string& str); + +/// Decode an urlencoded string +std::string decode_url(const std::string& str); + +/// Encode a string in Base64 +std::string encode_base64(const std::string& str); + +/// Decode a string encoded in Base64 +std::string decode_base64(const std::string& str); + +} +} +#endif diff --git a/ept/utils/sys.cc b/ept/utils/sys.cc new file mode 100644 index 0000000..8f6f2ff --- /dev/null +++ b/ept/utils/sys.cc @@ -0,0 +1,786 @@ +#include "sys.h" +#include "string.h" +#include <cstddef> +#include <cstring> +#include <exception> +#include <sstream> +#include <system_error> +#include <cerrno> +#include <sys/mman.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <alloca.h> + +namespace { + +inline const char* to_cstring(const std::string& s) +{ + return s.c_str(); +} + +inline const char* to_cstring(const char* s) +{ + return s; +} + +} + +namespace ept { +namespace sys { + +std::unique_ptr<struct stat> stat(const std::string& pathname) +{ + std::unique_ptr<struct stat> res(new struct stat); + if (::stat(pathname.c_str(), res.get()) == -1) + { + if (errno == ENOENT) + return std::unique_ptr<struct stat>(); + else + throw std::system_error(errno, std::system_category(), "cannot stat " + pathname); + } + return res; +} + +void stat(const std::string& pathname, struct stat& st) +{ + if (::stat(pathname.c_str(), &st) == -1) + throw std::system_error(errno, std::system_category(), "cannot stat " + pathname); +} + +#define common_stat_body(testfunc) \ + struct stat st; \ + if (::stat(pathname.c_str(), &st) == -1) { \ + if (errno == ENOENT) \ + return false; \ + else \ + throw std::system_error(errno, std::system_category(), "cannot stat " + pathname); \ + } \ + return testfunc(st.st_mode) + +bool isdir(const std::string& pathname) +{ + common_stat_body(S_ISDIR); +} + +bool isblk(const std::string& pathname) +{ + common_stat_body(S_ISBLK); +} + +bool ischr(const std::string& pathname) +{ + common_stat_body(S_ISCHR); +} + +bool isfifo(const std::string& pathname) +{ + common_stat_body(S_ISFIFO); +} + +bool islnk(const std::string& pathname) +{ + common_stat_body(S_ISLNK); +} + +bool isreg(const std::string& pathname) +{ + common_stat_body(S_ISREG); +} + +bool issock(const std::string& pathname) +{ + common_stat_body(S_ISSOCK); +} + +#undef common_stat_body + +time_t timestamp(const std::string& file) +{ + struct stat st; + stat(file, st); + return st.st_mtime; +} + +time_t timestamp(const std::string& file, time_t def) +{ + auto st = sys::stat(file); + return st.get() ? st->st_mtime : def; +} + +size_t size(const std::string& file) +{ + struct stat st; + stat(file, st); + return (size_t)st.st_size; +} + +size_t size(const std::string& file, size_t def) +{ + auto st = sys::stat(file); + return st.get() ? (size_t)st->st_size : def; +} + +ino_t inode(const std::string& file) +{ + struct stat st; + stat(file, st); + return st.st_ino; +} + +ino_t inode(const std::string& file, ino_t def) +{ + auto st = sys::stat(file); + return st.get() ? st->st_ino : def; +} + + +bool access(const std::string &s, int m) +{ + return ::access(s.c_str(), m) == 0; +} + +bool exists(const std::string& file) +{ + return sys::access(file, F_OK); +} + +std::string getcwd() +{ +#if defined(__GLIBC__) + char* cwd = ::get_current_dir_name(); + if (cwd == NULL) + throw std::system_error(errno, std::system_category(), "cannot get the current working directory"); + const std::string str(cwd); + ::free(cwd); + return str; +#else + size_t size = pathconf(".", _PC_PATH_MAX); + char *buf = (char *)alloca( size ); + if (::getcwd(buf, size) == NULL) + throw std::system_error(errno, std::system_category(), "cannot get the current working directory"); + return buf; +#endif +} + +std::string abspath(const std::string& pathname) +{ + if (pathname[0] == '/') + return str::normpath(pathname); + else + return str::normpath(str::joinpath(sys::getcwd(), pathname)); +} + + +/* + * MMap + */ + +MMap::MMap(void* addr, size_t length) + : addr(addr), length(length) +{ +} + +MMap::MMap(MMap&& o) + : addr(o.addr), length(o.length) +{ + o.addr = MAP_FAILED; + o.length = 0; +} + +MMap& MMap::operator=(MMap&& o) +{ + if (this == &o) return *this; + + munmap(); + addr = o.addr; + length = o.length; + o.addr = MAP_FAILED; + o.length = 0; + return *this; +} + +MMap::~MMap() +{ + if (addr != MAP_FAILED) ::munmap(addr, length); +} + +void MMap::munmap() +{ + if (::munmap(addr, length) == -1) + throw std::system_error(errno, std::system_category(), "cannot unmap memory"); + addr = MAP_FAILED; +} + + +/* + * FileDescriptor + */ + +FileDescriptor::FileDescriptor() {} +FileDescriptor::FileDescriptor(FileDescriptor&& o) + : fd(o.fd) +{ + o.fd = -1; +} +FileDescriptor::FileDescriptor(int fd) : fd(fd) {} +FileDescriptor::~FileDescriptor() {} + +void FileDescriptor::throw_error(const char* desc) +{ + throw std::system_error(errno, std::system_category(), desc); +} + +void FileDescriptor::close() +{ + if (fd == -1) return; + if (::close(fd) == -1) + throw_error("cannot close"); + fd = -1; +} + +void FileDescriptor::fstat(struct stat& st) +{ + if (::fstat(fd, &st) == -1) + throw_error("cannot stat"); +} + +void FileDescriptor::fchmod(mode_t mode) +{ + if (::fchmod(fd, mode) == -1) + throw_error("cannot fchmod"); +} + +size_t FileDescriptor::write(const void* buf, size_t count) +{ + ssize_t res = ::write(fd, buf, count); + if (res == -1) + throw_error("cannot write"); + return res; +} + +void FileDescriptor::write_all(const void* buf, size_t count) +{ + size_t written = 0; + while (written < count) + written += write((unsigned char*)buf + written, count - written); +} + +MMap FileDescriptor::mmap(size_t length, int prot, int flags, off_t offset) +{ + void* res =::mmap(0, length, prot, flags, fd, offset); + if (res == MAP_FAILED) + throw_error("cannot mmap"); + return MMap(res, length); +} + + +/* + * NamedFileDescriptor + */ + +NamedFileDescriptor::NamedFileDescriptor(int fd, const std::string& pathname) + : FileDescriptor(fd), pathname(pathname) +{ +} + +NamedFileDescriptor::NamedFileDescriptor(NamedFileDescriptor&& o) + : FileDescriptor(std::move(o)), pathname(std::move(o.pathname)) +{ +} + +NamedFileDescriptor& NamedFileDescriptor::operator=(NamedFileDescriptor&& o) +{ + if (this == &o) return *this; + fd = o.fd; + pathname = std::move(o.pathname); + o.fd = -1; + return *this; +} + +void NamedFileDescriptor::throw_error(const char* desc) +{ + throw std::system_error(errno, std::system_category(), pathname + ": " + desc); +} + + +/* + * Path + */ + +Path::Path(const char* pathname, int flags) + : NamedFileDescriptor(-1, pathname) +{ + fd = open(pathname, flags | O_PATH); + if (fd == -1) + throw_error("cannot open path"); +} + +Path::Path(const std::string& pathname, int flags) + : NamedFileDescriptor(-1, pathname) +{ + fd = open(pathname.c_str(), flags | O_PATH); + if (fd == -1) + throw_error("cannot open path"); +} + +Path::Path(Path& parent, const char* pathname, int flags) + : NamedFileDescriptor(parent.openat(pathname, flags | O_PATH), + str::joinpath(parent.name(), pathname)) +{ +} + +Path::~Path() +{ + if (fd != -1) + ::close(fd); +} + +DIR* Path::fdopendir() +{ + int fd1 = ::openat(fd, ".", O_DIRECTORY); + if (fd1 == -1) + throw_error("cannot open directory"); + + DIR* res = ::fdopendir(fd1); + if (!res) + throw_error("cannot fdopendir"); + + return res; +} + +Path::iterator Path::begin() +{ + if (fd == -1) + return iterator(); + else + return iterator(*this); +} + +Path::iterator Path::end() +{ + return iterator(); +} + +int Path::openat(const char* pathname, int flags, mode_t mode) +{ + int res = ::openat(fd, pathname, flags, mode); + if (res == -1) + throw_error("cannot openat"); + return res; +} + +void Path::fstatat(const char* pathname, struct stat& st) +{ + if (::fstatat(fd, pathname, &st, 0) == -1) + throw_error("cannot fstatat"); +} + +void Path::lstatat(const char* pathname, struct stat& st) +{ + if (::fstatat(fd, pathname, &st, AT_SYMLINK_NOFOLLOW) == -1) + throw_error("cannot fstatat"); +} + +void Path::unlinkat(const char* pathname) +{ + if (::unlinkat(fd, pathname, 0) == -1) + throw_error("cannot unlinkat"); +} + +void Path::rmdirat(const char* pathname) +{ + if (::unlinkat(fd, pathname, AT_REMOVEDIR) == -1) + throw_error("cannot unlinkat"); +} + +Path::iterator::iterator() +{ +} + +Path::iterator::iterator(Path& dir) + : path(&dir) +{ + this->dir = dir.fdopendir(); + + long name_max = fpathconf(dir.fd, _PC_NAME_MAX); + if (name_max == -1) // Limit not defined, or error: take a guess + name_max = 255; + size_t len = offsetof(dirent, d_name) + name_max + 1; + cur_entry = (struct dirent*)malloc(len); + if (cur_entry == NULL) + throw std::bad_alloc(); + + operator++(); +} + +Path::iterator::~iterator() +{ + if (cur_entry) free(cur_entry); + if (dir) closedir(dir); +} + +bool Path::iterator::operator==(const iterator& i) const +{ + if (!dir && !i.dir) return true; + if (!dir || !i.dir) return false; + return cur_entry->d_ino == i.cur_entry->d_ino; +} +bool Path::iterator::operator!=(const iterator& i) const +{ + if (!dir && !i.dir) return false; + if (!dir || !i.dir) return true; + return cur_entry->d_ino != i.cur_entry->d_ino; +} + +void Path::iterator::operator++() +{ + struct dirent* result; + if (readdir_r(dir, cur_entry, &result) != 0) + path->throw_error("cannot readdir_r"); + + if (result == nullptr) + { + // Turn into an end iterator + free(cur_entry); + cur_entry = nullptr; + closedir(dir); + dir = nullptr; + } +} + +bool Path::iterator::isdir() const +{ +#if defined(_DIRENT_HAVE_D_TYPE) || defined(HAVE_STRUCT_DIRENT_D_TYPE) + if (cur_entry->d_type == DT_DIR) + return true; + if (cur_entry->d_type != DT_UNKNOWN) + return false; +#endif + // No d_type, we'll need to stat + struct stat st; + path->fstatat(cur_entry->d_name, st); + return S_ISDIR(st.st_mode); +} + +bool Path::iterator::isblk() const +{ +#if defined(_DIRENT_HAVE_D_TYPE) || defined(HAVE_STRUCT_DIRENT_D_TYPE) + if (cur_entry->d_type == DT_BLK) + return true; + if (cur_entry->d_type != DT_UNKNOWN) + return false; +#endif + // No d_type, we'll need to stat + struct stat st; + path->fstatat(cur_entry->d_name, st); + return S_ISBLK(st.st_mode); +} + +bool Path::iterator::ischr() const +{ +#if defined(_DIRENT_HAVE_D_TYPE) || defined(HAVE_STRUCT_DIRENT_D_TYPE) + if (cur_entry->d_type == DT_CHR) + return true; + if (cur_entry->d_type != DT_UNKNOWN) + return false; +#endif + // No d_type, we'll need to stat + struct stat st; + path->fstatat(cur_entry->d_name, st); + return S_ISCHR(st.st_mode); +} + +bool Path::iterator::isfifo() const +{ +#if defined(_DIRENT_HAVE_D_TYPE) || defined(HAVE_STRUCT_DIRENT_D_TYPE) + if (cur_entry->d_type == DT_FIFO) + return true; + if (cur_entry->d_type != DT_UNKNOWN) + return false; +#endif + // No d_type, we'll need to stat + struct stat st; + path->fstatat(cur_entry->d_name, st); + return S_ISFIFO(st.st_mode); +} + +bool Path::iterator::islnk() const +{ +#if defined(_DIRENT_HAVE_D_TYPE) || defined(HAVE_STRUCT_DIRENT_D_TYPE) + if (cur_entry->d_type == DT_LNK) + return true; + if (cur_entry->d_type != DT_UNKNOWN) + return false; +#endif + struct stat st; + path->fstatat(cur_entry->d_name, st); + return S_ISLNK(st.st_mode); +} + +bool Path::iterator::isreg() const +{ +#if defined(_DIRENT_HAVE_D_TYPE) || defined(HAVE_STRUCT_DIRENT_D_TYPE) + if (cur_entry->d_type == DT_REG) + return true; + if (cur_entry->d_type != DT_UNKNOWN) + return false; +#endif + struct stat st; + path->fstatat(cur_entry->d_name, st); + return S_ISREG(st.st_mode); +} + +bool Path::iterator::issock() const +{ +#if defined(_DIRENT_HAVE_D_TYPE) || defined(HAVE_STRUCT_DIRENT_D_TYPE) + if (cur_entry->d_type == DT_SOCK) + return true; + if (cur_entry->d_type != DT_UNKNOWN) + return false; +#endif + struct stat st; + path->fstatat(cur_entry->d_name, st); + return S_ISSOCK(st.st_mode); +} + + +void Path::rmtree() +{ + for (auto i = begin(); i != end(); ++i) + { + if (strcmp(i->d_name, ".") == 0 || strcmp(i->d_name, "..") == 0) continue; + if (i.isdir()) + { + Path sub(*this, i->d_name); + sub.rmtree(); + } + else + unlinkat(i->d_name); + } + // TODO: is there a way to do this using fd instead? + rmdir(name()); +} + +/* + * File + */ + +File::File(const std::string& pathname, int flags, mode_t mode) + : NamedFileDescriptor(-1, pathname) +{ + fd = open(pathname.c_str(), flags, mode); + if (fd == -1) + throw std::system_error(errno, std::system_category(), "cannot open file " + pathname); +} + +File::~File() +{ + if (fd != -1) ::close(fd); +} + +File File::mkstemp(const std::string& prefix) +{ + char* fbuf = (char*)alloca(prefix.size() + 7); + memcpy(fbuf, prefix.data(), prefix.size()); + memcpy(fbuf + prefix.size(), "XXXXXX", 7); + int fd = ::mkstemp(fbuf); + if (fd < 0) + throw std::system_error(errno, std::system_category(), std::string("cannot create temporary file ") + fbuf); + return File(fd, fbuf); +} + +std::string read_file(const std::string& file) +{ + File in(file, O_RDONLY); + + // Get the file size + struct stat st; + in.fstat(st); + + // mmap the input file + MMap src = in.mmap(st.st_size, PROT_READ, MAP_SHARED); + + return std::string((const char*)src, st.st_size); +} + +void write_file(const std::string& file, const std::string& data, mode_t mode) +{ + File out(file, O_WRONLY | O_CREAT, mode); + out.write_all(data.data(), data.size()); + out.close(); +} + +void write_file_atomically(const std::string& file, const std::string& data, mode_t mode) +{ + File out = File::mkstemp(file); + + // Read the umask + mode_t mask = umask(0777); + umask(mask); + + // Set the file permissions, honoring umask + out.fchmod(mode & ~mask); + + out.write_all(data.data(), data.size()); + out.close(); + + if (rename(out.name().c_str(), file.c_str()) < 0) + throw std::system_error(errno, std::system_category(), "cannot rename " + out.name() + " to " + file); +} + +#if 0 +void mkFilePath(const std::string& file) +{ + size_t pos = file.rfind('/'); + if (pos != std::string::npos) + mkpath(file.substr(0, pos)); +} +#endif + +bool unlink_ifexists(const std::string& file) +{ + if (::unlink(file.c_str()) != 0) + { + if (errno != ENOENT) + throw std::system_error(errno, std::system_category(), "cannot unlink " + file); + else + return false; + } + else + return true; +} + +bool rename_ifexists(const std::string& src, const std::string& dst) +{ + if (::rename(src.c_str(), dst.c_str()) != 0) + { + if (errno != ENOENT) + throw std::system_error(errno, std::system_category(), "cannot rename " + src + " to " + dst); + else + return false; + } + else + return true; +} + +template<typename String> +static void impl_mkdir_ifmissing(String pathname, mode_t mode) +{ + for (unsigned i = 0; i < 5; ++i) + { + // If it does not exist, make it + if (::mkdir(to_cstring(pathname), mode) != -1) + return; + + // throw on all errors except EEXIST. Note that EEXIST "includes the case + // where pathname is a symbolic link, dangling or not." + if (errno != EEXIST && errno != EISDIR) + { + std::stringstream msg; + msg << "cannot create directory " << pathname; + throw std::system_error(errno, std::system_category(), msg.str()); + } + + // Ensure that, if dir exists, it is a directory + std::unique_ptr<struct stat> st = sys::stat(pathname); + if (st.get() == NULL) + { + // Either dir has just been deleted, or we hit a dangling + // symlink. + // + // Retry creating a directory: the more we keep failing, the more + // the likelyhood of a dangling symlink increases. + // + // We could lstat here, but it would add yet another case for a + // race condition if the broken symlink gets deleted between the + // stat and the lstat. + continue; + } + else if (!S_ISDIR(st->st_mode)) + { + // If it exists but it is not a directory, complain + std::stringstream msg; + msg << pathname << " exists but is not a directory"; + throw std::runtime_error(msg.str()); + } + else + // If it exists and it is a directory, we're fine + return; + } + std::stringstream msg; + msg << pathname << " exists and looks like a dangling symlink"; + throw std::runtime_error(msg.str()); +} + +void mkdir_ifmissing(const char* pathname, mode_t mode) +{ + return impl_mkdir_ifmissing(pathname, mode); +} + +void mkdir_ifmissing(const std::string& pathname, mode_t mode) +{ + return impl_mkdir_ifmissing(pathname, mode); +} + +void makedirs(const std::string& pathname, mode_t mode) +{ + if (pathname == "/" || pathname == ".") return; + std::string parent = str::dirname(pathname); + + // First ensure that the upper path exists + makedirs(parent, mode); + + // Then create this dir + mkdir_ifmissing(pathname, mode); +} + +std::string which(const std::string& name) +{ + // argv[0] has an explicit path: ensure it becomes absolute + if (name.find('/') != std::string::npos) + return sys::abspath(name); + + // argv[0] has no explicit path, look for it in $PATH + const char* path = getenv("PATH"); + if (!path) return name; + + str::Split splitter(path, ":", true); + for (const auto& i: splitter) + { + std::string candidate = str::joinpath(i, name); + if (sys::access(candidate, X_OK)) + return sys::abspath(candidate); + } + + return name; +} + +void unlink(const std::string& pathname) +{ + if (::unlink(pathname.c_str()) < 0) + throw std::system_error(errno, std::system_category(), "cannot unlink " + pathname); +} + +void rmdir(const std::string& pathname) +{ + if (::rmdir(pathname.c_str()) < 0) + throw std::system_error(errno, std::system_category(), "cannot rmdir " + pathname); +} + +void rmtree(const std::string& pathname) +{ + Path path(pathname); + path.rmtree(); +} + +#if 0 +std::string mkdtemp( std::string tmpl ) +{ + char *_tmpl = reinterpret_cast< char * >( alloca( tmpl.size() + 1 ) ); + strcpy( _tmpl, tmpl.c_str() ); + return ::mkdtemp( _tmpl ); +} +#endif +} +} diff --git a/ept/utils/sys.h b/ept/utils/sys.h new file mode 100644 index 0000000..334c983 --- /dev/null +++ b/ept/utils/sys.h @@ -0,0 +1,468 @@ +#ifndef EPT_SYS_H +#define EPT_SYS_H + +/** + * @author Enrico Zini <enrico@enricozini.org> + * @brief Operating system functions + * + * Copyright (C) 2007--2015 Enrico Zini <enrico@debian.org> + */ + +#include <string> +//#include <iosfwd> +#include <memory> +#include <iterator> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <dirent.h> + +namespace ept { +namespace sys { + +/** + * stat() the given file and return the struct stat with the results. + * If the file does not exist, return NULL. + * Raises exceptions in case of errors. + */ +std::unique_ptr<struct stat> stat(const std::string& pathname); + +/** + * stat() the given file filling in the given structure. + * Raises exceptions in case of errors, including if the file does not exist. + */ +void stat(const std::string& pathname, struct stat& st); + +/** + * Returns true if the given pathname is a directory, else false. + * + * It also returns false if the pathname does not exist. + */ +bool isdir(const std::string& pathname); + +/// Same as isdir but checks for block devices +bool isblk(const std::string& pathname); + +/// Same as isdir but checks for character devices +bool ischr(const std::string& pathname); + +/// Same as isdir but checks for FIFOs +bool isfifo(const std::string& pathname); + +/// Same as isdir but checks for symbolic links +bool islnk(const std::string& pathname); + +/// Same as isdir but checks for regular files +bool isreg(const std::string& pathname); + +/// Same as isdir but checks for sockets +bool issock(const std::string& pathname); + +/// File mtime +time_t timestamp(const std::string& file); + +/// File mtime (or def if the file does not exist) +time_t timestamp(const std::string& file, time_t def); + +/// File size +size_t size(const std::string& file); + +/// File size (or def if the file does not exist) +size_t size(const std::string& file, size_t def); + +/// File inode number +ino_t inode(const std::string& file); + +/// File inode number (or 0 if the file does not exist) +ino_t inode(const std::string& file, ino_t def); + +/// access() a filename +bool access(const std::string& s, int m); + +/// Same as access(s, F_OK); +bool exists(const std::string& s); + +/// Get the absolute path of the current working directory +std::string getcwd(); + +/// Get the absolute path of a file +std::string abspath(const std::string& pathname); + +/** + * Wraps a mmapped memory area, unmapping it on destruction. + * + * MMap objects can be used as normal pointers + */ +class MMap +{ + void* addr; + size_t length; + +public: + MMap(const MMap&) = delete; + MMap(MMap&&); + MMap(void* addr, size_t length); + ~MMap(); + + MMap& operator=(const MMap&) = delete; + MMap& operator=(MMap&&); + + size_t size() const { return length; } + + void munmap(); + + template<typename T> + operator const T*() const { return reinterpret_cast<const T*>(addr); } + + template<typename T> + operator T*() const { return reinterpret_cast<T*>(addr); }; +}; + +/** + * Common operations on file descriptors. + * + * Except when documented otherwise, methods of this class are just thin + * wrappers around the libc functions with the same name, that check error + * results and throw exceptions if the functions failed. + * + * Implementing what to do on construction and destruction is left to the + * subclassers: at the FileDescriptor level, the destructor does nothing and + * leaves the file descriptor open. + */ +class FileDescriptor +{ +protected: + int fd = -1; + +public: + FileDescriptor(); + FileDescriptor(FileDescriptor&& o); + FileDescriptor(int fd); + virtual ~FileDescriptor(); + + /** + * Throw an exception based on errno and the given message. + * + * This can be overridden by subclasses that may have more information + * about the file descriptor, so that they can generate more descriptive + * messages. + */ + [[noreturn]] virtual void throw_error(const char* desc); + + void close(); + + void fstat(struct stat& st); + void fchmod(mode_t mode); + + size_t write(const void* buf, size_t count); + + /** + * Write all the data in buf, retrying partial writes + */ + void write_all(const void* buf, size_t count); + + MMap mmap(size_t length, int prot, int flags, off_t offset=0); + + operator int() const { return fd; } +}; + + +/** + * File descriptor with a name + */ + +class NamedFileDescriptor : public FileDescriptor +{ +protected: + std::string pathname; + +public: + NamedFileDescriptor(int fd, const std::string& pathname); + NamedFileDescriptor(NamedFileDescriptor&&); + + NamedFileDescriptor& operator=(NamedFileDescriptor&&); + + [[noreturn]] virtual void throw_error(const char* desc); + + /// Return the file pathname + const std::string& name() const { return pathname; } +}; + +/** + * Wrap a path on the file system opened with O_PATH + */ +struct Path : public NamedFileDescriptor +{ + /** + * Iterator for directory entries + */ + struct iterator : public std::iterator<std::input_iterator_tag, struct dirent> + { + Path* path = nullptr; + DIR* dir = nullptr; + struct dirent* cur_entry = nullptr; + + // End iterator + iterator(); + // Start iteration on dir + iterator(Path& dir); + iterator(iterator&) = delete; + iterator(iterator&& o) + : dir(o.dir), cur_entry(o.cur_entry) + { + o.dir = nullptr; + o.cur_entry = nullptr; + } + ~iterator(); + iterator& operator=(iterator&) = delete; + iterator& operator=(iterator&&) = delete; + + bool operator==(const iterator& i) const; + bool operator!=(const iterator& i) const; + struct dirent& operator*() const { return *cur_entry; } + struct dirent* operator->() const { return cur_entry; } + void operator++(); + + /// @return true if we refer to a directory, else false + bool isdir() const; + + /// @return true if we refer to a block device, else false + bool isblk() const; + + /// @return true if we refer to a character device, else false + bool ischr() const; + + /// @return true if we refer to a named pipe (FIFO). + bool isfifo() const; + + /// @return true if we refer to a symbolic link. + bool islnk() const; + + /// @return true if we refer to a regular file. + bool isreg() const; + + /// @return true if we refer to a Unix domain socket. + bool issock() const; + }; + + using NamedFileDescriptor::NamedFileDescriptor; + + /** + * Open the given pathname with flags | O_PATH. + */ + Path(const char* pathname, int flags=0); + /** + * Open the given pathname with flags | O_PATH. + */ + Path(const std::string& pathname, int flags=0); + /** + * Open the given pathname calling parent.openat, with flags | O_PATH + */ + Path(Path& parent, const char* pathname, int flags=0); + Path(const Path&) = delete; + Path(Path&&) = default; + Path& operator=(const Path&) = delete; + Path& operator=(Path&&) = default; + + /** + * The destructor closes the file descriptor, but does not check errors on + * ::close(). + * + * In normal program flow, it is a good idea to explicitly call + * Path::close() in places where it can throw safely. + */ + ~Path(); + + DIR* fdopendir(); + + /// Begin iterator on all directory entries + iterator begin(); + + /// End iterator on all directory entries + iterator end(); + + int openat(const char* pathname, int flags, mode_t mode=0777); + + void fstatat(const char* pathname, struct stat& st); + + /// fstatat with the AT_SYMLINK_NOFOLLOW flag set + void lstatat(const char* pathname, struct stat& st); + + void unlinkat(const char* pathname); + + /// unlinkat with the AT_REMOVEDIR flag set + void rmdirat(const char* pathname); + + /** + * Delete the directory pointed to by this Path, with all its contents. + * + * The path must point to a directory. + */ + void rmtree(); +}; + + +/** + * open(2) file descriptors + */ +class File : public NamedFileDescriptor +{ +public: + using NamedFileDescriptor::NamedFileDescriptor; + + File(File&&) = default; + File(const File&) = delete; + + /// Wrapper around open(2) + File(const std::string& pathname, int flags, mode_t mode=0777); + + /** + * The destructor closes the file descriptor, but does not check errors on + * ::close(). + * + * In normal program flow, it is a good idea to explicitly call + * File::close() in places where it can throw safely. + */ + ~File(); + + File& operator=(const File&) = delete; + File& operator=(File&&) = default; + + static File mkstemp(const std::string& prefix); +}; + +/// Read whole file into memory. Throws exceptions on failure. +std::string read_file(const std::string &file); + +/** + * Write \a data to \a file, replacing existing contents if it already exists. + * + * New files are created with the given permission mode, honoring umask. + * Permissions of existing files do not change. + */ +void write_file(const std::string& file, const std::string& data, mode_t mode=0777); + +/** + * Write \a data to \a file, replacing existing contents if it already exists. + * + * Files are created with the given permission mode, honoring umask. If the + * file already exists, its mode is ignored. + * + * Data is written to a temporary file, then moved to its final destination, to + * ensure an atomic operation. + */ +void write_file_atomically(const std::string& file, const std::string& data, mode_t mode=0777); + +#if 0 +// Create a temporary directory based on a template. +std::string mkdtemp(std::string templ); + +/// Ensure that the path to the given file exists, creating it if it does not. +/// The file itself will not get created. +void mkFilePath(const std::string& file); +#endif + +/** + * Delete a file if it exists. If it does not exist, do nothing. + * + * @return true if the file was deleted, false if it did not exist + */ +bool unlink_ifexists(const std::string& file); + +/** + * Move \a src to \a dst, without raising exception if \a src does not exist + * + * @return true if the file was renamed, false if it did not exist + */ +bool rename_ifexists(const std::string& src, const std::string& dst); + +/// Create the given directory, if it does not already exists. +/// It will complain if the given pathname already exists but is not a +/// directory. +void mkdir_ifmissing(const char* pathname, mode_t mode=0777); + +void mkdir_ifmissing(const std::string& pathname, mode_t mode=0777); + +/// Create all the component of the given directory, including the directory +/// itself. +void makedirs(const std::string& pathname, mode_t=0777); + +/** + * Compute the absolute path of an executable. + * + * If \a name is specified as a partial path, it ensures it is made absolute. + * If \a name is not specified as a path, it looks for the executable in $PATH + * and return its absolute pathname. + */ +std::string which(const std::string& name); + +/// Delete the file using unlink() +void unlink(const std::string& pathname); + +/// Remove the directory using rmdir(2) +void rmdir(const std::string& pathname); + +/// Delete the directory \a pathname and all its contents. +void rmtree(const std::string& pathname); + +#if 0 +/// Nicely wrap access to directories +class Directory +{ +protected: + /// Directory pathname + std::string m_path; + +public: + class const_iterator + { + /// Directory we are iterating + const Directory* dir; + /// DIR* pointer + void* dirp; + /// dirent structure used for iterating entries + struct dirent* direntbuf; + + public: + // Create an end iterator + const_iterator(); + // Create a begin iterator + const_iterator(const Directory& dir); + // Cleanup properly + ~const_iterator(); + + /// auto_ptr style copy semantics + const_iterator(const const_iterator& i); + const_iterator& operator=(const const_iterator& i); + + /// Move to the next directory entry + const_iterator& operator++(); + + /// @return the current file name + std::string operator*() const; + + bool operator==(const const_iterator& iter) const; + bool operator!=(const const_iterator& iter) const; + }; + + Directory(const std::string& path); + ~Directory(); + + /// Pathname of the directory + const std::string& path() const { return m_path; } + + /// Check if the directory exists + bool exists() const; + + /// Begin iterator + const_iterator begin() const; + + /// End iterator + const_iterator end() const; +}; + +#endif +} +} + +#endif diff --git a/ept/utils/tests-main.cc b/ept/utils/tests-main.cc new file mode 100644 index 0000000..1aec45a --- /dev/null +++ b/ept/utils/tests-main.cc @@ -0,0 +1,139 @@ +#include "tests.h" +#include <signal.h> +#include <cstdlib> +#include <cstring> +#include <exception> + +void signal_to_exception(int) +{ + throw std::runtime_error("killing signal catched"); +} + +int main(int argc,const char* argv[]) +{ + using namespace ept::tests; + + signal(SIGSEGV, signal_to_exception); + signal(SIGILL, signal_to_exception); + +#if 0 + if( (argc == 2 && (! strcmp ("help", argv[1]))) || argc > 3 ) + { + std::cout << "TUT example test application." << std::endl; + std::cout << "Usage: example [regression] | [list] | [ group] [test]" << std::endl; + std::cout << " List all groups: example list" << std::endl; + std::cout << " Run all tests: example regression" << std::endl; + std::cout << " Run one group: example std::auto_ptr" << std::endl; + std::cout << " Run one test: example std::auto_ptr 3" << std::endl;; + } + + // std::cout << "\nFAILURE and EXCEPTION in these tests are FAKE ;)\n\n"; + + tut::runner.get().set_callback(&visi); + + try + { + if( argc == 1 || (argc == 2 && std::string(argv[1]) == "regression") ) + { + tut::runner.get().run_tests(); + } + else if( argc == 2 && std::string(argv[1]) == "list" ) + { + std::cout << "registered test groups:" << std::endl; + tut::groupnames gl = tut::runner.get().list_groups(); + tut::groupnames::const_iterator i = gl.begin(); + tut::groupnames::const_iterator e = gl.end(); + while( i != e ) + { + std::cout << " " << *i << std::endl; + ++i; + } + } + else if( argc == 2 && std::string(argv[1]) != "regression" ) + { + tut::runner.get().run_tests(argv[1]); + } + else if( argc == 3 ) + { + tut::runner.get().run_test(argv[1],::atoi(argv[2])); + } + } + catch( const std::exception& ex ) + { + std::cerr << "tut raised exception: " << ex.what() << std::endl; + } +#endif + + auto& tests = TestRegistry::get(); + + SimpleTestController controller; + + if (const char* whitelist = getenv("TEST_WHITELIST")) + controller.whitelist = whitelist; + + if (const char* blacklist = getenv("TEST_BLACKLIST")) + controller.blacklist = blacklist; + + auto all_results = tests.run_tests(controller); + + unsigned methods_ok = 0; + unsigned methods_failed = 0; + unsigned methods_skipped = 0; + unsigned test_cases_ok = 0; + unsigned test_cases_failed = 0; + + for (const auto& tc_res: all_results) + { + if (!tc_res.fail_setup.empty()) + { + fprintf(stderr, "%s: %s\n", tc_res.test_case.c_str(), tc_res.fail_setup.c_str()); + ++test_cases_failed; + } else { + if (!tc_res.fail_teardown.empty()) + { + fprintf(stderr, "%s: %s\n", tc_res.test_case.c_str(), tc_res.fail_teardown.c_str()); + ++test_cases_failed; + } + else + ++test_cases_ok; + + for (const auto& tm_res: tc_res.methods) + { + if (tm_res.skipped) + ++methods_skipped; + else if (tm_res.is_success()) + ++methods_ok; + else + { + fprintf(stderr, "\n"); + if (tm_res.exception_typeid.empty()) + fprintf(stderr, "%s.%s: %s\n", tm_res.test_case.c_str(), tm_res.test_method.c_str(), tm_res.error_message.c_str()); + else + fprintf(stderr, "%s.%s:[%s] %s\n", tm_res.test_case.c_str(), tm_res.test_method.c_str(), tm_res.exception_typeid.c_str(), tm_res.error_message.c_str()); + for (const auto& frame : tm_res.error_stack) + fprintf(stderr, " %s", frame.format().c_str()); + ++methods_failed; + } + } + } + } + + bool success = true; + + if (test_cases_failed) + { + success = false; + fprintf(stderr, "\n%u/%u test cases had issues initializing or cleaning up\n", + test_cases_failed, test_cases_ok + test_cases_failed); + } + + if (methods_failed) + { + success = false; + fprintf(stderr, "\n%u/%u tests failed\n", methods_failed, methods_ok + methods_failed); + } + else + fprintf(stderr, "%u tests succeeded\n", methods_ok); + + return success ? 0 : 1; +} diff --git a/ept/utils/tests.cc b/ept/utils/tests.cc new file mode 100644 index 0000000..28ea280 --- /dev/null +++ b/ept/utils/tests.cc @@ -0,0 +1,578 @@ +/* + * @author Enrico Zini <enrico@enricozini.org>, Peter Rockai (mornfall) <me@mornfall.net> + * @brief Utility functions for the unit tests + * + * Copyright (C) 2006--2007 Peter Rockai (mornfall) <me@mornfall.net> + * Copyright (C) 2003--2015 Enrico Zini <enrico@debian.org> + */ + +#include "tests.h" +#include "string.h" +#include <fnmatch.h> +#include <cmath> +#include <iomanip> +#include <sys/types.h> +#include <regex.h> + +using namespace std; +using namespace ept; + +const ept::tests::LocationInfo ept_test_location_info; + +namespace ept { +namespace tests { + +/* + * TestStackFrame + */ + +std::string TestStackFrame::format() const +{ + std::stringstream ss; + format(ss); + return ss.str(); +} + +void TestStackFrame::format(std::ostream& out) const +{ + out << file << ":" << line << ":" << call; + if (!local_info.empty()) + out << " [" << local_info << "]"; + out << endl; +} + + +/* + * TestStack + */ + +void TestStack::backtrace(std::ostream& out) const +{ + for (const auto& frame: *this) + frame.format(out); +} + +std::string TestStack::backtrace() const +{ + std::stringstream ss; + backtrace(ss); + return ss.str(); +} + + +/* + * TestFailed + */ + +TestFailed::TestFailed(const std::exception& e) + : message(typeid(e).name()) +{ + message += ": "; + message += e.what(); +} + + +#if 0 +std::string Location::fail_msg(const std::string& error) const +{ + std::stringstream ss; + ss << "test failed at:" << endl; + backtrace(ss); + ss << file << ":" << line << ":error: " << error << endl; + return ss.str(); +} + +std::string Location::fail_msg(std::function<void(std::ostream&)> write_error) const +{ + std::stringstream ss; + ss << "test failed at:" << endl; + backtrace(ss); + ss << file << ":" << line << ":error: "; + write_error(ss); + ss << endl; + return ss.str(); +} +#endif + +std::ostream& LocationInfo::operator()() +{ + str(std::string()); + clear(); + return *this; +} + +/* + * Assertions + */ + +void assert_startswith(const std::string& actual, const std::string& expected) +{ + if (str::startswith(actual, expected)) return; + std::stringstream ss; + ss << "'" << actual << "' does not start with '" << expected << "'"; + throw TestFailed(ss.str()); +} + +void assert_endswith(const std::string& actual, const std::string& expected) +{ + if (str::endswith(actual, expected)) return; + std::stringstream ss; + ss << "'" << actual << "' does not end with '" << expected << "'"; + throw TestFailed(ss.str()); +} + +void assert_contains(const std::string& actual, const std::string& expected) +{ + if (actual.find(expected) != std::string::npos) return; + std::stringstream ss; + ss << "'" << actual << "' does not contain '" << expected << "'"; + throw TestFailed(ss.str()); +} + +void assert_not_contains(const std::string& actual, const std::string& expected) +{ + if (actual.find(expected) == std::string::npos) return; + std::stringstream ss; + ss << "'" << actual << "' contains '" << expected << "'"; + throw TestFailed(ss.str()); +} + +namespace { + +struct Regexp +{ + regex_t compiled; + + Regexp(const char* regex) + { + if (int err = regcomp(&compiled, regex, REG_EXTENDED | REG_NOSUB)) + raise_error(err); + } + ~Regexp() + { + regfree(&compiled); + } + + bool search(const char* s) + { + return regexec(&compiled, s, 0, nullptr, 0) != REG_NOMATCH; + } + + void raise_error(int code) + { + // Get the size of the error message string + size_t size = regerror(code, &compiled, nullptr, 0); + + char* buf = new char[size]; + regerror(code, &compiled, buf, size); + string msg(buf); + delete[] buf; + throw std::runtime_error(msg); + } +}; + +} + +void assert_re_matches(const std::string& actual, const std::string& expected) +{ + Regexp re(expected.c_str()); + if (re.search(actual.c_str())) return; + std::stringstream ss; + ss << "'" << actual << "' does not match '" << expected << "'"; + throw TestFailed(ss.str()); +} + +void assert_not_re_matches(const std::string& actual, const std::string& expected) +{ + Regexp re(expected.c_str()); + if (!re.search(actual.c_str())) return; + std::stringstream ss; + ss << "'" << actual << "' should not match '" << expected << "'"; + throw TestFailed(ss.str()); +} + +void assert_true(std::nullptr_t actual) +{ + throw TestFailed("actual value nullptr is not true"); +}; + +void assert_false(std::nullptr_t actual) +{ +}; + + +static void _actual_must_be_set(const char* actual) +{ + if (!actual) + throw TestFailed("actual value is the null pointer instead of a valid string"); +} + +void ActualCString::operator==(const char* expected) const +{ + if (expected && _actual) + assert_equal<std::string, std::string>(_actual, expected); + else if (!expected && !_actual) + ; + else if (expected) + { + std::stringstream ss; + ss << "actual value is nullptr instead of the expected string \"" << str::encode_cstring(expected) << "\""; + throw TestFailed(ss.str()); + } + else + { + std::stringstream ss; + ss << "actual value is the string \"" << str::encode_cstring(_actual) << "\" instead of nullptr"; + throw TestFailed(ss.str()); + } +} + +void ActualCString::operator==(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_equal<std::string, std::string>(_actual, expected); +} + +void ActualCString::operator!=(const char* expected) const +{ + if (expected && _actual) + assert_not_equal<std::string, std::string>(_actual, expected); + else if (!expected && !_actual) + throw TestFailed("actual and expected values are both nullptr but they should be different"); +} + +void ActualCString::operator!=(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_not_equal<std::string, std::string>(_actual, expected); +} + +void ActualCString::operator<(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_less<std::string, std::string>(_actual, expected); +} + +void ActualCString::operator<=(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_less_equal<std::string, std::string>(_actual, expected); +} + +void ActualCString::operator>(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_greater<std::string, std::string>(_actual, expected); +} + +void ActualCString::operator>=(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_greater_equal<std::string, std::string>(_actual, expected); +} + +void ActualCString::matches(const std::string& re) const +{ + _actual_must_be_set(_actual); + assert_re_matches(_actual, re); +} + +void ActualCString::not_matches(const std::string& re) const +{ + _actual_must_be_set(_actual); + assert_not_re_matches(_actual, re); +} + +void ActualCString::startswith(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_startswith(_actual, expected); +} + +void ActualCString::endswith(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_endswith(_actual, expected); +} + +void ActualCString::contains(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_contains(_actual, expected); +} + +void ActualCString::not_contains(const std::string& expected) const +{ + _actual_must_be_set(_actual); + assert_not_contains(_actual, expected); +} + +void ActualStdString::startswith(const std::string& expected) const +{ + assert_startswith(_actual, expected); +} + +void ActualStdString::endswith(const std::string& expected) const +{ + assert_endswith(_actual, expected); +} + +void ActualStdString::contains(const std::string& expected) const +{ + assert_contains(_actual, expected); +} + +void ActualStdString::not_contains(const std::string& expected) const +{ + assert_not_contains(_actual, expected); +} + +void ActualStdString::matches(const std::string& re) const +{ + assert_re_matches(_actual, re); +} + +void ActualStdString::not_matches(const std::string& re) const +{ + assert_not_re_matches(_actual, re); +} + +void ActualDouble::almost_equal(double expected, unsigned places) const +{ + if (round((_actual - expected) * exp10(places)) == 0.0) + return; + std::stringstream ss; + ss << std::setprecision(places) << fixed << _actual << " is different than the expected " << expected; + throw TestFailed(ss.str()); +} + +void ActualDouble::not_almost_equal(double expected, unsigned places) const +{ + if (round(_actual - expected * exp10(places)) != 0.0) + return; + std::stringstream ss; + ss << std::setprecision(places) << fixed << _actual << " is the same as the expected " << expected; + throw TestFailed(ss.str()); +} + +void ActualFunction::throws(const std::string& what_match) const +{ + bool thrown = false; + try { + _actual(); + } catch (std::exception& e) { + thrown = true; + wassert(actual(e.what()).matches(what_match)); + } + if (!thrown) + throw TestFailed("code did not throw any exception"); +} + +#if 0 +void test_assert_file_exists(WIBBLE_TEST_LOCPRM, const std::string& fname) +{ + if (not sys::fs::exists(fname)) + { + std::stringstream ss; + ss << "file '" << fname << "' does not exists"; + ept_test_location.fail_test(ss.str()); + } +} + +void test_assert_not_file_exists(WIBBLE_TEST_LOCPRM, const std::string& fname) +{ + if (sys::fs::exists(fname)) + { + std::stringstream ss; + ss << "file '" << fname << "' does exists"; + ept_test_location.fail_test(ss.str()); + } +} + +#if 0 +struct TestFileExists +{ + std::string pathname; + bool inverted; + TestFileExists(const std::string& pathname, bool inverted=false) : pathname(pathname), inverted(inverted) {} + TestFileExists operator!() { return TestFileExists(pathname, !inverted); } + void check(EPT_TEST_LOCPRM) const; +}; +#endif + +void TestFileExists::check(WIBBLE_TEST_LOCPRM) const +{ + if (!inverted) + { + if (sys::fs::exists(pathname)) return; + std::stringstream ss; + ss << "file '" << pathname << "' does not exists"; + ept_test_location.fail_test(ss.str()); + } else { + if (not sys::fs::exists(pathname)) return; + std::stringstream ss; + ss << "file '" << pathname << "' exists"; + ept_test_location.fail_test(ss.str()); + } +} +#endif + +TestRegistry& TestRegistry::get() +{ + static TestRegistry* instance = 0; + if (!instance) + instance = new TestRegistry(); + return *instance; +} + +void TestRegistry::register_test_case(TestCase& test_case) +{ + entries.emplace_back(&test_case); +} + +std::vector<TestCaseResult> TestRegistry::run_tests(TestController& controller) +{ + std::vector<TestCaseResult> res; + for (auto& e: entries) + { + e->register_tests(); + // TODO: filter on e.name + res.emplace_back(std::move(e->run_tests(controller))); + } + return res; +} + +TestCaseResult TestCase::run_tests(TestController& controller) +{ + TestCaseResult res(name); + + if (!controller.test_case_begin(*this, res)) + { + res.skipped = true; + controller.test_case_end(*this, res); + return res; + } + + try { + setup(); + } catch (std::exception& e) { + res.set_setup_failed(e); + controller.test_case_end(*this, res); + return res; + } + + for (auto& m: methods) + { + // TODO: filter on m.name + res.add_test_method(run_test(controller, m)); + } + + try { + teardown(); + } catch (std::exception& e) { + res.set_teardown_failed(e); + } + + controller.test_case_end(*this, res); + return res; +} + +TestMethodResult TestCase::run_test(TestController& controller, TestMethod& method) +{ + TestMethodResult res(name, method.name); + + if (!controller.test_method_begin(method, res)) + { + res.skipped = true; + controller.test_method_end(method, res); + return res; + } + + bool run = true; + try { + method_setup(res); + } catch (std::exception& e) { + res.set_setup_exception(e); + run = false; + } + + if (run) + { + try { + method.test_function(); + } catch (TestFailed& e) { + // Location::fail_test() was called + res.set_failed(e); + } catch (std::exception& e) { + // std::exception was thrown + res.set_exception(e); + } catch (...) { + // An unknown exception was thrown + res.set_unknown_exception(); + } + } + + try { + method_teardown(res); + } catch (std::exception& e) { + res.set_teardown_exception(e); + } + + controller.test_method_end(method, res); + return res; +} + +bool SimpleTestController::test_method_should_run(const std::string& fullname) const +{ + if (!whitelist.empty() && fnmatch(whitelist.c_str(), fullname.c_str(), 0) == FNM_NOMATCH) + return false; + + if (!blacklist.empty() && fnmatch(blacklist.c_str(), fullname.c_str(), 0) != FNM_NOMATCH) + return false; + + return true; +} + +bool SimpleTestController::test_case_begin(const TestCase& test_case, const TestCaseResult& test_case_result) +{ + // Skip test case if all its methods should not run + bool should_run = false; + for (const auto& m : test_case.methods) + should_run |= test_method_should_run(test_case.name + "." + m.name); + if (!should_run) return false; + + fprintf(stdout, "%s: ", test_case.name.c_str()); + fflush(stdout); + return true; +} + +void SimpleTestController::test_case_end(const TestCase& test_case, const TestCaseResult& test_case_result) +{ + if (test_case_result.skipped) + ; + else if (test_case_result.is_success()) + fprintf(stdout, "\n"); + else + fprintf(stdout, "\n"); + fflush(stdout); +} + +bool SimpleTestController::test_method_begin(const TestMethod& test_method, const TestMethodResult& test_method_result) +{ + string name = test_method_result.test_case + "." + test_method.name; + return test_method_should_run(name); +} + +void SimpleTestController::test_method_end(const TestMethod& test_method, const TestMethodResult& test_method_result) +{ + if (test_method_result.skipped) + putc('s', stdout); + else if (test_method_result.is_success()) + putc('.', stdout); + else + putc('x', stdout); + fflush(stdout); +} + +} +} diff --git a/ept/utils/tests.h b/ept/utils/tests.h new file mode 100644 index 0000000..6622cae --- /dev/null +++ b/ept/utils/tests.h @@ -0,0 +1,798 @@ +#ifndef EPT_TESTS_H +#define EPT_TESTS_H + +/** + * @author Enrico Zini <enrico@enricozini.org>, Peter Rockai (mornfall) <me@mornfall.net> + * @brief Utility functions for the unit tests + * + * Copyright (C) 2006--2007 Peter Rockai (mornfall) <me@mornfall.net> + * Copyright (C) 2003--2013 Enrico Zini <enrico@debian.org> + */ + +#include <string> +#include <sstream> +#include <exception> +#include <functional> +#include <vector> + +namespace ept { +namespace tests { +struct LocationInfo; +} +} + +/* + * These global arguments will be shadowed by local variables in functions that + * implement tests. + * + * They are here to act as default root nodes to fulfill method signatures when + * tests are called from outside other tests. + */ +extern const ept::tests::LocationInfo ept_test_location_info; + +namespace ept { +namespace tests { + +/** + * Add information to the test backtrace for the tests run in the current + * scope. + * + * Example usage: + * \code + * test_function(...) + * { + * EPT_TEST_INFO(info); + * for (unsigned i = 0; i < 10; ++i) + * { + * info() << "Iteration #" << i; + * ... + * } + * } + * \endcode + */ +struct LocationInfo : public std::stringstream +{ + LocationInfo() {} + + /** + * Clear the current information and return the output stream to which new + * information can be sent + */ + std::ostream& operator()(); +}; + +/// Information about one stack frame in the test execution stack +struct TestStackFrame +{ + const char* file; + int line; + const char* call; + std::string local_info; + + TestStackFrame(const char* file, int line, const char* call) + : file(file), line(line), call(call) + { + } + + TestStackFrame(const char* file, int line, const char* call, const LocationInfo& local_info) + : file(file), line(line), call(call), local_info(local_info.str()) + { + } + + std::string format() const; + + void format(std::ostream& out) const; +}; + +struct TestStack : public std::vector<TestStackFrame> +{ + using vector::vector; + + /// Return the formatted backtrace for this location + std::string backtrace() const; + + /// Write the formatted backtrace for this location to \a out + void backtrace(std::ostream& out) const; +}; + +/** + * Exception raised when a test assertion fails, normally by + * Location::fail_test + */ +struct TestFailed : public std::exception +{ + std::string message; + TestStack stack; + + TestFailed(const std::exception& e); + + template<typename ...Args> + TestFailed(const std::exception& e, Args&&... args) + : TestFailed(e) + { + add_stack_info(std::forward<Args>(args)...); + } + + TestFailed(const std::string& message) : message(message) {} + + template<typename ...Args> + TestFailed(const std::string& message, Args&&... args) + : TestFailed(message) + { + add_stack_info(std::forward<Args>(args)...); + } + + const char* what() const noexcept override { return message.c_str(); } + + template<typename ...Args> + void add_stack_info(Args&&... args) { stack.emplace_back(std::forward<Args>(args)...); } +}; + +/** + * Use this to declare a local variable with the given name that will be + * picked up by tests as extra local info + */ +#define EPT_TEST_INFO(name) \ + ept::tests::LocationInfo ept_test_location_info; \ + ept::tests::LocationInfo& name = ept_test_location_info + + +/// Test function that ensures that the actual value is true +template<typename A> +void assert_true(const A& actual) +{ + if (actual) return; + std::stringstream ss; + ss << "actual value " << actual << " is not true"; + throw TestFailed(ss.str()); +}; + +void assert_true(std::nullptr_t actual); + +/// Test function that ensures that the actual value is false +template<typename A> +void assert_false(const A& actual) +{ + if (!actual) return; + std::stringstream ss; + ss << "actual value " << actual << " is not false"; + throw TestFailed(ss.str()); +}; + +void assert_false(std::nullptr_t actual); + +/** + * Test function that ensures that the actual value is the same as a reference + * one + */ +template<typename A, typename E> +void assert_equal(const A& actual, const E& expected) +{ + if (actual == expected) return; + std::stringstream ss; + ss << "value '" << actual << "' is different than the expected '" << expected << "'"; + throw TestFailed(ss.str()); +} + +/** + * Test function that ensures that the actual value is different than a + * reference one + */ +template<typename A, typename E> +void assert_not_equal(const A& actual, const E& expected) +{ + if (actual != expected) return; + std::stringstream ss; + ss << "value '" << actual << "' is not different than the expected '" << expected << "'"; + throw TestFailed(ss.str()); +} + +/// Ensure that the actual value is less than the reference value +template<typename A, typename E> +void assert_less(const A& actual, const E& expected) +{ + if (actual < expected) return; + std::stringstream ss; + ss << "value '" << actual << "' is not less than the expected '" << expected << "'"; + throw TestFailed(ss.str()); +} + +/// Ensure that the actual value is less or equal than the reference value +template<typename A, typename E> +void assert_less_equal(const A& actual, const E& expected) +{ + if (actual <= expected) return; + std::stringstream ss; + ss << "value '" << actual << "' is not less than or equals to the expected '" << expected << "'"; + throw TestFailed(ss.str()); +} + +/// Ensure that the actual value is greater than the reference value +template<typename A, typename E> +void assert_greater(const A& actual, const E& expected) +{ + if (actual > expected) return; + std::stringstream ss; + ss << "value '" << actual << "' is not greater than the expected '" << expected << "'"; + throw TestFailed(ss.str()); +} + +/// Ensure that the actual value is greather or equal than the reference value +template<typename A, typename E> +void assert_greater_equal(const A& actual, const E& expected) +{ + if (actual >= expected) return; + std::stringstream ss; + ss << "value '" << actual << "' is not greater than or equals to the expected '" << expected << "'"; + throw TestFailed(ss.str()); +} + +/// Ensure that the string \a actual starts with \a expected +void assert_startswith(const std::string& actual, const std::string& expected); + +/// Ensure that the string \a actual ends with \a expected +void assert_endswith(const std::string& actual, const std::string& expected); + +/// Ensure that the string \a actual contains \a expected +void assert_contains(const std::string& actual, const std::string& expected); + +/// Ensure that the string \a actual does not contain \a expected +void assert_not_contains(const std::string& actual, const std::string& expected); + +/** + * Ensure that the string \a actual matches the extended regular expression + * \a expected. + * + * The syntax is that of extended regular expression (see man regex(7) ). + */ +void assert_re_matches(const std::string& actual, const std::string& expected); + +/** + * Ensure that the string \a actual does not match the extended regular + * expression \a expected. + * + * The syntax is that of extended regular expression (see man regex(7) ). + */ +void assert_not_re_matches(const std::string& actual, const std::string& expected); + + +template<class A> +struct Actual +{ + A _actual; + Actual(const A& actual) : _actual(actual) {} + ~Actual() {} + + void istrue() const { assert_true(_actual); } + void isfalse() const { assert_false(_actual); } + template<typename E> void operator==(const E& expected) const { assert_equal(_actual, expected); } + template<typename E> void operator!=(const E& expected) const { assert_not_equal(_actual, expected); } + template<typename E> void operator<(const E& expected) const { return assert_less(_actual, expected); } + template<typename E> void operator<=(const E& expected) const { return assert_less_equal(_actual, expected); } + template<typename E> void operator>(const E& expected) const { return assert_greater(_actual, expected); } + template<typename E> void operator>=(const E& expected) const { return assert_greater_equal(_actual, expected); } +}; + +struct ActualCString +{ + const char* _actual; + ActualCString(const char* s) : _actual(s) {} + + void istrue() const { return assert_true(_actual); } + void isfalse() const { return assert_false(_actual); } + void operator==(const char* expected) const; + void operator==(const std::string& expected) const; + void operator!=(const char* expected) const; + void operator!=(const std::string& expected) const; + void operator<(const std::string& expected) const; + void operator<=(const std::string& expected) const; + void operator>(const std::string& expected) const; + void operator>=(const std::string& expected) const; + void startswith(const std::string& expected) const; + void endswith(const std::string& expected) const; + void contains(const std::string& expected) const; + void not_contains(const std::string& expected) const; + void matches(const std::string& re) const; + void not_matches(const std::string& re) const; +}; + +struct ActualStdString : public Actual<std::string> +{ + ActualStdString(const std::string& s) : Actual<std::string>(s) {} + + void startswith(const std::string& expected) const; + void endswith(const std::string& expected) const; + void contains(const std::string& expected) const; + void not_contains(const std::string& expected) const; + void matches(const std::string& re) const; + void not_matches(const std::string& re) const; +}; + +struct ActualDouble : public Actual<double> +{ + using Actual::Actual; + + void almost_equal(double expected, unsigned places) const; + void not_almost_equal(double expected, unsigned places) const; +}; + +template<typename A> +inline Actual<A> actual(const A& actual) { return Actual<A>(actual); } +inline ActualCString actual(const char* actual) { return ActualCString(actual); } +inline ActualCString actual(char* actual) { return ActualCString(actual); } +inline ActualStdString actual(const std::string& actual) { return ActualStdString(actual); } +inline ActualDouble actual(double actual) { return ActualDouble(actual); } + +struct ActualFunction : public Actual<std::function<void()>> +{ + using Actual::Actual; + + void throws(const std::string& what_match) const; +}; + +inline ActualFunction actual_function(std::function<void()> actual) { return ActualFunction(actual); } + + +/** + * Run the given command, raising TestFailed with the appropriate backtrace + * information if it threw an exception. + * + * If the command raises TestFailed, it adds the current stack to its stack + * information. + */ +#define wassert(...) \ + do { try { \ + __VA_ARGS__ ; \ + } catch (TestFailed& e) { \ + e.add_stack_info(__FILE__, __LINE__, #__VA_ARGS__, ept_test_location_info); \ + throw; \ + } catch (std::exception& e) { \ + throw TestFailed(e, __FILE__, __LINE__, #__VA_ARGS__, ept_test_location_info); \ + } } while(0) + +/** + * Call a function returning its result, and raising TestFailed with the + * appropriate backtrace information if it threw an exception. + * + * If the function raises TestFailed, it adds the current stack to its stack + * information. + */ +#define wcallchecked(func) \ + [&]() { try { \ + return func; \ + } catch (TestFailed& e) { \ + e.add_stack_info(__FILE__, __LINE__, #func, ept_test_location_info); \ + throw; \ + } catch (std::exception& e) { \ + throw TestFailed(e, __FILE__, __LINE__, #func, ept_test_location_info); \ + } }() + + +struct TestCase; + +/** + * Result of running a test method. + */ +struct TestMethodResult +{ + /// Name of the test case + std::string test_case; + + /// Name of the test method + std::string test_method; + + /// If non-empty, the test failed with this error + std::string error_message; + + /// Stack frame of where the error happened + TestStack error_stack; + + /// If non-empty, the test raised an exception and this is its type ID + std::string exception_typeid; + + /// True if the test has been skipped + bool skipped = false; + + + TestMethodResult(const std::string& test_case, const std::string& test_method) + : test_case(test_case), test_method(test_method) {} + + void set_failed(TestFailed& e) + { + error_message = e.what(); + error_stack = e.stack; + if (error_message.empty()) + error_message = "test failed with an empty error message"; + } + + void set_exception(std::exception& e) + { + error_message = e.what(); + if (error_message.empty()) + error_message = "test threw an exception with an empty error message"; + exception_typeid = typeid(e).name(); + } + + void set_unknown_exception() + { + error_message = "unknown exception caught"; + } + + void set_setup_exception(std::exception& e) + { + error_message = "[setup failed: "; + error_message += e.what(); + error_message += "]"; + } + + void set_teardown_exception(std::exception& e) + { + error_message = "[teardown failed: "; + error_message += e.what(); + error_message += "]"; + } + + bool is_success() const + { + return error_message.empty(); + } +}; + +/** + * Result of running a whole test case + */ +struct TestCaseResult +{ + /// Name of the test case + std::string test_case; + /// Outcome of all the methods that have been run + std::vector<TestMethodResult> methods; + /// Set to a non-empty string if the setup method of the test case failed + std::string fail_setup; + /// Set to a non-empty string if the teardown method of the test case + /// failed + std::string fail_teardown; + /// Set to true if this test case has been skipped + bool skipped = false; + + TestCaseResult(const std::string& test_case) : test_case(test_case) {} + + void set_setup_failed() + { + fail_setup = "test case setup method threw an unknown exception"; + } + + void set_setup_failed(std::exception& e) + { + fail_setup = "test case setup method threw an exception: "; + fail_setup += e.what(); + } + + void set_teardown_failed() + { + fail_teardown = "test case teardown method threw an unknown exception"; + } + + void set_teardown_failed(std::exception& e) + { + fail_teardown = "test case teardown method threw an exception: "; + fail_teardown += e.what(); + } + + void add_test_method(TestMethodResult&& e) + { + methods.emplace_back(std::move(e)); + } + + bool is_success() const + { + if (!fail_setup.empty() || !fail_teardown.empty()) return false; + for (const auto& m: methods) + if (!m.is_success()) + return false; + return true; + } +}; + +struct TestCase; +struct TestCaseResult; +struct TestMethod; +struct TestMethodResult; + +/** + * Abstract interface for the objects that supervise test execution. + * + * This can be used for printing progress, or to skip test methods or test + * cases. + */ +struct TestController +{ + virtual ~TestController() {} + + /** + * Called before running a test case. + * + * @returns true if the test case should be run, false if it should be skipped + */ + virtual bool test_case_begin(const TestCase& test_case, const TestCaseResult& test_case_result) { return true; } + + /** + * Called after running a test case. + */ + virtual void test_case_end(const TestCase& test_case, const TestCaseResult& test_case_result) {} + + /** + * Called before running a test method. + * + * @returns true if the test method should be run, false if it should be skipped + */ + virtual bool test_method_begin(const TestMethod& test_method, const TestMethodResult& test_method_result) { return true; } + + /** + * Called after running a test method. + */ + virtual void test_method_end(const TestMethod& test_method, const TestMethodResult& test_method_result) {} +}; + +/** + * Simple default implementation of TestController. + * + * It does progress printing to stdout and basic glob-based test method + * filtering. + */ +struct SimpleTestController : public TestController +{ + /// Any method not matching this glob expression will not be run + std::string whitelist; + + /// Any method matching this glob expression will not be run + std::string blacklist; + + bool test_case_begin(const TestCase& test_case, const TestCaseResult& test_case_result) override; + void test_case_end(const TestCase& test_case, const TestCaseResult& test_case_result) override; + bool test_method_begin(const TestMethod& test_method, const TestMethodResult& test_method_result) override; + void test_method_end(const TestMethod& test_method, const TestMethodResult& test_method_result) override; + + bool test_method_should_run(const std::string& fullname) const; +}; + + +/** + * Test registry. + * + * It collects information about all known test cases and takes care of running + * them. + */ +struct TestRegistry +{ + /// All known test cases + std::vector<TestCase*> entries; + + /** + * Register a new test case. + * + * No memory management is done: test_case needs to exist for the whole + * lifetime of TestRegistry. + */ + void register_test_case(TestCase& test_case); + + /** + * Run all the registered tests using the given controller + */ + std::vector<TestCaseResult> run_tests(TestController& controller); + + /// Get the singleton instance of TestRegistry + static TestRegistry& get(); +}; + +/** + * Test method information + */ +struct TestMethod +{ + /// Name of the test method + std::string name; + + /// Main body of the test method + std::function<void()> test_function; + + TestMethod(const std::string& name, std::function<void()> test_function) + : name(name), test_function(test_function) {} +}; + + +/** + * Test case collecting several test methods, and self-registering with the + * singleton instance of TestRegistry. + */ +struct TestCase +{ + /// Name of the test case + std::string name; + + /// All registered test methods + std::vector<TestMethod> methods; + + TestCase(const std::string& name) + : name(name) + { + TestRegistry::get().register_test_case(*this); + } + virtual ~TestCase() {} + + /** + * This will be called before running the test case, to populate it with + * its test methods. + * + * This needs to be reimplemented with a function that will mostly be a + * sequence of calls to add_method(). + */ + virtual void register_tests() = 0; + + /** + * Set up the test case before it is run. + */ + virtual void setup() {} + + /** + * Clean up after the test case is run + */ + virtual void teardown() {} + + /** + * Set up before the test method is run + */ + virtual void method_setup(TestMethodResult&) {} + + /** + * Clean up after the test method is run + */ + virtual void method_teardown(TestMethodResult&) {} + + /** + * Call setup(), run all the tests that have been registered, then + * call teardown(). + * + * Exceptions in setup() and teardown() are caught and reported in + * TestCaseResult. Test are run using run_test(). + */ + virtual TestCaseResult run_tests(TestController& controller); + + /** + * Run a test method. + * + * Call method_setup(), run all the tests that have been registered, then + * call method_teardown(). + * + * Exceptions thrown by the test method are caught and reported in + * TestMethodResult. + * + * Exceptions in method_setup() and method_teardown() are caught and + * reported in TestMethodResult. + */ + virtual TestMethodResult run_test(TestController& controller, TestMethod& method); + + /** + * Register a new test method + */ + template<typename ...Args> + void add_method(const std::string& name, std::function<void()> test_function) + { + methods.emplace_back(name, test_function); + } + + /** + * Register a new test method + */ + template<typename ...Args> + void add_method(const std::string& name, std::function<void()> test_function, Args&&... args) + { + methods.emplace_back(name, test_function, std::forward<Args>(args)...); + } + + /** + * Register a new test metheod, with arguments. + * + * Any extra arguments to the function will be passed to the test method. + */ + template<typename FUNC, typename ...Args> + void add_method(const std::string& name, FUNC test_function, Args&&... args) + { + methods.emplace_back(name, [test_function, args...]() { test_function(args...); }); + } +}; + + +/** + * Base class for test fixtures. + * + * A fixture will have a constructor and a destructor to do setup/teardown, and + * a reset() function to be called inbetween tests. + * + * Fixtures do not need to descend from Fixture: this implementation is + * provided as a default for tests that do not need one, or as a base for + * fixtures that do not need reset(). + */ +struct Fixture +{ + virtual ~Fixture() {} + + // Called before each test + virtual void test_setup() {} + + // Called after each test + virtual void test_teardown() {} +}; + +/** + * Test case that includes a fixture + */ +template<typename FIXTURE> +struct FixtureTestCase : public TestCase +{ + typedef FIXTURE Fixture; + + Fixture* fixture = 0; + std::function<Fixture*()> make_fixture; + + template<typename... Args> + FixtureTestCase(const std::string& name, Args... args) + : TestCase(name) + { + make_fixture = [=]() { return new Fixture(args...); }; + } + + void setup() override + { + TestCase::setup(); + fixture = make_fixture(); + } + + void teardown() override + { + delete fixture; + fixture = 0; + TestCase::teardown(); + } + + void method_setup(TestMethodResult& mr) override + { + TestCase::method_setup(mr); + if (fixture) fixture->test_setup(); + } + + void method_teardown(TestMethodResult& mr) override + { + if (fixture) fixture->test_teardown(); + TestCase::method_teardown(mr); + } + + /** + * Add a method that takes a reference to the fixture as argument. + * + * Any extra arguments to the function will be passed to the test method + * after the fixture. + */ + template<typename FUNC, typename ...Args> + void add_method(const std::string& name, FUNC test_function, Args&&... args) + { + methods.emplace_back(name, [this, test_function, args...] { test_function(*fixture, args...); }); + } +}; + +#if 0 + struct Test + { + std::string name; + std::function<void()> test_func; + }; + + /// Add tests to the test case + virtual void add_tests() {} +#endif + + +} +} + +#endif |