From 335c2171146b4e2db1fdc2f450107bbaaf6c7927 Mon Sep 17 00:00:00 2001 From: whitequark Date: Thu, 9 Mar 2017 14:44:32 +0000 Subject: [PATCH] Collect together and rigorously test all our ad-hoc path functions. --- src/CMakeLists.txt | 8 +- src/platform/platform.cpp | 335 ++++++++++++++++++++++++++++++++++++++ src/platform/platform.h | 51 ++++++ src/solvespace.h | 3 + test/CMakeLists.txt | 1 + test/core/path/test.cpp | 245 ++++++++++++++++++++++++++++ test/harness.cpp | 26 ++- test/harness.h | 15 +- 8 files changed, 674 insertions(+), 10 deletions(-) create mode 100644 src/platform/platform.cpp create mode 100644 src/platform/platform.h create mode 100644 test/core/path/test.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 060f663..cff58c2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -33,10 +33,12 @@ set(libslvs_SOURCES expr.cpp constraint.cpp constrainteq.cpp - system.cpp) + system.cpp + platform/platform.cpp) set(libslvs_HEADERS - solvespace.h) + solvespace.h + platform/platform.h) add_library(slvs SHARED ${libslvs_SOURCES} @@ -139,6 +141,7 @@ set(solvespace_core_HEADERS sketch.h solvespace.h ui.h + platform/platform.h render/render.h render/gl2shader.h srf/surface.h) @@ -180,6 +183,7 @@ set(solvespace_core_SOURCES undoredo.cpp util.cpp view.cpp + platform/platform.cpp render/render.cpp render/render2d.cpp srf/boolean.cpp diff --git a/src/platform/platform.cpp b/src/platform/platform.cpp new file mode 100644 index 0000000..e61f37e --- /dev/null +++ b/src/platform/platform.cpp @@ -0,0 +1,335 @@ +//----------------------------------------------------------------------------- +// Common platform-dependent functionality. +// +// Copyright 2017 whitequark +//----------------------------------------------------------------------------- +#if defined(__APPLE__) +# include +#endif +#include "solvespace.h" +#if defined(WIN32) +# include +#else +# include +#endif + +namespace SolveSpace { + +using namespace Platform; + +//----------------------------------------------------------------------------- +// Utility functions. +//----------------------------------------------------------------------------- + +static std::vector Split(const std::string &joined, char separator) { + std::vector parts; + + size_t oldpos = 0, pos = 0; + while(true) { + oldpos = pos; + pos = joined.find(separator, pos); + if(pos == std::string::npos) break; + parts.push_back(joined.substr(oldpos, pos - oldpos)); + pos += 1; + } + + if(oldpos != joined.length() - 1) { + parts.push_back(joined.substr(oldpos)); + } + + return parts; +} + +static std::string Concat(const std::vector &parts, char separator) { + std::string joined; + + bool first = true; + for(auto &part : parts) { + if(!first) joined += separator; + joined += part; + first = false; + } + + return joined; +} + +//----------------------------------------------------------------------------- +// Path manipulation. +//----------------------------------------------------------------------------- + +#if defined(WIN32) +const char SEPARATOR = '\\'; +#else +const char SEPARATOR = '/'; +#endif + +Path Path::From(std::string raw) { + Path path = { raw }; + return path; +} + +Path Path::CurrentDirectory() { +#if defined(WIN32) + // On Windows, ssfopen needs an absolute UNC path proper, so get that. + std::wstring rawW; + rawW.resize(GetCurrentDirectoryW(0, NULL)); + DWORD length = GetCurrentDirectoryW((int)rawW.length(), &rawW[0]); + ssassert(length > 0 && length == rawW.length() - 1, "Cannot get current directory"); + rawW.resize(length); + return From(Narrow(rawW)); +#else + char *raw = getcwd(NULL, 0); + ssassert(raw != NULL, "Cannot get current directory"); + Path path = From(raw); + free(raw); + return path; +#endif +} + +std::string Path::FileName() const { + std::string fileName = raw; + size_t slash = fileName.rfind(SEPARATOR); + if(slash != std::string::npos) { + fileName = fileName.substr(slash + 1); + } + return fileName; +} + +std::string Path::FileStem() const { + std::string baseName = FileName(); + size_t dot = baseName.rfind('.'); + if(dot != std::string::npos) { + baseName = baseName.substr(0, dot); + } + return baseName; +} + +std::string Path::Extension() const { + size_t dot = raw.rfind('.'); + if(dot != std::string::npos) { + return raw.substr(dot + 1); + } + return ""; +} + +bool Path::HasExtension(std::string theirExt) const { + std::string ourExt = Extension(); + std::transform(ourExt.begin(), ourExt.end(), ourExt.begin(), ::tolower); + std::transform(theirExt.begin(), theirExt.end(), theirExt.begin(), ::tolower); + return ourExt == theirExt; +} + +Path Path::WithExtension(std::string ext) const { + Path withExt = *this; + size_t dot = withExt.raw.rfind('.'); + if(dot != std::string::npos) { + withExt.raw.erase(dot); + } + withExt.raw += "."; + withExt.raw += ext; + return withExt; +} + +static void FindPrefix(const std::string &raw, size_t *pos) { + *pos = std::string::npos; +#if defined(WIN32) + if(raw.size() >= 7 && raw[2] == '?' && raw[3] == '\\' && + isalpha(raw[4]) && raw[5] == ':' && raw[6] == '\\') { + *pos = 7; + } else if(raw.size() >= 3 && isalpha(raw[0]) && raw[1] == ':' && raw[2] == '\\') { + *pos = 3; + } else if(raw.size() >= 2 && raw[0] == '\\' && raw[1] == '\\') { + size_t slashAt = raw.find('\\', 2); + if(slashAt != std::string::npos) { + *pos = raw.find('\\', slashAt + 1); + } + } +#else + if(raw.size() >= 1 && raw[0] == '/') { + *pos = 1; + } +#endif +} + +bool Path::IsAbsolute() const { + size_t pos; + FindPrefix(raw, &pos); + return pos != std::string::npos; +} + +// Removes one component from the end of the path. +// Returns an empty path if the path consists only of a root. +Path Path::Parent() const { + Path parent = { raw }; + if(!parent.raw.empty() && parent.raw.back() == SEPARATOR) { + parent.raw.pop_back(); + } + size_t slash = parent.raw.rfind(SEPARATOR); + if(slash != std::string::npos) { + parent.raw = parent.raw.substr(0, slash + 1); + } else { + parent.raw.clear(); + } + if(IsAbsolute() && !parent.IsAbsolute()) { + return From(""); + } + return parent; +} + +// Concatenates a component to this path. +// Returns an empty path if this path or the component is empty. +Path Path::Join(const std::string &component) const { + ssassert(component.find(SEPARATOR) == std::string::npos, + "Use the Path::Join(const Path &) overload to append an entire path"); + return Join(Path::From(component)); +} + +// Concatenates a relative path to this path. +// Returns an empty path if either path is empty, or the other path is absolute. +Path Path::Join(const Path &other) const { + if(IsEmpty() || other.IsEmpty() || other.IsAbsolute()) { + return From(""); + } + + Path joined = { raw }; + if(joined.raw.back() != SEPARATOR) { + joined.raw += SEPARATOR; + } + joined.raw += other.raw; + return joined; +} + +// Expands the "." and ".." components in this path. +// On Windows, additionally prepends the UNC prefix to absolute paths without one. +// Returns an empty path if a ".." component would escape from the root. +Path Path::Expand(bool fromCurrentDirectory) const { + Path source; + Path expanded; + + if(fromCurrentDirectory && !IsAbsolute()) { + source = CurrentDirectory().Join(*this); + } else { + source = *this; + } + + size_t splitAt; + FindPrefix(source.raw, &splitAt); + if(splitAt != std::string::npos) { + expanded.raw = source.raw.substr(0, splitAt); + } else { + splitAt = 0; + } + + std::vector expandedComponents; + for(std::string component : Split(source.raw.substr(splitAt), SEPARATOR)) { + if(component == ".") { + // skip + } else if(component == "..") { + if(!expandedComponents.empty()) { + expandedComponents.pop_back(); + } else { + return From(""); + } + } else if(!component.empty()) { + expandedComponents.push_back(component); + } + } + + if(expanded.IsEmpty()) { + if(expandedComponents.empty()) { + expandedComponents.push_back("."); + } + expanded = From(Concat(expandedComponents, SEPARATOR)); + } else if(!expandedComponents.empty()) { + expanded = expanded.Join(From(Concat(expandedComponents, SEPARATOR))); + } + +#if defined(WIN32) + if(expanded.IsAbsolute() && expanded.raw.substr(0, 2) != "\\\\") { + expanded.raw = "\\\\?\\" + expanded.raw; + } +#endif + + return expanded; +} + +static std::string FilesystemNormalize(const std::string &str) { +#if defined(WIN32) + std::wstring strW = Widen(str); + std::transform(strW.begin(), strW.end(), strW.begin(), towlower); + return Narrow(strW); +#elif defined(__APPLE__) + CFMutableStringRef cfStr = + CFStringCreateMutableCopy(NULL, 0, + CFStringCreateWithBytesNoCopy(NULL, (const UInt8*)str.data(), str.size(), + kCFStringEncodingUTF8, /*isExternalRepresentation=*/false, kCFAllocatorNull)); + CFStringLowercase(cfStr, NULL); + std::string normalizedStr; + normalizedStr.resize(CFStringGetMaximumSizeOfFileSystemRepresentation(cfStr)); + CFStringGetFileSystemRepresentation(cfStr, &normalizedStr[0], normalizedStr.size()); + return normalizedStr; +#else + return str; +#endif +} + +bool Path::Equals(const Path &other) const { + return FilesystemNormalize(raw) == FilesystemNormalize(other.raw); +} + +// Returns a relative path from a given base path. +// Returns an empty path if any of the paths is not absolute, or +// if they belong to different roots, or +// if they cannot be expanded. +Path Path::RelativeTo(const Path &base) const { + Path expanded = Expand(); + Path baseExpanded = base.Expand(); + if(!(expanded.IsAbsolute() && baseExpanded.IsAbsolute())){ + return From(""); + } + + size_t splitAt; + FindPrefix(expanded.raw, &splitAt); + size_t baseSplitAt; + FindPrefix(baseExpanded.raw, &baseSplitAt); + if(FilesystemNormalize(expanded.raw.substr(0, splitAt)) != + FilesystemNormalize(baseExpanded.raw.substr(0, splitAt))) { + return From(""); + } + + std::vector components = + Split(expanded.raw.substr(splitAt), SEPARATOR); + std::vector baseComponents = + Split(baseExpanded.raw.substr(baseSplitAt), SEPARATOR); + size_t common; + for(common = 0; common < baseComponents.size() && + common < components.size(); common++) { + if(FilesystemNormalize(baseComponents[common]) != + FilesystemNormalize(components[common])) { + break; + } + } + + std::vector resultComponents; + for(size_t i = common; i < baseComponents.size(); i++) { + resultComponents.push_back(".."); + } + resultComponents.insert(resultComponents.end(), + components.begin() + common, components.end()); + if(resultComponents.empty()) { + resultComponents.push_back("."); + } + return From(Concat(resultComponents, SEPARATOR)); +} + +Path Path::FromPortable(const std::string &repr) { + return From(Concat(Split(repr, '/'), SEPARATOR)); +} + +std::string Path::ToPortable() const { + ssassert(!IsAbsolute(), "absolute paths cannot be made portable"); + + return Concat(Split(raw, SEPARATOR), '/'); +} + +} diff --git a/src/platform/platform.h b/src/platform/platform.h new file mode 100644 index 0000000..3768885 --- /dev/null +++ b/src/platform/platform.h @@ -0,0 +1,51 @@ +//----------------------------------------------------------------------------- +// Common platform-dependent functionality. +// +// Copyright 2017 whitequark +//----------------------------------------------------------------------------- + +#ifndef SOLVESPACE_PLATFORM_H +#define SOLVESPACE_PLATFORM_H + +namespace Platform { + +// A filesystem path, respecting the conventions of the current platform. +// Transformation functions return an empty path on error. +class Path { +public: + std::string raw; + + static Path From(std::string raw); + static Path CurrentDirectory(); + + void Clear() { raw.clear(); } + + bool Equals(const Path &other) const; + bool IsEmpty() const { return raw.empty(); } + bool IsAbsolute() const; + bool HasExtension(std::string ext) const; + + std::string FileName() const; + std::string FileStem() const; + std::string Extension() const; + + Path WithExtension(std::string ext) const; + Path Parent() const; + Path Join(const std::string &component) const; + Path Join(const Path &other) const; + Path Expand(bool fromCurrentDirectory = false) const; + Path RelativeTo(const Path &base) const; + + // Converting to and from a platform-independent representation + // (conventionally, the Unix one). + static Path FromPortable(const std::string &repr); + std::string ToPortable() const; +}; + +struct PathLess { + bool operator()(const Path &a, const Path &b) const { return a.raw < b.raw; } +}; + +} + +#endif diff --git a/src/solvespace.h b/src/solvespace.h index 797aed2..33d9381 100644 --- a/src/solvespace.h +++ b/src/solvespace.h @@ -141,6 +141,9 @@ enum class ContextCommand : uint32_t; //================ // From the platform-specific code. + +#include "platform/platform.h" + #if defined(WIN32) #define PATH_SEP "\\" #else diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 88a4c13..bac0fa7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -13,6 +13,7 @@ set(testsuite_SOURCES harness.cpp core/expr/test.cpp core/locale/test.cpp + core/path/test.cpp constraint/points_coincident/test.cpp constraint/pt_pt_distance/test.cpp constraint/pt_plane_distance/test.cpp diff --git a/test/core/path/test.cpp b/test/core/path/test.cpp new file mode 100644 index 0000000..9263956 --- /dev/null +++ b/test/core/path/test.cpp @@ -0,0 +1,245 @@ +#include "harness.h" + +using Platform::Path; + +#if defined(WIN32) +#define S "\\" +#define R "C:" +#define U "\\\\?\\C:" +#else +#define S "/" +#define R "" +#define U "" +#endif + +TEST_CASE(from_raw) { + Path path = Path::From("/foo"); + CHECK_EQ_STR(path.raw, "/foo"); +} + +#if defined(WIN32) || defined(__APPLE__) +TEST_CASE(equals_win32_apple) { + CHECK_TRUE(Path::From(R S "foo").Equals(Path::From(R S "foo"))); + CHECK_TRUE(Path::From(R S "foo").Equals(Path::From(R S "FOO"))); + CHECK_FALSE(Path::From(R S "foo").Equals(Path::From(R S "bar"))); +} +#else +TEST_CASE(equals_unix) { + CHECK_TRUE(Path::From(R S "foo").Equals(Path::From(R S "foo"))); + CHECK_FALSE(Path::From(R S "foo").Equals(Path::From(R S "FOO"))); + CHECK_FALSE(Path::From(R S "foo").Equals(Path::From(R S "bar"))); +} +#endif + +#if defined(WIN32) +TEST_CASE(is_absolute_win32) { + CHECK_TRUE(Path::From("c:\\foo").IsAbsolute()); + CHECK_TRUE(Path::From("\\\\?\\c:\\").IsAbsolute()); + CHECK_TRUE(Path::From("\\\\server\\share\\").IsAbsolute()); + CHECK_FALSE(Path::From("c:/foo").IsAbsolute()); + CHECK_FALSE(Path::From("c:foo").IsAbsolute()); + CHECK_FALSE(Path::From("\\\\?").IsAbsolute()); + CHECK_FALSE(Path::From("\\\\server\\").IsAbsolute()); + CHECK_FALSE(Path::From("\\\\?\\c:").IsAbsolute()); + CHECK_FALSE(Path::From("\\\\server\\share").IsAbsolute()); + CHECK_FALSE(Path::From("foo").IsAbsolute()); + CHECK_FALSE(Path::From("/foo").IsAbsolute()); +} +#else +TEST_CASE(is_absolute_unix) { + CHECK_TRUE(Path::From("/foo").IsAbsolute()); + CHECK_FALSE(Path::From("c:/foo").IsAbsolute()); + CHECK_FALSE(Path::From("c:\\foo").IsAbsolute()); + CHECK_FALSE(Path::From("\\\\?\\foo").IsAbsolute()); + CHECK_FALSE(Path::From("c:foo").IsAbsolute()); + CHECK_FALSE(Path::From("foo").IsAbsolute()); +} +#endif + +TEST_CASE(has_extension) { + CHECK_TRUE(Path::From("foo.bar").HasExtension("bar")); + CHECK_TRUE(Path::From("foo.bar").HasExtension("BAR")); + CHECK_TRUE(Path::From("foo.bAr").HasExtension("BaR")); + CHECK_TRUE(Path::From("foo.bar").HasExtension("bar")); + CHECK_FALSE(Path::From("foo.bar").HasExtension("baz")); +} + +TEST_CASE(file_name) { + CHECK_EQ_STR(Path::From("foo").FileName(), "foo"); + CHECK_EQ_STR(Path::From("foo" S "bar").FileName(), "bar"); +} + +TEST_CASE(file_stem) { + CHECK_EQ_STR(Path::From("foo").FileStem(), "foo"); + CHECK_EQ_STR(Path::From("foo" S "bar").FileStem(), "bar"); + CHECK_EQ_STR(Path::From("foo.ext").FileStem(), "foo"); + CHECK_EQ_STR(Path::From("foo" S "bar.ext").FileStem(), "bar"); +} + +TEST_CASE(extension) { + CHECK_EQ_STR(Path::From("foo").Extension(), ""); + CHECK_EQ_STR(Path::From("foo.bar").Extension(), "bar"); + CHECK_EQ_STR(Path::From("foo.bar.baz").Extension(), "baz"); +} + +TEST_CASE(with_extension) { + CHECK_EQ_STR(Path::From("foo.bar").WithExtension("baz").raw, "foo.baz"); + CHECK_EQ_STR(Path::From("foo").WithExtension("baz").raw, "foo.baz"); +} + +TEST_CASE(parent) { + Path path; + path = Path::From("foo" S "bar"); + CHECK_EQ_STR(path.Parent().raw, "foo" S); + path = Path::From("foo" S "bar" S); + CHECK_EQ_STR(path.Parent().raw, "foo" S); + path = Path::From(R S "foo" S "bar"); + CHECK_EQ_STR(path.Parent().raw, R S "foo" S); + path = Path::From(R S "foo"); + CHECK_EQ_STR(path.Parent().raw, R S); + + path = Path::From(""); + CHECK_TRUE(path.Parent().IsEmpty()); + path = Path::From("foo"); + CHECK_TRUE(path.Parent().IsEmpty()); + path = Path::From("foo" S); + CHECK_TRUE(path.Parent().IsEmpty()); + path = Path::From(R S); + CHECK_TRUE(path.Parent().IsEmpty()); +} + +#if defined(WIN32) +TEST_CASE(parent_win32) { + Path path; + path = Path::From(U S); + CHECK_TRUE(path.Parent().IsEmpty()); +} +#endif + +TEST_CASE(join) { + Path path; + path = Path::From("foo"); + CHECK_EQ_STR(path.Join(Path::From("bar")).raw, "foo" S "bar"); + path = Path::From("foo" S); + CHECK_EQ_STR(path.Join(Path::From("bar")).raw, "foo" S "bar"); + + path = Path::From(""); + CHECK_TRUE(path.Join(Path::From("bar")).IsEmpty()); + path = Path::From("foo"); + CHECK_TRUE(path.Join(Path::From("")).IsEmpty()); + path = Path::From("foo"); + CHECK_TRUE(path.Join(Path::From(R S "bar")).IsEmpty()); +} + +TEST_CASE(expand) { + Path path; + path = Path::From("foo"); + CHECK_EQ_STR(path.Expand().raw, "foo"); + path = Path::From("foo" S "bar"); + CHECK_EQ_STR(path.Expand().raw, "foo" S "bar"); + path = Path::From("foo" S); + CHECK_EQ_STR(path.Expand().raw, "foo"); + path = Path::From("foo" S "."); + CHECK_EQ_STR(path.Expand().raw, "foo"); + path = Path::From("foo" S "." S "bar"); + CHECK_EQ_STR(path.Expand().raw, "foo" S "bar"); + path = Path::From("foo" S ".." S "bar"); + CHECK_EQ_STR(path.Expand().raw, "bar"); + path = Path::From("foo" S ".."); + CHECK_EQ_STR(path.Expand().raw, "."); + path = Path::From(R S "foo" S ".."); + CHECK_EQ_STR(path.Expand().raw, U S); + path = Path::From(R S); + CHECK_EQ_STR(path.Expand().raw, U S); + + path = Path::From(R S ".."); + CHECK_TRUE(path.Expand().IsEmpty()); + path = Path::From(R S ".." S "foo"); + CHECK_TRUE(path.Expand().IsEmpty()); + path = Path::From(".."); + CHECK_TRUE(path.Expand().IsEmpty()); +} + +#if defined(WIN32) +TEST_CASE(expand_win32) { + Path path; + path = Path::From(R S "foo"); + CHECK_EQ_STR(path.Expand().raw, U S "foo"); + path = Path::From(U S "foo"); + CHECK_EQ_STR(path.Expand().raw, U S "foo"); +} +#endif + +TEST_CASE(expand_from_cwd) { + Path cwd = Path::CurrentDirectory().Expand(); + + Path path; + path = Path::From(R S "foo"); + CHECK_EQ_STR(path.Expand(/*fromCurrentDirectory=*/true).raw, + U S "foo"); + path = Path::From("foo" S "bar"); + CHECK_EQ_STR(path.Expand(/*fromCurrentDirectory=*/true).raw, + cwd.raw + S "foo" S "bar"); + path = Path::From(".." S "bar"); + CHECK_EQ_STR(path.Expand(/*fromCurrentDirectory=*/true).raw, + cwd.Parent().raw + "bar"); +} + +TEST_CASE(relative_to) { + Path base; + base = Path::From(R S "foo" S "bar"); + CHECK_EQ_STR(Path::From(R S "foo").RelativeTo(base).raw, + ".."); + base = Path::From(R S "foo" S "bar"); + CHECK_EQ_STR(Path::From(R S "foo" S "baz").RelativeTo(base).raw, + ".." S "baz"); + base = Path::From(R S "foo" S "bar"); + CHECK_EQ_STR(Path::From(R S "foo" S "bar" S "quux").RelativeTo(base).raw, + "quux"); + base = Path::From(R S "foo" S "bar"); + CHECK_EQ_STR(Path::From(R S "foo" S "bar").RelativeTo(base).raw, + "."); + + base = Path::From("foo"); + CHECK_TRUE(Path::From(R S "foo" S "bar").RelativeTo(base).IsEmpty()); + base = Path::From(R S "foo" S "bar"); + CHECK_TRUE(Path::From("foo").RelativeTo(base).IsEmpty()); +} + +#if defined(WIN32) +TEST_CASE(relative_to_win32) { + Path base; + base = Path::From("C:\\foo"); + CHECK_EQ_STR(Path::From("\\\\?\\C:\\bar").RelativeTo(base).raw, + "..\\bar"); + base = Path::From("C:\\foo"); + CHECK_EQ_STR(Path::From("c:\\FOO").RelativeTo(base).raw, + "."); + + base = Path::From("C:\\foo"); + CHECK_TRUE(Path::From("D:\\bar").RelativeTo(base).IsEmpty()); + CHECK_TRUE(Path::From("\\\\server\\share\\bar").RelativeTo(base).IsEmpty()); +} +#elif defined(__APPLE__) +TEST_CASE(relative_to_apple) { + Path base; + base = Path::From("/users/foo"); + CHECK_EQ_STR(Path::From("/Users/FOO").RelativeTo(base).raw, + "."); +} +#else +TEST_CASE(relative_to_unix) { + Path base; + base = Path::From("/users/foo"); + CHECK_EQ_STR(Path::From("/Users/FOO").RelativeTo(base).raw, + "../../Users/FOO"); +} +#endif + +TEST_CASE(from_portable) { + CHECK_EQ_STR(Path::FromPortable("foo/bar").raw, "foo" S "bar"); +} + +TEST_CASE(to_portable) { + CHECK_EQ_STR(Path::From("foo" S "bar").ToPortable(), "foo/bar"); +} diff --git a/test/harness.cpp b/test/harness.cpp index 901697c..79465c7 100644 --- a/test/harness.cpp +++ b/test/harness.cpp @@ -174,10 +174,25 @@ std::string Test::Helper::GetAssetPath(std::string testFile, std::string assetNa return PathSepUnixToPlatform(HostRoot() + "/" + testFile + assetName); } -bool Test::Helper::CheckTrue(const char *file, int line, const char *expr, bool result) { - if(!RecordCheck(result)) { - PrintFailure(file, line, - ssprintf("(%s) == %s", expr, result ? "true" : "false")); +bool Test::Helper::CheckBool(const char *file, int line, const char *expr, bool value, + bool reference) { + if(!RecordCheck(value == reference)) { + std::string msg = ssprintf("(%s) = %s ≠ %s", expr, + value ? "true" : "false", + reference ? "true" : "false"); + PrintFailure(file, line, msg); + return false; + } else { + return true; + } +} + +bool Test::Helper::CheckEqualString(const char *file, int line, const char *valueExpr, + const std::string &value, const std::string &reference) { + if(!RecordCheck(value == reference)) { + std::string msg = ssprintf("(%s) = \"%s\" ≠ \"%s\"", valueExpr, + value.c_str(), reference.c_str()); + PrintFailure(file, line, msg); return false; } else { return true; @@ -188,7 +203,8 @@ bool Test::Helper::CheckEqualEpsilon(const char *file, int line, const char *val double value, double reference) { bool result = fabs(value - reference) < LENGTH_EPS; if(!RecordCheck(result)) { - std::string msg = ssprintf("(%s) = %.4g ≉ %.4g", valueExpr, value, reference); + std::string msg = ssprintf("(%s) = %.4g ≉ %.4g", valueExpr, + value, reference); PrintFailure(file, line, msg); return false; } else { diff --git a/test/harness.h b/test/harness.h index 9e434a2..e129dbc 100644 --- a/test/harness.h +++ b/test/harness.h @@ -5,8 +5,9 @@ //----------------------------------------------------------------------------- #include "solvespace.h" -// Hack... we should rename the one in ui.h instead. +// Hack... we should rename the ones in ui.h instead. #undef CHECK_TRUE +#undef CHECK_FALSE namespace SolveSpace { namespace Test { @@ -21,7 +22,10 @@ public: std::string GetAssetPath(std::string testFile, std::string assetName, std::string mangle = ""); - bool CheckTrue(const char *file, int line, const char *expr, bool result); + bool CheckBool(const char *file, int line, const char *expr, + bool value, bool reference); + bool CheckEqualString(const char *file, int line, const char *valueExpr, + const std::string &value, const std::string &reference); bool CheckEqualEpsilon(const char *file, int line, const char *valueExpr, double value, double reference); bool CheckLoad(const char *file, int line, const char *fixture); @@ -52,7 +56,12 @@ using namespace SolveSpace; static void Test_##name(Test::Helper *helper) // { ... } #define CHECK_TRUE(cond) \ - do { if(!helper->CheckTrue(__FILE__, __LINE__, #cond, cond)) return; } while(0) + do { if(!helper->CheckBool(__FILE__, __LINE__, #cond, cond, true)) return; } while(0) +#define CHECK_FALSE(cond) \ + do { if(!helper->CheckBool(__FILE__, __LINE__, #cond, cond, false)) return; } while(0) +#define CHECK_EQ_STR(value, reference) \ + do { if(!helper->CheckEqualString(__FILE__, __LINE__, \ + #value, value, reference)) return; } while(0) #define CHECK_EQ_EPS(value, reference) \ do { if(!helper->CheckEqualEpsilon(__FILE__, __LINE__, \ #value, value, reference)) return; } while(0)