From 33f5f056b14c35a3bfee6e467719c16971b1993c Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sat, 22 Jan 2022 22:48:40 +0000 Subject: [PATCH 01/95] first bits of a dynamc lib --- Cargo.lock | 27 + Cargo.toml | 3 +- binding-tests/.gitignore | 2 + binding-tests/Makefile | 19 + binding-tests/doctest.cpp | 2 + binding-tests/doctest.h | 6816 +++++++++++++++++++++++++++++++++++++ binding-tests/uuid.cpp | 7 + lib/Cargo.toml | 15 + lib/Makefile | 2 + lib/build.rs | 18 + lib/src/lib.rs | 1 + lib/src/storage.rs | 16 + lib/taskchampion.h | 15 + 13 files changed, 6942 insertions(+), 1 deletion(-) create mode 100644 binding-tests/.gitignore create mode 100644 binding-tests/Makefile create mode 100644 binding-tests/doctest.cpp create mode 100644 binding-tests/doctest.h create mode 100644 binding-tests/uuid.cpp create mode 100644 lib/Cargo.toml create mode 100644 lib/Makefile create mode 100644 lib/build.rs create mode 100644 lib/src/lib.rs create mode 100644 lib/src/storage.rs create mode 100644 lib/taskchampion.h diff --git a/Cargo.lock b/Cargo.lock index b63a0a2de..caa3684ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -591,6 +591,25 @@ dependencies = [ "url", ] +[[package]] +name = "cbindgen" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e3973b165dc0f435831a9e426de67e894de532754ff7a3f307c03ee5dec7dc" +dependencies = [ + "clap", + "heck", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", + "tempfile", + "toml", +] + [[package]] name = "cc" version = "1.0.68" @@ -3008,6 +3027,14 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "taskchampion-lib" +version = "0.1.0" +dependencies = [ + "cbindgen", + "taskchampion", +] + [[package]] name = "taskchampion-sync-server" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index eef6d1ce7..a552a0dda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,6 @@ members = [ "taskchampion", "cli", "sync-server", - "replica-server-tests" + "replica-server-tests", + "lib" ] diff --git a/binding-tests/.gitignore b/binding-tests/.gitignore new file mode 100644 index 000000000..fb93ddffb --- /dev/null +++ b/binding-tests/.gitignore @@ -0,0 +1,2 @@ +*.o +doctest diff --git a/binding-tests/Makefile b/binding-tests/Makefile new file mode 100644 index 000000000..e5f09d787 --- /dev/null +++ b/binding-tests/Makefile @@ -0,0 +1,19 @@ +CXX=g++ +INC=-I ../lib +LIB=-L ../target/debug +RPATH=-Wl,-rpath,../target/debug + +TESTS = uuid.cpp + +.PHONY: all test + +all: test + +test: doctest + @./doctest --no-version --no-intro + +%.o: %.cpp ../lib/taskchampion.h + $(CXX) $(INC) -c $< -o $@ + +doctest: doctest.o $(subst .cpp,.o,$(TESTS)) + $(CXX) $(LIB) $(RPATH) $< $(subst .cpp,.o,$(TESTS)) -ltaskchampion -o $@ diff --git a/binding-tests/doctest.cpp b/binding-tests/doctest.cpp new file mode 100644 index 000000000..a3f832e49 --- /dev/null +++ b/binding-tests/doctest.cpp @@ -0,0 +1,2 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest.h" diff --git a/binding-tests/doctest.h b/binding-tests/doctest.h new file mode 100644 index 000000000..d25f52682 --- /dev/null +++ b/binding-tests/doctest.h @@ -0,0 +1,6816 @@ +// ====================================================================== lgtm [cpp/missing-header-guard] +// == DO NOT MODIFY THIS FILE BY HAND - IT IS AUTO GENERATED BY CMAKE! == +// ====================================================================== +// +// doctest.h - the lightest feature-rich C++ single-header testing framework for unit tests and TDD +// +// Copyright (c) 2016-2021 Viktor Kirilov +// +// Distributed under the MIT Software License +// See accompanying file LICENSE.txt or copy at +// https://opensource.org/licenses/MIT +// +// The documentation can be found at the library's page: +// https://github.com/doctest/doctest/blob/master/doc/markdown/readme.md +// +// ================================================================================================= +// ================================================================================================= +// ================================================================================================= +// +// The library is heavily influenced by Catch - https://github.com/catchorg/Catch2 +// which uses the Boost Software License - Version 1.0 +// see here - https://github.com/catchorg/Catch2/blob/master/LICENSE.txt +// +// The concept of subcases (sections in Catch) and expression decomposition are from there. +// Some parts of the code are taken directly: +// - stringification - the detection of "ostream& operator<<(ostream&, const T&)" and StringMaker<> +// - the Approx() helper class for floating point comparison +// - colors in the console +// - breaking into a debugger +// - signal / SEH handling +// - timer +// - XmlWriter class - thanks to Phil Nash for allowing the direct reuse (AKA copy/paste) +// +// The expression decomposing templates are taken from lest - https://github.com/martinmoene/lest +// which uses the Boost Software License - Version 1.0 +// see here - https://github.com/martinmoene/lest/blob/master/LICENSE.txt +// +// ================================================================================================= +// ================================================================================================= +// ================================================================================================= + +#ifndef DOCTEST_LIBRARY_INCLUDED +#define DOCTEST_LIBRARY_INCLUDED + +// ================================================================================================= +// == VERSION ====================================================================================== +// ================================================================================================= + +#define DOCTEST_VERSION_MAJOR 2 +#define DOCTEST_VERSION_MINOR 4 +#define DOCTEST_VERSION_PATCH 8 + +// util we need here +#define DOCTEST_TOSTR_IMPL(x) #x +#define DOCTEST_TOSTR(x) DOCTEST_TOSTR_IMPL(x) + +#define DOCTEST_VERSION_STR \ + DOCTEST_TOSTR(DOCTEST_VERSION_MAJOR) "." \ + DOCTEST_TOSTR(DOCTEST_VERSION_MINOR) "." \ + DOCTEST_TOSTR(DOCTEST_VERSION_PATCH) + +#define DOCTEST_VERSION \ + (DOCTEST_VERSION_MAJOR * 10000 + DOCTEST_VERSION_MINOR * 100 + DOCTEST_VERSION_PATCH) + +// ================================================================================================= +// == COMPILER VERSION ============================================================================= +// ================================================================================================= + +// ideas for the version stuff are taken from here: https://github.com/cxxstuff/cxx_detect + +#define DOCTEST_COMPILER(MAJOR, MINOR, PATCH) ((MAJOR)*10000000 + (MINOR)*100000 + (PATCH)) + +// GCC/Clang and GCC/MSVC are mutually exclusive, but Clang/MSVC are not because of clang-cl... +#if defined(_MSC_VER) && defined(_MSC_FULL_VER) +#if _MSC_VER == _MSC_FULL_VER / 10000 +#define DOCTEST_MSVC DOCTEST_COMPILER(_MSC_VER / 100, _MSC_VER % 100, _MSC_FULL_VER % 10000) +#else // MSVC +#define DOCTEST_MSVC \ + DOCTEST_COMPILER(_MSC_VER / 100, (_MSC_FULL_VER / 100000) % 100, _MSC_FULL_VER % 100000) +#endif // MSVC +#endif // MSVC +#if defined(__clang__) && defined(__clang_minor__) +#define DOCTEST_CLANG DOCTEST_COMPILER(__clang_major__, __clang_minor__, __clang_patchlevel__) +#elif defined(__GNUC__) && defined(__GNUC_MINOR__) && defined(__GNUC_PATCHLEVEL__) && \ + !defined(__INTEL_COMPILER) +#define DOCTEST_GCC DOCTEST_COMPILER(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#endif // GCC + +#ifndef DOCTEST_MSVC +#define DOCTEST_MSVC 0 +#endif // DOCTEST_MSVC +#ifndef DOCTEST_CLANG +#define DOCTEST_CLANG 0 +#endif // DOCTEST_CLANG +#ifndef DOCTEST_GCC +#define DOCTEST_GCC 0 +#endif // DOCTEST_GCC + +// ================================================================================================= +// == COMPILER WARNINGS HELPERS ==================================================================== +// ================================================================================================= + +#if DOCTEST_CLANG +#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x) +#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH _Pragma("clang diagnostic push") +#define DOCTEST_CLANG_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(clang diagnostic ignored w) +#define DOCTEST_CLANG_SUPPRESS_WARNING_POP _Pragma("clang diagnostic pop") +#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w) \ + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH DOCTEST_CLANG_SUPPRESS_WARNING(w) +#else // DOCTEST_CLANG +#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +#define DOCTEST_CLANG_SUPPRESS_WARNING(w) +#define DOCTEST_CLANG_SUPPRESS_WARNING_POP +#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_CLANG + +#if DOCTEST_GCC +#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x) +#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH _Pragma("GCC diagnostic push") +#define DOCTEST_GCC_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(GCC diagnostic ignored w) +#define DOCTEST_GCC_SUPPRESS_WARNING_POP _Pragma("GCC diagnostic pop") +#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w) \ + DOCTEST_GCC_SUPPRESS_WARNING_PUSH DOCTEST_GCC_SUPPRESS_WARNING(w) +#else // DOCTEST_GCC +#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH +#define DOCTEST_GCC_SUPPRESS_WARNING(w) +#define DOCTEST_GCC_SUPPRESS_WARNING_POP +#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_GCC + +#if DOCTEST_MSVC +#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH __pragma(warning(push)) +#define DOCTEST_MSVC_SUPPRESS_WARNING(w) __pragma(warning(disable : w)) +#define DOCTEST_MSVC_SUPPRESS_WARNING_POP __pragma(warning(pop)) +#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w) \ + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH DOCTEST_MSVC_SUPPRESS_WARNING(w) +#else // DOCTEST_MSVC +#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +#define DOCTEST_MSVC_SUPPRESS_WARNING(w) +#define DOCTEST_MSVC_SUPPRESS_WARNING_POP +#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_MSVC + +// ================================================================================================= +// == COMPILER WARNINGS ============================================================================ +// ================================================================================================= + +// both the header and the implementation suppress all of these, +// so it only makes sense to aggregrate them like so +#define DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH \ + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunknown-pragmas") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wweak-vtables") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wpadded") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-prototypes") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-local-typedef") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat-pedantic") \ + \ + DOCTEST_GCC_SUPPRESS_WARNING_PUSH \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wunknown-pragmas") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wpragmas") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Weffc++") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-overflow") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-aliasing") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-declarations") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wunused-local-typedefs") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wuseless-cast") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wnoexcept") \ + \ + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH \ + /* these 4 also disabled globally via cmake: */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4514) /* unreferenced inline function has been removed */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4571) /* SEH related */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4710) /* function not inlined */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4711) /* function selected for inline expansion*/ \ + /* */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4616) /* invalid compiler warning */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4619) /* invalid compiler warning */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4996) /* The compiler encountered a deprecated declaration */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4706) /* assignment within conditional expression */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4512) /* 'class' : assignment operator could not be generated */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4127) /* conditional expression is constant */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4820) /* padding */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4625) /* copy constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4626) /* assignment operator was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5027) /* move assignment operator implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5026) /* move constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4640) /* construction of local static object not thread-safe */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5045) /* Spectre mitigation for memory load */ \ + /* static analysis */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26439) /* Function may not throw. Declare it 'noexcept' */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26495) /* Always initialize a member variable */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26451) /* Arithmetic overflow ... */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26444) /* Avoid unnamed objects with custom ctor and dtor... */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26812) /* Prefer 'enum class' over 'enum' */ + +#define DOCTEST_SUPPRESS_COMMON_WARNINGS_POP \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP \ + DOCTEST_GCC_SUPPRESS_WARNING_POP \ + DOCTEST_MSVC_SUPPRESS_WARNING_POP + +DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH + +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wnon-virtual-dtor") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wdeprecated") + +DOCTEST_GCC_SUPPRESS_WARNING_PUSH +DOCTEST_GCC_SUPPRESS_WARNING("-Wctor-dtor-privacy") +DOCTEST_GCC_SUPPRESS_WARNING("-Wnon-virtual-dtor") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-promo") + +DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +DOCTEST_MSVC_SUPPRESS_WARNING(4623) // default constructor was implicitly defined as deleted + +#define DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN \ + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH \ + DOCTEST_MSVC_SUPPRESS_WARNING(4548) /* before comma no effect; expected side - effect */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4265) /* virtual functions, but destructor is not virtual */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4986) /* exception specification does not match previous */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4350) /* 'member1' called instead of 'member2' */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4668) /* not defined as a preprocessor macro */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4365) /* signed/unsigned mismatch */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4774) /* format string not a string literal */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4820) /* padding */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4625) /* copy constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4626) /* assignment operator was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5027) /* move assignment operator implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5026) /* move constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4623) /* default constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5039) /* pointer to pot. throwing function passed to extern C */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5045) /* Spectre mitigation for memory load */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5105) /* macro producing 'defined' has undefined behavior */ + +#define DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END DOCTEST_MSVC_SUPPRESS_WARNING_POP + +// ================================================================================================= +// == FEATURE DETECTION ============================================================================ +// ================================================================================================= + +// general compiler feature support table: https://en.cppreference.com/w/cpp/compiler_support +// MSVC C++11 feature support table: https://msdn.microsoft.com/en-us/library/hh567368.aspx +// GCC C++11 feature support table: https://gcc.gnu.org/projects/cxx-status.html +// MSVC version table: +// https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#Internal_version_numbering +// MSVC++ 14.3 (17) _MSC_VER == 1930 (Visual Studio 2022) +// MSVC++ 14.2 (16) _MSC_VER == 1920 (Visual Studio 2019) +// MSVC++ 14.1 (15) _MSC_VER == 1910 (Visual Studio 2017) +// MSVC++ 14.0 _MSC_VER == 1900 (Visual Studio 2015) +// MSVC++ 12.0 _MSC_VER == 1800 (Visual Studio 2013) +// MSVC++ 11.0 _MSC_VER == 1700 (Visual Studio 2012) +// MSVC++ 10.0 _MSC_VER == 1600 (Visual Studio 2010) +// MSVC++ 9.0 _MSC_VER == 1500 (Visual Studio 2008) +// MSVC++ 8.0 _MSC_VER == 1400 (Visual Studio 2005) + +// Universal Windows Platform support +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) +#define DOCTEST_CONFIG_NO_WINDOWS_SEH +#endif // WINAPI_FAMILY +#if DOCTEST_MSVC && !defined(DOCTEST_CONFIG_WINDOWS_SEH) +#define DOCTEST_CONFIG_WINDOWS_SEH +#endif // MSVC +#if defined(DOCTEST_CONFIG_NO_WINDOWS_SEH) && defined(DOCTEST_CONFIG_WINDOWS_SEH) +#undef DOCTEST_CONFIG_WINDOWS_SEH +#endif // DOCTEST_CONFIG_NO_WINDOWS_SEH + +#if !defined(_WIN32) && !defined(__QNX__) && !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && \ + !defined(__EMSCRIPTEN__) +#define DOCTEST_CONFIG_POSIX_SIGNALS +#endif // _WIN32 +#if defined(DOCTEST_CONFIG_NO_POSIX_SIGNALS) && defined(DOCTEST_CONFIG_POSIX_SIGNALS) +#undef DOCTEST_CONFIG_POSIX_SIGNALS +#endif // DOCTEST_CONFIG_NO_POSIX_SIGNALS + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +#if !defined(__cpp_exceptions) && !defined(__EXCEPTIONS) && !defined(_CPPUNWIND) +#define DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // no exceptions +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +#define DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#if defined(DOCTEST_CONFIG_NO_EXCEPTIONS) && !defined(DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS) +#define DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS && !DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS + +#if defined(DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN) && !defined(DOCTEST_CONFIG_IMPLEMENT) +#define DOCTEST_CONFIG_IMPLEMENT +#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + +#if defined(_WIN32) || defined(__CYGWIN__) +#if DOCTEST_MSVC +#define DOCTEST_SYMBOL_EXPORT __declspec(dllexport) +#define DOCTEST_SYMBOL_IMPORT __declspec(dllimport) +#else // MSVC +#define DOCTEST_SYMBOL_EXPORT __attribute__((dllexport)) +#define DOCTEST_SYMBOL_IMPORT __attribute__((dllimport)) +#endif // MSVC +#else // _WIN32 +#define DOCTEST_SYMBOL_EXPORT __attribute__((visibility("default"))) +#define DOCTEST_SYMBOL_IMPORT +#endif // _WIN32 + +#ifdef DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL +#ifdef DOCTEST_CONFIG_IMPLEMENT +#define DOCTEST_INTERFACE DOCTEST_SYMBOL_EXPORT +#else // DOCTEST_CONFIG_IMPLEMENT +#define DOCTEST_INTERFACE DOCTEST_SYMBOL_IMPORT +#endif // DOCTEST_CONFIG_IMPLEMENT +#else // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL +#define DOCTEST_INTERFACE +#endif // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL + +#define DOCTEST_EMPTY + +#if DOCTEST_MSVC +#define DOCTEST_NOINLINE __declspec(noinline) +#define DOCTEST_UNUSED +#define DOCTEST_ALIGNMENT(x) +#elif DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 5, 0) +#define DOCTEST_NOINLINE +#define DOCTEST_UNUSED +#define DOCTEST_ALIGNMENT(x) +#else +#define DOCTEST_NOINLINE __attribute__((noinline)) +#define DOCTEST_UNUSED __attribute__((unused)) +#define DOCTEST_ALIGNMENT(x) __attribute__((aligned(x))) +#endif + +#ifndef DOCTEST_NORETURN +#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_NORETURN +#else // DOCTEST_MSVC +#define DOCTEST_NORETURN [[noreturn]] +#endif // DOCTEST_MSVC +#endif // DOCTEST_NORETURN + +#ifndef DOCTEST_NOEXCEPT +#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_NOEXCEPT +#else // DOCTEST_MSVC +#define DOCTEST_NOEXCEPT noexcept +#endif // DOCTEST_MSVC +#endif // DOCTEST_NOEXCEPT + +#ifndef DOCTEST_CONSTEXPR +#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_CONSTEXPR const +#else // DOCTEST_MSVC +#define DOCTEST_CONSTEXPR constexpr +#endif // DOCTEST_MSVC +#endif // DOCTEST_CONSTEXPR + +// ================================================================================================= +// == FEATURE DETECTION END ======================================================================== +// ================================================================================================= + +// internal macros for string concatenation and anonymous variable name generation +#define DOCTEST_CAT_IMPL(s1, s2) s1##s2 +#define DOCTEST_CAT(s1, s2) DOCTEST_CAT_IMPL(s1, s2) +#ifdef __COUNTER__ // not standard and may be missing for some compilers +#define DOCTEST_ANONYMOUS(x) DOCTEST_CAT(x, __COUNTER__) +#else // __COUNTER__ +#define DOCTEST_ANONYMOUS(x) DOCTEST_CAT(x, __LINE__) +#endif // __COUNTER__ + +#ifndef DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE +#define DOCTEST_REF_WRAP(x) x& +#else // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE +#define DOCTEST_REF_WRAP(x) x +#endif // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE + +// not using __APPLE__ because... this is how Catch does it +#ifdef __MAC_OS_X_VERSION_MIN_REQUIRED +#define DOCTEST_PLATFORM_MAC +#elif defined(__IPHONE_OS_VERSION_MIN_REQUIRED) +#define DOCTEST_PLATFORM_IPHONE +#elif defined(_WIN32) +#define DOCTEST_PLATFORM_WINDOWS +#else // DOCTEST_PLATFORM +#define DOCTEST_PLATFORM_LINUX +#endif // DOCTEST_PLATFORM + +namespace doctest { namespace detail { + static DOCTEST_CONSTEXPR int consume(const int*, int) { return 0; } +}} + +#define DOCTEST_GLOBAL_NO_WARNINGS(var, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wglobal-constructors") \ + static const int var = doctest::detail::consume(&var, __VA_ARGS__); \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#ifndef DOCTEST_BREAK_INTO_DEBUGGER +// should probably take a look at https://github.com/scottt/debugbreak +#ifdef DOCTEST_PLATFORM_LINUX +#if defined(__GNUC__) && (defined(__i386) || defined(__x86_64)) +// Break at the location of the failing check if possible +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("int $3\n" : :) // NOLINT (hicpp-no-assembler) +#else +#include +#define DOCTEST_BREAK_INTO_DEBUGGER() raise(SIGTRAP) +#endif +#elif defined(DOCTEST_PLATFORM_MAC) +#if defined(__x86_64) || defined(__x86_64__) || defined(__amd64__) || defined(__i386) +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("int $3\n" : :) // NOLINT (hicpp-no-assembler) +#else +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("brk #0"); // NOLINT (hicpp-no-assembler) +#endif +#elif DOCTEST_MSVC +#define DOCTEST_BREAK_INTO_DEBUGGER() __debugbreak() +#elif defined(__MINGW32__) +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wredundant-decls") +extern "C" __declspec(dllimport) void __stdcall DebugBreak(); +DOCTEST_GCC_SUPPRESS_WARNING_POP +#define DOCTEST_BREAK_INTO_DEBUGGER() ::DebugBreak() +#else // linux +#define DOCTEST_BREAK_INTO_DEBUGGER() (static_cast(0)) +#endif // linux +#endif // DOCTEST_BREAK_INTO_DEBUGGER + +// this is kept here for backwards compatibility since the config option was changed +#ifdef DOCTEST_CONFIG_USE_IOSFWD +#define DOCTEST_CONFIG_USE_STD_HEADERS +#endif // DOCTEST_CONFIG_USE_IOSFWD + +// for clang - always include ciso646 (which drags some std stuff) because +// we want to check if we are using libc++ with the _LIBCPP_VERSION macro in +// which case we don't want to forward declare stuff from std - for reference: +// https://github.com/doctest/doctest/issues/126 +// https://github.com/doctest/doctest/issues/356 +#if DOCTEST_CLANG +#include +#ifdef _LIBCPP_VERSION +#define DOCTEST_CONFIG_USE_STD_HEADERS +#endif // _LIBCPP_VERSION +#endif // clang + +#ifdef DOCTEST_CONFIG_USE_STD_HEADERS +#ifndef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#define DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#include +#include +#include +#else // DOCTEST_CONFIG_USE_STD_HEADERS + +// Forward declaring 'X' in namespace std is not permitted by the C++ Standard. +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4643) + +namespace std { // NOLINT (cert-dcl58-cpp) +typedef decltype(nullptr) nullptr_t; +template +struct char_traits; +template <> +struct char_traits; +template +class basic_ostream; +typedef basic_ostream> ostream; +template +class basic_istream; +typedef basic_istream> istream; +template +class tuple; +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 +template +class allocator; +template +class basic_string; +using string = basic_string, allocator>; +#endif // VS 2019 +} // namespace std + +DOCTEST_MSVC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_USE_STD_HEADERS + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#include +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + +namespace doctest { + +DOCTEST_INTERFACE extern bool is_running_in_test; + +// A 24 byte string class (can be as small as 17 for x64 and 13 for x86) that can hold strings with length +// of up to 23 chars on the stack before going on the heap - the last byte of the buffer is used for: +// - "is small" bit - the highest bit - if "0" then it is small - otherwise its "1" (128) +// - if small - capacity left before going on the heap - using the lowest 5 bits +// - if small - 2 bits are left unused - the second and third highest ones +// - if small - acts as a null terminator if strlen() is 23 (24 including the null terminator) +// and the "is small" bit remains "0" ("as well as the capacity left") so its OK +// Idea taken from this lecture about the string implementation of facebook/folly - fbstring +// https://www.youtube.com/watch?v=kPR8h4-qZdk +// TODO: +// - optimizations - like not deleting memory unnecessarily in operator= and etc. +// - resize/reserve/clear +// - substr +// - replace +// - back/front +// - iterator stuff +// - find & friends +// - push_back/pop_back +// - assign/insert/erase +// - relational operators as free functions - taking const char* as one of the params +class DOCTEST_INTERFACE String +{ + static const unsigned len = 24; //!OCLINT avoid private static members + static const unsigned last = len - 1; //!OCLINT avoid private static members + + struct view // len should be more than sizeof(view) - because of the final byte for flags + { + char* ptr; + unsigned size; + unsigned capacity; + }; + + union + { + char buf[len]; + view data; + }; + + char* allocate(unsigned sz); + + bool isOnStack() const { return (buf[last] & 128) == 0; } + void setOnHeap(); + void setLast(unsigned in = last); + + void copy(const String& other); + +public: + String(); + ~String(); + + // cppcheck-suppress noExplicitConstructor + String(const char* in); + String(const char* in, unsigned in_size); + + String(std::istream& in, unsigned in_size); + + String(const String& other); + String& operator=(const String& other); + + String& operator+=(const String& other); + + String(String&& other); + String& operator=(String&& other); + + char operator[](unsigned i) const; + char& operator[](unsigned i); + + // the only functions I'm willing to leave in the interface - available for inlining + const char* c_str() const { return const_cast(this)->c_str(); } // NOLINT + char* c_str() { + if(isOnStack()) + return reinterpret_cast(buf); + return data.ptr; + } + + unsigned size() const; + unsigned capacity() const; + + int compare(const char* other, bool no_case = false) const; + int compare(const String& other, bool no_case = false) const; +}; + +DOCTEST_INTERFACE String operator+(const String& lhs, const String& rhs); + +DOCTEST_INTERFACE bool operator==(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator!=(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator<(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator>(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator<=(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator>=(const String& lhs, const String& rhs); + +DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& s, const String& in); + +namespace Color { + enum Enum + { + None = 0, + White, + Red, + Green, + Blue, + Cyan, + Yellow, + Grey, + + Bright = 0x10, + + BrightRed = Bright | Red, + BrightGreen = Bright | Green, + LightGrey = Bright | Grey, + BrightWhite = Bright | White + }; + + DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& s, Color::Enum code); +} // namespace Color + +namespace assertType { + enum Enum + { + // macro traits + + is_warn = 1, + is_check = 2 * is_warn, + is_require = 2 * is_check, + + is_normal = 2 * is_require, + is_throws = 2 * is_normal, + is_throws_as = 2 * is_throws, + is_throws_with = 2 * is_throws_as, + is_nothrow = 2 * is_throws_with, + + is_false = 2 * is_nothrow, + is_unary = 2 * is_false, // not checked anywhere - used just to distinguish the types + + is_eq = 2 * is_unary, + is_ne = 2 * is_eq, + + is_lt = 2 * is_ne, + is_gt = 2 * is_lt, + + is_ge = 2 * is_gt, + is_le = 2 * is_ge, + + // macro types + + DT_WARN = is_normal | is_warn, + DT_CHECK = is_normal | is_check, + DT_REQUIRE = is_normal | is_require, + + DT_WARN_FALSE = is_normal | is_false | is_warn, + DT_CHECK_FALSE = is_normal | is_false | is_check, + DT_REQUIRE_FALSE = is_normal | is_false | is_require, + + DT_WARN_THROWS = is_throws | is_warn, + DT_CHECK_THROWS = is_throws | is_check, + DT_REQUIRE_THROWS = is_throws | is_require, + + DT_WARN_THROWS_AS = is_throws_as | is_warn, + DT_CHECK_THROWS_AS = is_throws_as | is_check, + DT_REQUIRE_THROWS_AS = is_throws_as | is_require, + + DT_WARN_THROWS_WITH = is_throws_with | is_warn, + DT_CHECK_THROWS_WITH = is_throws_with | is_check, + DT_REQUIRE_THROWS_WITH = is_throws_with | is_require, + + DT_WARN_THROWS_WITH_AS = is_throws_with | is_throws_as | is_warn, + DT_CHECK_THROWS_WITH_AS = is_throws_with | is_throws_as | is_check, + DT_REQUIRE_THROWS_WITH_AS = is_throws_with | is_throws_as | is_require, + + DT_WARN_NOTHROW = is_nothrow | is_warn, + DT_CHECK_NOTHROW = is_nothrow | is_check, + DT_REQUIRE_NOTHROW = is_nothrow | is_require, + + DT_WARN_EQ = is_normal | is_eq | is_warn, + DT_CHECK_EQ = is_normal | is_eq | is_check, + DT_REQUIRE_EQ = is_normal | is_eq | is_require, + + DT_WARN_NE = is_normal | is_ne | is_warn, + DT_CHECK_NE = is_normal | is_ne | is_check, + DT_REQUIRE_NE = is_normal | is_ne | is_require, + + DT_WARN_GT = is_normal | is_gt | is_warn, + DT_CHECK_GT = is_normal | is_gt | is_check, + DT_REQUIRE_GT = is_normal | is_gt | is_require, + + DT_WARN_LT = is_normal | is_lt | is_warn, + DT_CHECK_LT = is_normal | is_lt | is_check, + DT_REQUIRE_LT = is_normal | is_lt | is_require, + + DT_WARN_GE = is_normal | is_ge | is_warn, + DT_CHECK_GE = is_normal | is_ge | is_check, + DT_REQUIRE_GE = is_normal | is_ge | is_require, + + DT_WARN_LE = is_normal | is_le | is_warn, + DT_CHECK_LE = is_normal | is_le | is_check, + DT_REQUIRE_LE = is_normal | is_le | is_require, + + DT_WARN_UNARY = is_normal | is_unary | is_warn, + DT_CHECK_UNARY = is_normal | is_unary | is_check, + DT_REQUIRE_UNARY = is_normal | is_unary | is_require, + + DT_WARN_UNARY_FALSE = is_normal | is_false | is_unary | is_warn, + DT_CHECK_UNARY_FALSE = is_normal | is_false | is_unary | is_check, + DT_REQUIRE_UNARY_FALSE = is_normal | is_false | is_unary | is_require, + }; +} // namespace assertType + +DOCTEST_INTERFACE const char* assertString(assertType::Enum at); +DOCTEST_INTERFACE const char* failureString(assertType::Enum at); +DOCTEST_INTERFACE const char* skipPathFromFilename(const char* file); + +struct DOCTEST_INTERFACE TestCaseData +{ + String m_file; // the file in which the test was registered (using String - see #350) + unsigned m_line; // the line where the test was registered + const char* m_name; // name of the test case + const char* m_test_suite; // the test suite in which the test was added + const char* m_description; + bool m_skip; + bool m_no_breaks; + bool m_no_output; + bool m_may_fail; + bool m_should_fail; + int m_expected_failures; + double m_timeout; +}; + +struct DOCTEST_INTERFACE AssertData +{ + // common - for all asserts + const TestCaseData* m_test_case; + assertType::Enum m_at; + const char* m_file; + int m_line; + const char* m_expr; + bool m_failed; + + // exception-related - for all asserts + bool m_threw; + String m_exception; + + // for normal asserts + String m_decomp; + + // for specific exception-related asserts + bool m_threw_as; + const char* m_exception_type; + const char* m_exception_string; +}; + +struct DOCTEST_INTERFACE MessageData +{ + String m_string; + const char* m_file; + int m_line; + assertType::Enum m_severity; +}; + +struct DOCTEST_INTERFACE SubcaseSignature +{ + String m_name; + const char* m_file; + int m_line; + + bool operator<(const SubcaseSignature& other) const; +}; + +struct DOCTEST_INTERFACE IContextScope +{ + IContextScope(); + virtual ~IContextScope(); + virtual void stringify(std::ostream*) const = 0; +}; + +namespace detail { + struct DOCTEST_INTERFACE TestCase; +} // namespace detail + +struct ContextOptions //!OCLINT too many fields +{ + std::ostream* cout = nullptr; // stdout stream + String binary_name; // the test binary name + + const detail::TestCase* currentTest = nullptr; + + // == parameters from the command line + String out; // output filename + String order_by; // how tests should be ordered + unsigned rand_seed; // the seed for rand ordering + + unsigned first; // the first (matching) test to be executed + unsigned last; // the last (matching) test to be executed + + int abort_after; // stop tests after this many failed assertions + int subcase_filter_levels; // apply the subcase filters for the first N levels + + bool success; // include successful assertions in output + bool case_sensitive; // if filtering should be case sensitive + bool exit; // if the program should be exited after the tests are ran/whatever + bool duration; // print the time duration of each test case + bool minimal; // minimal console output (only test failures) + bool quiet; // no console output + bool no_throw; // to skip exceptions-related assertion macros + bool no_exitcode; // if the framework should return 0 as the exitcode + bool no_run; // to not run the tests at all (can be done with an "*" exclude) + bool no_intro; // to not print the intro of the framework + bool no_version; // to not print the version of the framework + bool no_colors; // if output to the console should be colorized + bool force_colors; // forces the use of colors even when a tty cannot be detected + bool no_breaks; // to not break into the debugger + bool no_skip; // don't skip test cases which are marked to be skipped + bool gnu_file_line; // if line numbers should be surrounded with :x: and not (x): + bool no_path_in_filenames; // if the path to files should be removed from the output + bool no_line_numbers; // if source code line numbers should be omitted from the output + bool no_debug_output; // no output in the debug console when a debugger is attached + bool no_skipped_summary; // don't print "skipped" in the summary !!! UNDOCUMENTED !!! + bool no_time_in_output; // omit any time/timestamps from output !!! UNDOCUMENTED !!! + + bool help; // to print the help + bool version; // to print the version + bool count; // if only the count of matching tests is to be retrieved + bool list_test_cases; // to list all tests matching the filters + bool list_test_suites; // to list all suites matching the filters + bool list_reporters; // lists all registered reporters +}; + +namespace detail { + template + struct enable_if + {}; + + template + struct enable_if + { typedef TYPE type; }; + + // clang-format off + template struct remove_reference { typedef T type; }; + template struct remove_reference { typedef T type; }; + template struct remove_reference { typedef T type; }; + + template U declval(int); + + template T declval(long); + + template auto declval() DOCTEST_NOEXCEPT -> decltype(declval(0)) ; + + template struct is_lvalue_reference { const static bool value=false; }; + template struct is_lvalue_reference { const static bool value=true; }; + + template struct is_rvalue_reference { const static bool value=false; }; + template struct is_rvalue_reference { const static bool value=true; }; + + template + inline T&& forward(typename remove_reference::type& t) DOCTEST_NOEXCEPT + { + return static_cast(t); + } + + template + inline T&& forward(typename remove_reference::type&& t) DOCTEST_NOEXCEPT + { + static_assert(!is_lvalue_reference::value, + "Can not forward an rvalue as an lvalue."); + return static_cast(t); + } + + template struct remove_const { typedef T type; }; + template struct remove_const { typedef T type; }; +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template struct is_enum : public std::is_enum {}; + template struct underlying_type : public std::underlying_type {}; +#else + // Use compiler intrinsics + template struct is_enum { DOCTEST_CONSTEXPR static bool value = __is_enum(T); }; + template struct underlying_type { typedef __underlying_type(T) type; }; +#endif + // clang-format on + + template + struct deferred_false + // cppcheck-suppress unusedStructMember + { static const bool value = false; }; + + namespace has_insertion_operator_impl { + std::ostream &os(); + template + DOCTEST_REF_WRAP(T) val(); + + template + struct check { + static DOCTEST_CONSTEXPR bool value = false; + }; + + template + struct check(), void())> { + static DOCTEST_CONSTEXPR bool value = true; + }; + } // namespace has_insertion_operator_impl + + template + using has_insertion_operator = has_insertion_operator_impl::check; + + DOCTEST_INTERFACE std::ostream* tlssPush(); + DOCTEST_INTERFACE String tlssPop(); + + + template + struct StringMakerBase + { + template + static String convert(const DOCTEST_REF_WRAP(T)) { + return "{?}"; + } + }; + + // Vector and various type other than pointer or array. + template + struct filldata + { + static void fill(std::ostream* stream, const T &in) { + *stream << in; + } + }; + + template + struct filldata + { + static void fill(std::ostream* stream, const T (&in)[N]) { + for (unsigned long i = 0; i < N; i++) { + *stream << in[i]; + } + } + }; + + // Specialized since we don't want the terminating null byte! + template + struct filldata + { + static void fill(std::ostream* stream, const char(&in)[N]) { + *stream << in; + } + }; + + template + void filloss(std::ostream* stream, const T& in) { + filldata::fill(stream, in); + } + + template + void filloss(std::ostream* stream, const T (&in)[N]) { + // T[N], T(&)[N], T(&&)[N] have same behaviour. + // Hence remove reference. + filldata::type>::fill(stream, in); + } + + template <> + struct StringMakerBase + { + template + static String convert(const DOCTEST_REF_WRAP(T) in) { + /* When parameter "in" is a null terminated const char* it works. + * When parameter "in" is a T arr[N] without '\0' we can fill the + * stringstream with N objects (T=char).If in is char pointer * + * without '\0' , it would cause segfault + * stepping over unaccessible memory. + */ + + std::ostream* stream = tlssPush(); + filloss(stream, in); + return tlssPop(); + } + }; + + DOCTEST_INTERFACE String rawMemoryToString(const void* object, unsigned size); + + template + String rawMemoryToString(const DOCTEST_REF_WRAP(T) object) { + return rawMemoryToString(&object, sizeof(object)); + } + + template + const char* type_to_string() { + return "<>"; + } +} // namespace detail + +template +struct StringMaker : public detail::StringMakerBase::value> +{}; + +template +struct StringMaker +{ + template + static String convert(U* p) { + if(p) + return detail::rawMemoryToString(p); + return "NULL"; + } +}; + +template +struct StringMaker +{ + static String convert(R C::*p) { + if(p) + return detail::rawMemoryToString(p); + return "NULL"; + } +}; + +template ::value, bool>::type = true> +String toString(const DOCTEST_REF_WRAP(T) value) { + return StringMaker::convert(value); +} + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +DOCTEST_INTERFACE String toString(char* in); +DOCTEST_INTERFACE String toString(const char* in); +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +DOCTEST_INTERFACE String toString(bool in); +DOCTEST_INTERFACE String toString(float in); +DOCTEST_INTERFACE String toString(double in); +DOCTEST_INTERFACE String toString(double long in); + +DOCTEST_INTERFACE String toString(char in); +DOCTEST_INTERFACE String toString(char signed in); +DOCTEST_INTERFACE String toString(char unsigned in); +DOCTEST_INTERFACE String toString(int short in); +DOCTEST_INTERFACE String toString(int short unsigned in); +DOCTEST_INTERFACE String toString(int in); +DOCTEST_INTERFACE String toString(int unsigned in); +DOCTEST_INTERFACE String toString(int long in); +DOCTEST_INTERFACE String toString(int long unsigned in); +DOCTEST_INTERFACE String toString(int long long in); +DOCTEST_INTERFACE String toString(int long long unsigned in); +DOCTEST_INTERFACE String toString(std::nullptr_t in); + +template ::value, bool>::type = true> +String toString(const DOCTEST_REF_WRAP(T) value) { + typedef typename detail::underlying_type::type UT; + return toString(static_cast(value)); +} + +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 +DOCTEST_INTERFACE String toString(const std::string& in); +#endif // VS 2019 + +class DOCTEST_INTERFACE Approx +{ +public: + explicit Approx(double value); + + Approx operator()(double value) const; + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + explicit Approx(const T& value, + typename detail::enable_if::value>::type* = + static_cast(nullptr)) { + *this = Approx(static_cast(value)); + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + Approx& epsilon(double newEpsilon); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + typename detail::enable_if::value, Approx&>::type epsilon( + const T& newEpsilon) { + m_epsilon = static_cast(newEpsilon); + return *this; + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + Approx& scale(double newScale); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + typename detail::enable_if::value, Approx&>::type scale( + const T& newScale) { + m_scale = static_cast(newScale); + return *this; + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + // clang-format off + DOCTEST_INTERFACE friend bool operator==(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator==(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator!=(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator!=(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator<=(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator<=(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator>=(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator>=(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator< (double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator< (const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator> (double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator> (const Approx & lhs, double rhs); + + DOCTEST_INTERFACE friend String toString(const Approx& in); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#define DOCTEST_APPROX_PREFIX \ + template friend typename detail::enable_if::value, bool>::type + + DOCTEST_APPROX_PREFIX operator==(const T& lhs, const Approx& rhs) { return operator==(double(lhs), rhs); } + DOCTEST_APPROX_PREFIX operator==(const Approx& lhs, const T& rhs) { return operator==(rhs, lhs); } + DOCTEST_APPROX_PREFIX operator!=(const T& lhs, const Approx& rhs) { return !operator==(lhs, rhs); } + DOCTEST_APPROX_PREFIX operator!=(const Approx& lhs, const T& rhs) { return !operator==(rhs, lhs); } + DOCTEST_APPROX_PREFIX operator<=(const T& lhs, const Approx& rhs) { return double(lhs) < rhs.m_value || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator<=(const Approx& lhs, const T& rhs) { return lhs.m_value < double(rhs) || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator>=(const T& lhs, const Approx& rhs) { return double(lhs) > rhs.m_value || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator>=(const Approx& lhs, const T& rhs) { return lhs.m_value > double(rhs) || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator< (const T& lhs, const Approx& rhs) { return double(lhs) < rhs.m_value && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator< (const Approx& lhs, const T& rhs) { return lhs.m_value < double(rhs) && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator> (const T& lhs, const Approx& rhs) { return double(lhs) > rhs.m_value && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator> (const Approx& lhs, const T& rhs) { return lhs.m_value > double(rhs) && lhs != rhs; } +#undef DOCTEST_APPROX_PREFIX +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + // clang-format on + +private: + double m_epsilon; + double m_scale; + double m_value; +}; + +DOCTEST_INTERFACE String toString(const Approx& in); + +DOCTEST_INTERFACE const ContextOptions* getContextOptions(); + +#if !defined(DOCTEST_CONFIG_DISABLE) + +namespace detail { + // clang-format off +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + template struct decay_array { typedef T type; }; + template struct decay_array { typedef T* type; }; + template struct decay_array { typedef T* type; }; + + template struct not_char_pointer { enum { value = 1 }; }; + template<> struct not_char_pointer { enum { value = 0 }; }; + template<> struct not_char_pointer { enum { value = 0 }; }; + + template struct can_use_op : public not_char_pointer::type> {}; +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + // clang-format on + + struct DOCTEST_INTERFACE TestFailureException + { + }; + + DOCTEST_INTERFACE bool checkIfShouldThrow(assertType::Enum at); + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + DOCTEST_NORETURN +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + DOCTEST_INTERFACE void throwException(); + + struct DOCTEST_INTERFACE Subcase + { + SubcaseSignature m_signature; + bool m_entered = false; + + Subcase(const String& name, const char* file, int line); + ~Subcase(); + + operator bool() const; + }; + + template + String stringifyBinaryExpr(const DOCTEST_REF_WRAP(L) lhs, const char* op, + const DOCTEST_REF_WRAP(R) rhs) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) + return toString(lhs) + op + toString(rhs); + } + +#if DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 6, 0) +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-comparison") +#endif + +// This will check if there is any way it could find a operator like member or friend and uses it. +// If not it doesn't find the operator or if the operator at global scope is defined after +// this template, the template won't be instantiated due to SFINAE. Once the template is not +// instantiated it can look for global operator using normal conversions. +#define SFINAE_OP(ret,op) decltype((void)(doctest::detail::declval() op doctest::detail::declval()),ret{}) + +#define DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(op, op_str, op_macro) \ + template \ + DOCTEST_NOINLINE SFINAE_OP(Result,op) operator op(const R&& rhs) { \ + bool res = op_macro(doctest::detail::forward(lhs), doctest::detail::forward(rhs)); \ + if(m_at & assertType::is_false) \ + res = !res; \ + if(!res || doctest::getContextOptions()->success) \ + return Result(res, stringifyBinaryExpr(lhs, op_str, rhs)); \ + return Result(res); \ + } \ + template ::value, void >::type* = nullptr> \ + DOCTEST_NOINLINE SFINAE_OP(Result,op) operator op(const R& rhs) { \ + bool res = op_macro(doctest::detail::forward(lhs), rhs); \ + if(m_at & assertType::is_false) \ + res = !res; \ + if(!res || doctest::getContextOptions()->success) \ + return Result(res, stringifyBinaryExpr(lhs, op_str, rhs)); \ + return Result(res); \ + } + + // more checks could be added - like in Catch: + // https://github.com/catchorg/Catch2/pull/1480/files + // https://github.com/catchorg/Catch2/pull/1481/files +#define DOCTEST_FORBIT_EXPRESSION(rt, op) \ + template \ + rt& operator op(const R&) { \ + static_assert(deferred_false::value, \ + "Expression Too Complex Please Rewrite As Binary Comparison!"); \ + return *this; \ + } + + struct DOCTEST_INTERFACE Result + { + bool m_passed; + String m_decomp; + + Result() = default; + Result(bool passed, const String& decomposition = String()); + + // forbidding some expressions based on this table: https://en.cppreference.com/w/cpp/language/operator_precedence + DOCTEST_FORBIT_EXPRESSION(Result, &) + DOCTEST_FORBIT_EXPRESSION(Result, ^) + DOCTEST_FORBIT_EXPRESSION(Result, |) + DOCTEST_FORBIT_EXPRESSION(Result, &&) + DOCTEST_FORBIT_EXPRESSION(Result, ||) + DOCTEST_FORBIT_EXPRESSION(Result, ==) + DOCTEST_FORBIT_EXPRESSION(Result, !=) + DOCTEST_FORBIT_EXPRESSION(Result, <) + DOCTEST_FORBIT_EXPRESSION(Result, >) + DOCTEST_FORBIT_EXPRESSION(Result, <=) + DOCTEST_FORBIT_EXPRESSION(Result, >=) + DOCTEST_FORBIT_EXPRESSION(Result, =) + DOCTEST_FORBIT_EXPRESSION(Result, +=) + DOCTEST_FORBIT_EXPRESSION(Result, -=) + DOCTEST_FORBIT_EXPRESSION(Result, *=) + DOCTEST_FORBIT_EXPRESSION(Result, /=) + DOCTEST_FORBIT_EXPRESSION(Result, %=) + DOCTEST_FORBIT_EXPRESSION(Result, <<=) + DOCTEST_FORBIT_EXPRESSION(Result, >>=) + DOCTEST_FORBIT_EXPRESSION(Result, &=) + DOCTEST_FORBIT_EXPRESSION(Result, ^=) + DOCTEST_FORBIT_EXPRESSION(Result, |=) + }; + +#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH + DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion") + DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-compare") + //DOCTEST_CLANG_SUPPRESS_WARNING("-Wdouble-promotion") + //DOCTEST_CLANG_SUPPRESS_WARNING("-Wconversion") + //DOCTEST_CLANG_SUPPRESS_WARNING("-Wfloat-equal") + + DOCTEST_GCC_SUPPRESS_WARNING_PUSH + DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion") + DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-compare") + //DOCTEST_GCC_SUPPRESS_WARNING("-Wdouble-promotion") + //DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion") + //DOCTEST_GCC_SUPPRESS_WARNING("-Wfloat-equal") + + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH + // https://stackoverflow.com/questions/39479163 what's the difference between 4018 and 4389 + DOCTEST_MSVC_SUPPRESS_WARNING(4388) // signed/unsigned mismatch + DOCTEST_MSVC_SUPPRESS_WARNING(4389) // 'operator' : signed/unsigned mismatch + DOCTEST_MSVC_SUPPRESS_WARNING(4018) // 'expression' : signed/unsigned mismatch + //DOCTEST_MSVC_SUPPRESS_WARNING(4805) // 'operation' : unsafe mix of type 'type' and type 'type' in operation + +#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + + // clang-format off +#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_COMPARISON_RETURN_TYPE bool +#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_COMPARISON_RETURN_TYPE typename enable_if::value || can_use_op::value, bool>::type + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) + inline bool eq(const char* lhs, const char* rhs) { return String(lhs) == String(rhs); } + inline bool ne(const char* lhs, const char* rhs) { return String(lhs) != String(rhs); } + inline bool lt(const char* lhs, const char* rhs) { return String(lhs) < String(rhs); } + inline bool gt(const char* lhs, const char* rhs) { return String(lhs) > String(rhs); } + inline bool le(const char* lhs, const char* rhs) { return String(lhs) <= String(rhs); } + inline bool ge(const char* lhs, const char* rhs) { return String(lhs) >= String(rhs); } +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + // clang-format on + +#define DOCTEST_RELATIONAL_OP(name, op) \ + template \ + DOCTEST_COMPARISON_RETURN_TYPE name(const DOCTEST_REF_WRAP(L) lhs, \ + const DOCTEST_REF_WRAP(R) rhs) { \ + return lhs op rhs; \ + } + + DOCTEST_RELATIONAL_OP(eq, ==) + DOCTEST_RELATIONAL_OP(ne, !=) + DOCTEST_RELATIONAL_OP(lt, <) + DOCTEST_RELATIONAL_OP(gt, >) + DOCTEST_RELATIONAL_OP(le, <=) + DOCTEST_RELATIONAL_OP(ge, >=) + +#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_CMP_EQ(l, r) l == r +#define DOCTEST_CMP_NE(l, r) l != r +#define DOCTEST_CMP_GT(l, r) l > r +#define DOCTEST_CMP_LT(l, r) l < r +#define DOCTEST_CMP_GE(l, r) l >= r +#define DOCTEST_CMP_LE(l, r) l <= r +#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_CMP_EQ(l, r) eq(l, r) +#define DOCTEST_CMP_NE(l, r) ne(l, r) +#define DOCTEST_CMP_GT(l, r) gt(l, r) +#define DOCTEST_CMP_LT(l, r) lt(l, r) +#define DOCTEST_CMP_GE(l, r) ge(l, r) +#define DOCTEST_CMP_LE(l, r) le(l, r) +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + + template + // cppcheck-suppress copyCtorAndEqOperator + struct Expression_lhs + { + L lhs; + assertType::Enum m_at; + + explicit Expression_lhs(L&& in, assertType::Enum at) + : lhs(doctest::detail::forward(in)) + , m_at(at) {} + + DOCTEST_NOINLINE operator Result() { +// this is needed only for MSVC 2015 +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4800) // 'int': forcing value to bool + bool res = static_cast(lhs); +DOCTEST_MSVC_SUPPRESS_WARNING_POP + if(m_at & assertType::is_false) //!OCLINT bitwise operator in conditional + res = !res; + + if(!res || getContextOptions()->success) + return Result(res, toString(lhs)); + return Result(res); + } + + /* This is required for user-defined conversions from Expression_lhs to L */ + operator L() const { return lhs; } + + // clang-format off + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(==, " == ", DOCTEST_CMP_EQ) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(!=, " != ", DOCTEST_CMP_NE) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>, " > ", DOCTEST_CMP_GT) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<, " < ", DOCTEST_CMP_LT) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>=, " >= ", DOCTEST_CMP_GE) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<=, " <= ", DOCTEST_CMP_LE) //!OCLINT bitwise operator in conditional + // clang-format on + + // forbidding some expressions based on this table: https://en.cppreference.com/w/cpp/language/operator_precedence + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &&) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ||) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, =) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, +=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, -=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, *=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, /=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, %=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |=) + // these 2 are unfortunate because they should be allowed - they have higher precedence over the comparisons, but the + // ExpressionDecomposer class uses the left shift operator to capture the left operand of the binary expression... + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>) + }; + +#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + + DOCTEST_CLANG_SUPPRESS_WARNING_POP + DOCTEST_MSVC_SUPPRESS_WARNING_POP + DOCTEST_GCC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + +#if DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 6, 0) +DOCTEST_CLANG_SUPPRESS_WARNING_POP +#endif + + struct DOCTEST_INTERFACE ExpressionDecomposer + { + assertType::Enum m_at; + + ExpressionDecomposer(assertType::Enum at); + + // The right operator for capturing expressions is "<=" instead of "<<" (based on the operator precedence table) + // but then there will be warnings from GCC about "-Wparentheses" and since "_Pragma()" is problematic this will stay for now... + // https://github.com/catchorg/Catch2/issues/870 + // https://github.com/catchorg/Catch2/issues/565 + template + Expression_lhs operator<<(const L &&operand) { + return Expression_lhs(doctest::detail::forward(operand), m_at); + } + + template ::value,void >::type* = nullptr> + Expression_lhs operator<<(const L &operand) { + return Expression_lhs(operand, m_at); + } + }; + + struct DOCTEST_INTERFACE TestSuite + { + const char* m_test_suite = nullptr; + const char* m_description = nullptr; + bool m_skip = false; + bool m_no_breaks = false; + bool m_no_output = false; + bool m_may_fail = false; + bool m_should_fail = false; + int m_expected_failures = 0; + double m_timeout = 0; + + TestSuite& operator*(const char* in); + + template + TestSuite& operator*(const T& in) { + in.fill(*this); + return *this; + } + }; + + typedef void (*funcType)(); + + struct DOCTEST_INTERFACE TestCase : public TestCaseData + { + funcType m_test; // a function pointer to the test case + + const char* m_type; // for templated test cases - gets appended to the real name + int m_template_id; // an ID used to distinguish between the different versions of a templated test case + String m_full_name; // contains the name (only for templated test cases!) + the template type + + TestCase(funcType test, const char* file, unsigned line, const TestSuite& test_suite, + const char* type = "", int template_id = -1); + + TestCase(const TestCase& other); + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function + TestCase& operator=(const TestCase& other); + DOCTEST_MSVC_SUPPRESS_WARNING_POP + + TestCase& operator*(const char* in); + + template + TestCase& operator*(const T& in) { + in.fill(*this); + return *this; + } + + bool operator<(const TestCase& other) const; + }; + + // forward declarations of functions used by the macros + DOCTEST_INTERFACE int regTest(const TestCase& tc); + DOCTEST_INTERFACE int setTestSuite(const TestSuite& ts); + DOCTEST_INTERFACE bool isDebuggerActive(); + + template + int instantiationHelper(const T&) { return 0; } + + namespace binaryAssertComparison { + enum Enum + { + eq = 0, + ne, + gt, + lt, + ge, + le + }; + } // namespace binaryAssertComparison + + // clang-format off + template struct RelationalComparator { bool operator()(const DOCTEST_REF_WRAP(L), const DOCTEST_REF_WRAP(R) ) const { return false; } }; + +#define DOCTEST_BINARY_RELATIONAL_OP(n, op) \ + template struct RelationalComparator { bool operator()(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) const { return op(lhs, rhs); } }; + // clang-format on + + DOCTEST_BINARY_RELATIONAL_OP(0, doctest::detail::eq) + DOCTEST_BINARY_RELATIONAL_OP(1, doctest::detail::ne) + DOCTEST_BINARY_RELATIONAL_OP(2, doctest::detail::gt) + DOCTEST_BINARY_RELATIONAL_OP(3, doctest::detail::lt) + DOCTEST_BINARY_RELATIONAL_OP(4, doctest::detail::ge) + DOCTEST_BINARY_RELATIONAL_OP(5, doctest::detail::le) + + struct DOCTEST_INTERFACE ResultBuilder : public AssertData + { + ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type = "", const char* exception_string = ""); + + void setResult(const Result& res); + + template + DOCTEST_NOINLINE bool binary_assert(const DOCTEST_REF_WRAP(L) lhs, + const DOCTEST_REF_WRAP(R) rhs) { + m_failed = !RelationalComparator()(lhs, rhs); + if(m_failed || getContextOptions()->success) + m_decomp = stringifyBinaryExpr(lhs, ", ", rhs); + return !m_failed; + } + + template + DOCTEST_NOINLINE bool unary_assert(const DOCTEST_REF_WRAP(L) val) { + m_failed = !val; + + if(m_at & assertType::is_false) //!OCLINT bitwise operator in conditional + m_failed = !m_failed; + + if(m_failed || getContextOptions()->success) + m_decomp = toString(val); + + return !m_failed; + } + + void translateException(); + + bool log(); + void react() const; + }; + + namespace assertAction { + enum Enum + { + nothing = 0, + dbgbreak = 1, + shouldthrow = 2 + }; + } // namespace assertAction + + DOCTEST_INTERFACE void failed_out_of_a_testing_context(const AssertData& ad); + + DOCTEST_INTERFACE bool decomp_assert(assertType::Enum at, const char* file, int line, + const char* expr, Result result); + +#define DOCTEST_ASSERT_OUT_OF_TESTS(decomp) \ + do { \ + if(!is_running_in_test) { \ + if(failed) { \ + ResultBuilder rb(at, file, line, expr); \ + rb.m_failed = failed; \ + rb.m_decomp = decomp; \ + failed_out_of_a_testing_context(rb); \ + if(isDebuggerActive() && !getContextOptions()->no_breaks) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + if(checkIfShouldThrow(at)) \ + throwException(); \ + } \ + return !failed; \ + } \ + } while(false) + +#define DOCTEST_ASSERT_IN_TESTS(decomp) \ + ResultBuilder rb(at, file, line, expr); \ + rb.m_failed = failed; \ + if(rb.m_failed || getContextOptions()->success) \ + rb.m_decomp = decomp; \ + if(rb.log()) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + if(rb.m_failed && checkIfShouldThrow(at)) \ + throwException() + + template + DOCTEST_NOINLINE bool binary_assert(assertType::Enum at, const char* file, int line, + const char* expr, const DOCTEST_REF_WRAP(L) lhs, + const DOCTEST_REF_WRAP(R) rhs) { + bool failed = !RelationalComparator()(lhs, rhs); + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS(stringifyBinaryExpr(lhs, ", ", rhs)); + DOCTEST_ASSERT_IN_TESTS(stringifyBinaryExpr(lhs, ", ", rhs)); + return !failed; + } + + template + DOCTEST_NOINLINE bool unary_assert(assertType::Enum at, const char* file, int line, + const char* expr, const DOCTEST_REF_WRAP(L) val) { + bool failed = !val; + + if(at & assertType::is_false) //!OCLINT bitwise operator in conditional + failed = !failed; + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS(toString(val)); + DOCTEST_ASSERT_IN_TESTS(toString(val)); + return !failed; + } + + struct DOCTEST_INTERFACE IExceptionTranslator + { + IExceptionTranslator(); + virtual ~IExceptionTranslator(); + virtual bool translate(String&) const = 0; + }; + + template + class ExceptionTranslator : public IExceptionTranslator //!OCLINT destructor of virtual class + { + public: + explicit ExceptionTranslator(String (*translateFunction)(T)) + : m_translateFunction(translateFunction) {} + + bool translate(String& res) const override { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + try { + throw; // lgtm [cpp/rethrow-no-exception] + // cppcheck-suppress catchExceptionByValue + } catch(T ex) { // NOLINT + res = m_translateFunction(ex); //!OCLINT parameter reassignment + return true; + } catch(...) {} //!OCLINT - empty catch statement +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + static_cast(res); // to silence -Wunused-parameter + return false; + } + + private: + String (*m_translateFunction)(T); + }; + + DOCTEST_INTERFACE void registerExceptionTranslatorImpl(const IExceptionTranslator* et); + + template + struct StringStreamBase + { + template + static void convert(std::ostream* s, const T& in) { + *s << toString(in); + } + + // always treat char* as a string in this context - no matter + // if DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING is defined + static void convert(std::ostream* s, const char* in) { *s << String(in); } + }; + + template <> + struct StringStreamBase + { + template + static void convert(std::ostream* s, const T& in) { + *s << in; + } + }; + + template + struct StringStream : public StringStreamBase::value> + {}; + + template + void toStream(std::ostream* s, const T& value) { + StringStream::convert(s, value); + } + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + DOCTEST_INTERFACE void toStream(std::ostream* s, char* in); + DOCTEST_INTERFACE void toStream(std::ostream* s, const char* in); +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + DOCTEST_INTERFACE void toStream(std::ostream* s, bool in); + DOCTEST_INTERFACE void toStream(std::ostream* s, float in); + DOCTEST_INTERFACE void toStream(std::ostream* s, double in); + DOCTEST_INTERFACE void toStream(std::ostream* s, double long in); + + DOCTEST_INTERFACE void toStream(std::ostream* s, char in); + DOCTEST_INTERFACE void toStream(std::ostream* s, char signed in); + DOCTEST_INTERFACE void toStream(std::ostream* s, char unsigned in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int short in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int short unsigned in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int unsigned in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int long in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int long unsigned in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int long long in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int long long unsigned in); + + // ContextScope base class used to allow implementing methods of ContextScope + // that don't depend on the template parameter in doctest.cpp. + class DOCTEST_INTERFACE ContextScopeBase : public IContextScope { + protected: + ContextScopeBase(); + ContextScopeBase(ContextScopeBase&& other); + + void destroy(); + bool need_to_destroy{true}; + }; + + template class ContextScope : public ContextScopeBase + { + const L lambda_; + + public: + explicit ContextScope(const L &lambda) : lambda_(lambda) {} + + ContextScope(ContextScope &&other) : ContextScopeBase(static_cast(other)), lambda_(other.lambda_) {} + + void stringify(std::ostream* s) const override { lambda_(s); } + + ~ContextScope() override { + if (need_to_destroy) { + destroy(); + } + } + }; + + struct DOCTEST_INTERFACE MessageBuilder : public MessageData + { + std::ostream* m_stream; + bool logged = false; + + MessageBuilder(const char* file, int line, assertType::Enum severity); + MessageBuilder() = delete; + ~MessageBuilder(); + + // the preferred way of chaining parameters for stringification + template + MessageBuilder& operator,(const T& in) { + toStream(m_stream, in); + return *this; + } + + // kept here just for backwards-compatibility - the comma operator should be preferred now + template + MessageBuilder& operator<<(const T& in) { return this->operator,(in); } + + // the `,` operator has the lowest operator precedence - if `<<` is used by the user then + // the `,` operator will be called last which is not what we want and thus the `*` operator + // is used first (has higher operator precedence compared to `<<`) so that we guarantee that + // an operator of the MessageBuilder class is called first before the rest of the parameters + template + MessageBuilder& operator*(const T& in) { return this->operator,(in); } + + bool log(); + void react(); + }; + + template + ContextScope MakeContextScope(const L &lambda) { + return ContextScope(lambda); + } +} // namespace detail + +#define DOCTEST_DEFINE_DECORATOR(name, type, def) \ + struct name \ + { \ + type data; \ + name(type in = def) \ + : data(in) {} \ + void fill(detail::TestCase& state) const { state.DOCTEST_CAT(m_, name) = data; } \ + void fill(detail::TestSuite& state) const { state.DOCTEST_CAT(m_, name) = data; } \ + } + +DOCTEST_DEFINE_DECORATOR(test_suite, const char*, ""); +DOCTEST_DEFINE_DECORATOR(description, const char*, ""); +DOCTEST_DEFINE_DECORATOR(skip, bool, true); +DOCTEST_DEFINE_DECORATOR(no_breaks, bool, true); +DOCTEST_DEFINE_DECORATOR(no_output, bool, true); +DOCTEST_DEFINE_DECORATOR(timeout, double, 0); +DOCTEST_DEFINE_DECORATOR(may_fail, bool, true); +DOCTEST_DEFINE_DECORATOR(should_fail, bool, true); +DOCTEST_DEFINE_DECORATOR(expected_failures, int, 0); + +template +int registerExceptionTranslator(String (*translateFunction)(T)) { + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors") + static detail::ExceptionTranslator exceptionTranslator(translateFunction); + DOCTEST_CLANG_SUPPRESS_WARNING_POP + detail::registerExceptionTranslatorImpl(&exceptionTranslator); + return 0; +} + +} // namespace doctest + +// in a separate namespace outside of doctest because the DOCTEST_TEST_SUITE macro +// introduces an anonymous namespace in which getCurrentTestSuite gets overridden +namespace doctest_detail_test_suite_ns { +DOCTEST_INTERFACE doctest::detail::TestSuite& getCurrentTestSuite(); +} // namespace doctest_detail_test_suite_ns + +namespace doctest { +#else // DOCTEST_CONFIG_DISABLE +template +int registerExceptionTranslator(String (*)(T)) { + return 0; +} +#endif // DOCTEST_CONFIG_DISABLE + +namespace detail { + typedef void (*assert_handler)(const AssertData&); + struct ContextState; +} // namespace detail + +class DOCTEST_INTERFACE Context +{ + detail::ContextState* p; + + void parseArgs(int argc, const char* const* argv, bool withDefaults = false); + +public: + explicit Context(int argc = 0, const char* const* argv = nullptr); + + ~Context(); + + void applyCommandLine(int argc, const char* const* argv); + + void addFilter(const char* filter, const char* value); + void clearFilters(); + void setOption(const char* option, bool value); + void setOption(const char* option, int value); + void setOption(const char* option, const char* value); + + bool shouldExit(); + + void setAsDefaultForAssertsOutOfTestCases(); + + void setAssertHandler(detail::assert_handler ah); + + void setCout(std::ostream* out); + + int run(); +}; + +namespace TestCaseFailureReason { + enum Enum + { + None = 0, + AssertFailure = 1, // an assertion has failed in the test case + Exception = 2, // test case threw an exception + Crash = 4, // a crash... + TooManyFailedAsserts = 8, // the abort-after option + Timeout = 16, // see the timeout decorator + ShouldHaveFailedButDidnt = 32, // see the should_fail decorator + ShouldHaveFailedAndDid = 64, // see the should_fail decorator + DidntFailExactlyNumTimes = 128, // see the expected_failures decorator + FailedExactlyNumTimes = 256, // see the expected_failures decorator + CouldHaveFailedAndDid = 512 // see the may_fail decorator + }; +} // namespace TestCaseFailureReason + +struct DOCTEST_INTERFACE CurrentTestCaseStats +{ + int numAssertsCurrentTest; + int numAssertsFailedCurrentTest; + double seconds; + int failure_flags; // use TestCaseFailureReason::Enum + bool testCaseSuccess; +}; + +struct DOCTEST_INTERFACE TestCaseException +{ + String error_string; + bool is_crash; +}; + +struct DOCTEST_INTERFACE TestRunStats +{ + unsigned numTestCases; + unsigned numTestCasesPassingFilters; + unsigned numTestSuitesPassingFilters; + unsigned numTestCasesFailed; + int numAsserts; + int numAssertsFailed; +}; + +struct QueryData +{ + const TestRunStats* run_stats = nullptr; + const TestCaseData** data = nullptr; + unsigned num_data = 0; +}; + +struct DOCTEST_INTERFACE IReporter +{ + // The constructor has to accept "const ContextOptions&" as a single argument + // which has most of the options for the run + a pointer to the stdout stream + // Reporter(const ContextOptions& in) + + // called when a query should be reported (listing test cases, printing the version, etc.) + virtual void report_query(const QueryData&) = 0; + + // called when the whole test run starts + virtual void test_run_start() = 0; + // called when the whole test run ends (caching a pointer to the input doesn't make sense here) + virtual void test_run_end(const TestRunStats&) = 0; + + // called when a test case is started (safe to cache a pointer to the input) + virtual void test_case_start(const TestCaseData&) = 0; + // called when a test case is reentered because of unfinished subcases (safe to cache a pointer to the input) + virtual void test_case_reenter(const TestCaseData&) = 0; + // called when a test case has ended + virtual void test_case_end(const CurrentTestCaseStats&) = 0; + + // called when an exception is thrown from the test case (or it crashes) + virtual void test_case_exception(const TestCaseException&) = 0; + + // called whenever a subcase is entered (don't cache pointers to the input) + virtual void subcase_start(const SubcaseSignature&) = 0; + // called whenever a subcase is exited (don't cache pointers to the input) + virtual void subcase_end() = 0; + + // called for each assert (don't cache pointers to the input) + virtual void log_assert(const AssertData&) = 0; + // called for each message (don't cache pointers to the input) + virtual void log_message(const MessageData&) = 0; + + // called when a test case is skipped either because it doesn't pass the filters, has a skip decorator + // or isn't in the execution range (between first and last) (safe to cache a pointer to the input) + virtual void test_case_skipped(const TestCaseData&) = 0; + + // doctest will not be managing the lifetimes of reporters given to it but this would still be nice to have + virtual ~IReporter(); + + // can obtain all currently active contexts and stringify them if one wishes to do so + static int get_num_active_contexts(); + static const IContextScope* const* get_active_contexts(); + + // can iterate through contexts which have been stringified automatically in their destructors when an exception has been thrown + static int get_num_stringified_contexts(); + static const String* get_stringified_contexts(); +}; + +namespace detail { + typedef IReporter* (*reporterCreatorFunc)(const ContextOptions&); + + DOCTEST_INTERFACE void registerReporterImpl(const char* name, int prio, reporterCreatorFunc c, bool isReporter); + + template + IReporter* reporterCreator(const ContextOptions& o) { + return new Reporter(o); + } +} // namespace detail + +template +int registerReporter(const char* name, int priority, bool isReporter) { + detail::registerReporterImpl(name, priority, detail::reporterCreator, isReporter); + return 0; +} +} // namespace doctest + +// if registering is not disabled +#if !defined(DOCTEST_CONFIG_DISABLE) + +// common code in asserts - for convenience +#define DOCTEST_ASSERT_LOG_REACT_RETURN(b) \ + if(b.log()) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + b.react(); \ + return !b.m_failed + +#ifdef DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#define DOCTEST_WRAP_IN_TRY(x) x; +#else // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#define DOCTEST_WRAP_IN_TRY(x) \ + try { \ + x; \ + } catch(...) { DOCTEST_RB.translateException(); } +#endif // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS + +#ifdef DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS +#define DOCTEST_CAST_TO_VOID(...) \ + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wuseless-cast") \ + static_cast(__VA_ARGS__); \ + DOCTEST_GCC_SUPPRESS_WARNING_POP +#else // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS +#define DOCTEST_CAST_TO_VOID(...) __VA_ARGS__; +#endif // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS + +// registers the test by initializing a dummy var with a function +#define DOCTEST_REGISTER_FUNCTION(global_prefix, f, decorators) \ + global_prefix DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), \ + doctest::detail::regTest( \ + doctest::detail::TestCase( \ + f, __FILE__, __LINE__, \ + doctest_detail_test_suite_ns::getCurrentTestSuite()) * \ + decorators)) + +#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, decorators) \ + namespace { \ + struct der : public base \ + { \ + void f(); \ + }; \ + static void func() { \ + der v; \ + v.f(); \ + } \ + DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, func, decorators) \ + } \ + inline DOCTEST_NOINLINE void der::f() + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, decorators) \ + static void f(); \ + DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, f, decorators) \ + static void f() + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS(f, proxy, decorators) \ + static doctest::detail::funcType proxy() { return f; } \ + DOCTEST_REGISTER_FUNCTION(inline, proxy(), decorators) \ + static void f() + +// for registering tests +#define DOCTEST_TEST_CASE(decorators) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), decorators) + +// for registering tests in classes - requires C++17 for inline variables! +#if __cplusplus >= 201703L || (DOCTEST_MSVC >= DOCTEST_COMPILER(19, 12, 0) && _MSVC_LANG >= 201703L) +#define DOCTEST_TEST_CASE_CLASS(decorators) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_PROXY_), \ + decorators) +#else // DOCTEST_TEST_CASE_CLASS +#define DOCTEST_TEST_CASE_CLASS(...) \ + TEST_CASES_CAN_BE_REGISTERED_IN_CLASSES_ONLY_IN_CPP17_MODE_OR_WITH_VS_2017_OR_NEWER +#endif // DOCTEST_TEST_CASE_CLASS + +// for registering tests with a fixture +#define DOCTEST_TEST_CASE_FIXTURE(c, decorators) \ + DOCTEST_IMPLEMENT_FIXTURE(DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), c, \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), decorators) + +// for converting types to strings without the header and demangling +#define DOCTEST_TYPE_TO_STRING_IMPL(...) \ + template <> \ + inline const char* type_to_string<__VA_ARGS__>() { \ + return "<" #__VA_ARGS__ ">"; \ + } +#define DOCTEST_TYPE_TO_STRING(...) \ + namespace doctest { namespace detail { \ + DOCTEST_TYPE_TO_STRING_IMPL(__VA_ARGS__) \ + } \ + } \ + static_assert(true, "") + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, iter, func) \ + template \ + static void func(); \ + namespace { \ + template \ + struct iter; \ + template \ + struct iter> \ + { \ + iter(const char* file, unsigned line, int index) { \ + doctest::detail::regTest(doctest::detail::TestCase(func, file, line, \ + doctest_detail_test_suite_ns::getCurrentTestSuite(), \ + doctest::detail::type_to_string(), \ + int(line) * 1000 + index) \ + * dec); \ + iter>(file, line, index + 1); \ + } \ + }; \ + template <> \ + struct iter> \ + { \ + iter(const char*, unsigned, int) {} \ + }; \ + } \ + template \ + static void func() + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(dec, T, id) \ + DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(id, ITERATOR), \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)) + +#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, anon, ...) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_CAT(anon, DUMMY), \ + doctest::detail::instantiationHelper( \ + DOCTEST_CAT(id, ITERATOR)<__VA_ARGS__>(__FILE__, __LINE__, 0))) + +#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), std::tuple<__VA_ARGS__>) \ + static_assert(true, "") + +#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), __VA_ARGS__) \ + static_assert(true, "") + +#define DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, anon, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(anon, ITERATOR), anon); \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(anon, anon, std::tuple<__VA_ARGS__>) \ + template \ + static void anon() + +#define DOCTEST_TEST_CASE_TEMPLATE(dec, T, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), __VA_ARGS__) + +// for subcases +#define DOCTEST_SUBCASE(name) \ + if(const doctest::detail::Subcase & DOCTEST_ANONYMOUS(DOCTEST_ANON_SUBCASE_) DOCTEST_UNUSED = \ + doctest::detail::Subcase(name, __FILE__, __LINE__)) + +// for grouping tests in test suites by using code blocks +#define DOCTEST_TEST_SUITE_IMPL(decorators, ns_name) \ + namespace ns_name { namespace doctest_detail_test_suite_ns { \ + static DOCTEST_NOINLINE doctest::detail::TestSuite& getCurrentTestSuite() { \ + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4640) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors") \ + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wmissing-field-initializers") \ + static doctest::detail::TestSuite data{}; \ + static bool inited = false; \ + DOCTEST_MSVC_SUPPRESS_WARNING_POP \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP \ + DOCTEST_GCC_SUPPRESS_WARNING_POP \ + if(!inited) { \ + data* decorators; \ + inited = true; \ + } \ + return data; \ + } \ + } \ + } \ + namespace ns_name + +#define DOCTEST_TEST_SUITE(decorators) \ + DOCTEST_TEST_SUITE_IMPL(decorators, DOCTEST_ANONYMOUS(DOCTEST_ANON_SUITE_)) + +// for starting a testsuite block +#define DOCTEST_TEST_SUITE_BEGIN(decorators) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), \ + doctest::detail::setTestSuite(doctest::detail::TestSuite() * decorators)) \ + static_assert(true, "") + +// for ending a testsuite block +#define DOCTEST_TEST_SUITE_END \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), \ + doctest::detail::setTestSuite(doctest::detail::TestSuite() * "")) \ + typedef int DOCTEST_ANONYMOUS(DOCTEST_ANON_FOR_SEMICOLON_) + +// for registering exception translators +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(translatorName, signature) \ + inline doctest::String translatorName(signature); \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_), \ + doctest::registerExceptionTranslator(translatorName)) \ + doctest::String translatorName(signature) + +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \ + DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_), \ + signature) + +// for registering reporters +#define DOCTEST_REGISTER_REPORTER(name, priority, reporter) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_REPORTER_), \ + doctest::registerReporter(name, priority, true)) \ + static_assert(true, "") + +// for registering listeners +#define DOCTEST_REGISTER_LISTENER(name, priority, reporter) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_REPORTER_), \ + doctest::registerReporter(name, priority, false)) \ + static_assert(true, "") + +// clang-format off +// for logging - disabling formatting because it's important to have these on 2 separate lines - see PR #557 +#define DOCTEST_INFO(...) \ + DOCTEST_INFO_IMPL(DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_), \ + DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_OTHER_), \ + __VA_ARGS__) +// clang-format on + +#define DOCTEST_INFO_IMPL(mb_name, s_name, ...) \ + auto DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_) = doctest::detail::MakeContextScope( \ + [&](std::ostream* s_name) { \ + doctest::detail::MessageBuilder mb_name(__FILE__, __LINE__, doctest::assertType::is_warn); \ + mb_name.m_stream = s_name; \ + mb_name * __VA_ARGS__; \ + }) + +#define DOCTEST_CAPTURE(x) DOCTEST_INFO(#x " := ", x) + +#define DOCTEST_ADD_AT_IMPL(type, file, line, mb, ...) \ + [&] { \ + doctest::detail::MessageBuilder mb(file, line, doctest::assertType::type); \ + mb * __VA_ARGS__; \ + if(mb.log()) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + mb.react(); \ + }() + +// clang-format off +#define DOCTEST_ADD_MESSAGE_AT(file, line, ...) DOCTEST_ADD_AT_IMPL(is_warn, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) +#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, ...) DOCTEST_ADD_AT_IMPL(is_check, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) +#define DOCTEST_ADD_FAIL_AT(file, line, ...) DOCTEST_ADD_AT_IMPL(is_require, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) +// clang-format on + +#define DOCTEST_MESSAGE(...) DOCTEST_ADD_MESSAGE_AT(__FILE__, __LINE__, __VA_ARGS__) +#define DOCTEST_FAIL_CHECK(...) DOCTEST_ADD_FAIL_CHECK_AT(__FILE__, __LINE__, __VA_ARGS__) +#define DOCTEST_FAIL(...) DOCTEST_ADD_FAIL_AT(__FILE__, __LINE__, __VA_ARGS__) + +#define DOCTEST_TO_LVALUE(...) __VA_ARGS__ // Not removed to keep backwards compatibility. + +#ifndef DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_ASSERT_IMPLEMENT_2(assert_type, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY(DOCTEST_RB.setResult( \ + doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) \ + << __VA_ARGS__)) \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB) \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \ + [&] { \ + DOCTEST_ASSERT_IMPLEMENT_2(assert_type, __VA_ARGS__); \ + }() + +#else // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +// necessary for _MESSAGE +#define DOCTEST_ASSERT_IMPLEMENT_2 DOCTEST_ASSERT_IMPLEMENT_1 + +#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \ + doctest::detail::decomp_assert( \ + doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, \ + doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) \ + << __VA_ARGS__) DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_WARN(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN, __VA_ARGS__) +#define DOCTEST_CHECK(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK, __VA_ARGS__) +#define DOCTEST_REQUIRE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE, __VA_ARGS__) +#define DOCTEST_WARN_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN_FALSE, __VA_ARGS__) +#define DOCTEST_CHECK_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK_FALSE, __VA_ARGS__) +#define DOCTEST_REQUIRE_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE_FALSE, __VA_ARGS__) + +// clang-format off +#define DOCTEST_WARN_MESSAGE(cond, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN, cond); }() +#define DOCTEST_CHECK_MESSAGE(cond, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK, cond); }() +#define DOCTEST_REQUIRE_MESSAGE(cond, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE, cond); }() +#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN_FALSE, cond); }() +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK_FALSE, cond); }() +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE_FALSE, cond); }() +// clang-format on + +#define DOCTEST_ASSERT_THROWS_AS(expr, assert_type, message, ...) \ + [&] { \ + if(!doctest::getContextOptions()->no_throw) { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #expr, #__VA_ARGS__, message); \ + try { \ + DOCTEST_CAST_TO_VOID(expr) \ + } catch(const typename doctest::detail::remove_const< \ + typename doctest::detail::remove_reference<__VA_ARGS__>::type>::type&) { \ + DOCTEST_RB.translateException(); \ + DOCTEST_RB.m_threw_as = true; \ + } catch(...) { DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } else { \ + return false; \ + } \ + }() + +#define DOCTEST_ASSERT_THROWS_WITH(expr, expr_str, assert_type, ...) \ + [&] { \ + if(!doctest::getContextOptions()->no_throw) { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, expr_str, "", __VA_ARGS__); \ + try { \ + DOCTEST_CAST_TO_VOID(expr) \ + } catch(...) { DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } else { \ + return false; \ + } \ + }() + +#define DOCTEST_ASSERT_NOTHROW(assert_type, ...) \ + [&] { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + try { \ + DOCTEST_CAST_TO_VOID(__VA_ARGS__) \ + } catch(...) { DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + }() + +// clang-format off +#define DOCTEST_WARN_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_WARN_THROWS, "") +#define DOCTEST_CHECK_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_CHECK_THROWS, "") +#define DOCTEST_REQUIRE_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_REQUIRE_THROWS, "") + +#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_AS, "", __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_AS, "", __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_AS, "", __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_WARN_THROWS_WITH, __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_CHECK_THROWS_WITH, __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_REQUIRE_THROWS_WITH, __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_WITH_AS, message, __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_WITH_AS, message, __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_WITH_AS, message, __VA_ARGS__) + +#define DOCTEST_WARN_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_WARN_NOTHROW, __VA_ARGS__) +#define DOCTEST_CHECK_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_CHECK_NOTHROW, __VA_ARGS__) +#define DOCTEST_REQUIRE_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_REQUIRE_NOTHROW, __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS(expr); }() +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS(expr); }() +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS(expr); }() +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_AS(expr, ex); }() +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_AS(expr, ex); }() +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_AS(expr, ex); }() +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_WITH(expr, with); }() +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_WITH(expr, with); }() +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_WITH(expr, with); }() +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_WITH_AS(expr, with, ex); }() +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ex); }() +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ex); }() +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_NOTHROW(expr); }() +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_NOTHROW(expr); }() +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_NOTHROW(expr); }() +// clang-format on + +#ifndef DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_BINARY_ASSERT(assert_type, comp, ...) \ + [&] { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY( \ + DOCTEST_RB.binary_assert( \ + __VA_ARGS__)) \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + }() + +#define DOCTEST_UNARY_ASSERT(assert_type, ...) \ + [&] { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY(DOCTEST_RB.unary_assert(__VA_ARGS__)) \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + }() + +#else // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_BINARY_ASSERT(assert_type, comparison, ...) \ + doctest::detail::binary_assert( \ + doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, __VA_ARGS__) + +#define DOCTEST_UNARY_ASSERT(assert_type, ...) \ + doctest::detail::unary_assert(doctest::assertType::assert_type, __FILE__, __LINE__, \ + #__VA_ARGS__, __VA_ARGS__) + +#endif // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_WARN_EQ(...) DOCTEST_BINARY_ASSERT(DT_WARN_EQ, eq, __VA_ARGS__) +#define DOCTEST_CHECK_EQ(...) DOCTEST_BINARY_ASSERT(DT_CHECK_EQ, eq, __VA_ARGS__) +#define DOCTEST_REQUIRE_EQ(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_EQ, eq, __VA_ARGS__) +#define DOCTEST_WARN_NE(...) DOCTEST_BINARY_ASSERT(DT_WARN_NE, ne, __VA_ARGS__) +#define DOCTEST_CHECK_NE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_NE, ne, __VA_ARGS__) +#define DOCTEST_REQUIRE_NE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_NE, ne, __VA_ARGS__) +#define DOCTEST_WARN_GT(...) DOCTEST_BINARY_ASSERT(DT_WARN_GT, gt, __VA_ARGS__) +#define DOCTEST_CHECK_GT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GT, gt, __VA_ARGS__) +#define DOCTEST_REQUIRE_GT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GT, gt, __VA_ARGS__) +#define DOCTEST_WARN_LT(...) DOCTEST_BINARY_ASSERT(DT_WARN_LT, lt, __VA_ARGS__) +#define DOCTEST_CHECK_LT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LT, lt, __VA_ARGS__) +#define DOCTEST_REQUIRE_LT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LT, lt, __VA_ARGS__) +#define DOCTEST_WARN_GE(...) DOCTEST_BINARY_ASSERT(DT_WARN_GE, ge, __VA_ARGS__) +#define DOCTEST_CHECK_GE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GE, ge, __VA_ARGS__) +#define DOCTEST_REQUIRE_GE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GE, ge, __VA_ARGS__) +#define DOCTEST_WARN_LE(...) DOCTEST_BINARY_ASSERT(DT_WARN_LE, le, __VA_ARGS__) +#define DOCTEST_CHECK_LE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LE, le, __VA_ARGS__) +#define DOCTEST_REQUIRE_LE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LE, le, __VA_ARGS__) + +#define DOCTEST_WARN_UNARY(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY, __VA_ARGS__) +#define DOCTEST_CHECK_UNARY(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY, __VA_ARGS__) +#define DOCTEST_REQUIRE_UNARY(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY, __VA_ARGS__) +#define DOCTEST_WARN_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY_FALSE, __VA_ARGS__) +#define DOCTEST_CHECK_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY_FALSE, __VA_ARGS__) +#define DOCTEST_REQUIRE_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY_FALSE, __VA_ARGS__) + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS + +#undef DOCTEST_WARN_THROWS +#undef DOCTEST_CHECK_THROWS +#undef DOCTEST_REQUIRE_THROWS +#undef DOCTEST_WARN_THROWS_AS +#undef DOCTEST_CHECK_THROWS_AS +#undef DOCTEST_REQUIRE_THROWS_AS +#undef DOCTEST_WARN_THROWS_WITH +#undef DOCTEST_CHECK_THROWS_WITH +#undef DOCTEST_REQUIRE_THROWS_WITH +#undef DOCTEST_WARN_THROWS_WITH_AS +#undef DOCTEST_CHECK_THROWS_WITH_AS +#undef DOCTEST_REQUIRE_THROWS_WITH_AS +#undef DOCTEST_WARN_NOTHROW +#undef DOCTEST_CHECK_NOTHROW +#undef DOCTEST_REQUIRE_NOTHROW + +#undef DOCTEST_WARN_THROWS_MESSAGE +#undef DOCTEST_CHECK_THROWS_MESSAGE +#undef DOCTEST_REQUIRE_THROWS_MESSAGE +#undef DOCTEST_WARN_THROWS_AS_MESSAGE +#undef DOCTEST_CHECK_THROWS_AS_MESSAGE +#undef DOCTEST_REQUIRE_THROWS_AS_MESSAGE +#undef DOCTEST_WARN_THROWS_WITH_MESSAGE +#undef DOCTEST_CHECK_THROWS_WITH_MESSAGE +#undef DOCTEST_REQUIRE_THROWS_WITH_MESSAGE +#undef DOCTEST_WARN_THROWS_WITH_AS_MESSAGE +#undef DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE +#undef DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE +#undef DOCTEST_WARN_NOTHROW_MESSAGE +#undef DOCTEST_CHECK_NOTHROW_MESSAGE +#undef DOCTEST_REQUIRE_NOTHROW_MESSAGE + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#define DOCTEST_WARN_THROWS(...) ([] { return false; }) +#define DOCTEST_CHECK_THROWS(...) ([] { return false; }) +#define DOCTEST_REQUIRE_THROWS(...) ([] { return false; }) +#define DOCTEST_WARN_THROWS_AS(expr, ...) ([] { return false; }) +#define DOCTEST_CHECK_THROWS_AS(expr, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) ([] { return false; }) +#define DOCTEST_WARN_THROWS_WITH(expr, ...) ([] { return false; }) +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) ([] { return false; }) +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) ([] { return false; }) +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) ([] { return false; }) +#define DOCTEST_WARN_NOTHROW(...) ([] { return false; }) +#define DOCTEST_CHECK_NOTHROW(...) ([] { return false; }) +#define DOCTEST_REQUIRE_NOTHROW(...) ([] { return false; }) + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) ([] { return false; }) +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) ([] { return false; }) +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) ([] { return false; }) +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) ([] { return false; }) +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) ([] { return false; }) +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) ([] { return false; }) +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) ([] { return false; }) +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) ([] { return false; }) +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) ([] { return false; }) +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) ([] { return false; }) + +#else // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#undef DOCTEST_REQUIRE +#undef DOCTEST_REQUIRE_FALSE +#undef DOCTEST_REQUIRE_MESSAGE +#undef DOCTEST_REQUIRE_FALSE_MESSAGE +#undef DOCTEST_REQUIRE_EQ +#undef DOCTEST_REQUIRE_NE +#undef DOCTEST_REQUIRE_GT +#undef DOCTEST_REQUIRE_LT +#undef DOCTEST_REQUIRE_GE +#undef DOCTEST_REQUIRE_LE +#undef DOCTEST_REQUIRE_UNARY +#undef DOCTEST_REQUIRE_UNARY_FALSE + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +// ================================================================================================= +// == WHAT FOLLOWS IS VERSIONS OF THE MACROS THAT DO NOT DO ANY REGISTERING! == +// == THIS CAN BE ENABLED BY DEFINING DOCTEST_CONFIG_DISABLE GLOBALLY! == +// ================================================================================================= +#else // DOCTEST_CONFIG_DISABLE + +#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, name) \ + namespace { \ + template \ + struct der : public base \ + { void f(); }; \ + } \ + template \ + inline void der::f() + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, name) \ + template \ + static inline void f() + +// for registering tests +#define DOCTEST_TEST_CASE(name) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) + +// for registering tests in classes +#define DOCTEST_TEST_CASE_CLASS(name) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) + +// for registering tests with a fixture +#define DOCTEST_TEST_CASE_FIXTURE(x, name) \ + DOCTEST_IMPLEMENT_FIXTURE(DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), x, \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) + +// for converting types to strings without the header and demangling +#define DOCTEST_TYPE_TO_STRING(...) static_assert(true, "") +#define DOCTEST_TYPE_TO_STRING_IMPL(...) + +// for typed tests +#define DOCTEST_TEST_CASE_TEMPLATE(name, type, ...) \ + template \ + inline void DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)() + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(name, type, id) \ + template \ + inline void DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)() + +#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) static_assert(true, "") +#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) static_assert(true, "") + +// for subcases +#define DOCTEST_SUBCASE(name) + +// for a testsuite block +#define DOCTEST_TEST_SUITE(name) namespace + +// for starting a testsuite block +#define DOCTEST_TEST_SUITE_BEGIN(name) static_assert(true, "") + +// for ending a testsuite block +#define DOCTEST_TEST_SUITE_END typedef int DOCTEST_ANONYMOUS(DOCTEST_ANON_FOR_SEMICOLON_) + +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \ + template \ + static inline doctest::String DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_)(signature) + +#define DOCTEST_REGISTER_REPORTER(name, priority, reporter) +#define DOCTEST_REGISTER_LISTENER(name, priority, reporter) + +#define DOCTEST_INFO(...) (static_cast(0)) +#define DOCTEST_CAPTURE(x) (static_cast(0)) +#define DOCTEST_ADD_MESSAGE_AT(file, line, ...) (static_cast(0)) +#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, ...) (static_cast(0)) +#define DOCTEST_ADD_FAIL_AT(file, line, ...) (static_cast(0)) +#define DOCTEST_MESSAGE(...) (static_cast(0)) +#define DOCTEST_FAIL_CHECK(...) (static_cast(0)) +#define DOCTEST_FAIL(...) (static_cast(0)) + +#ifdef DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED + +#define DOCTEST_WARN(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_CHECK(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_REQUIRE(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_WARN_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_CHECK_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_FALSE(...) [&] { return !(__VA_ARGS__); }() + +#define DOCTEST_WARN_MESSAGE(cond, ...) [&] { return cond; }() +#define DOCTEST_CHECK_MESSAGE(cond, ...) [&] { return cond; }() +#define DOCTEST_REQUIRE_MESSAGE(cond, ...) [&] { return cond; }() +#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() + +namespace doctest { +namespace detail { +#define DOCTEST_RELATIONAL_OP(name, op) \ + template \ + bool name(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) { return lhs op rhs; } + + DOCTEST_RELATIONAL_OP(eq, ==) + DOCTEST_RELATIONAL_OP(ne, !=) + DOCTEST_RELATIONAL_OP(lt, <) + DOCTEST_RELATIONAL_OP(gt, >) + DOCTEST_RELATIONAL_OP(le, <=) + DOCTEST_RELATIONAL_OP(ge, >=) +} // namespace detail +} // namespace doctest + +#define DOCTEST_WARN_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() +#define DOCTEST_CHECK_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() +#define DOCTEST_WARN_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() +#define DOCTEST_CHECK_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() +#define DOCTEST_WARN_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() +#define DOCTEST_CHECK_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() +#define DOCTEST_WARN_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() +#define DOCTEST_CHECK_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() +#define DOCTEST_WARN_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() +#define DOCTEST_CHECK_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() +#define DOCTEST_WARN_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() +#define DOCTEST_CHECK_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() +#define DOCTEST_WARN_UNARY(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_CHECK_UNARY(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_REQUIRE_UNARY(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_WARN_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_CHECK_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() + +#else // DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED + +#define DOCTEST_WARN(...) ([] { return false; }) +#define DOCTEST_CHECK(...) ([] { return false; }) +#define DOCTEST_REQUIRE(...) ([] { return false; }) +#define DOCTEST_WARN_FALSE(...) ([] { return false; }) +#define DOCTEST_CHECK_FALSE(...) ([] { return false; }) +#define DOCTEST_REQUIRE_FALSE(...) ([] { return false; }) + +#define DOCTEST_WARN_MESSAGE(cond, ...) ([] { return false; }) +#define DOCTEST_CHECK_MESSAGE(cond, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_MESSAGE(cond, ...) ([] { return false; }) +#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) ([] { return false; }) +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) ([] { return false; }) + +#define DOCTEST_WARN_EQ(...) ([] { return false; }) +#define DOCTEST_CHECK_EQ(...) ([] { return false; }) +#define DOCTEST_REQUIRE_EQ(...) ([] { return false; }) +#define DOCTEST_WARN_NE(...) ([] { return false; }) +#define DOCTEST_CHECK_NE(...) ([] { return false; }) +#define DOCTEST_REQUIRE_NE(...) ([] { return false; }) +#define DOCTEST_WARN_GT(...) ([] { return false; }) +#define DOCTEST_CHECK_GT(...) ([] { return false; }) +#define DOCTEST_REQUIRE_GT(...) ([] { return false; }) +#define DOCTEST_WARN_LT(...) ([] { return false; }) +#define DOCTEST_CHECK_LT(...) ([] { return false; }) +#define DOCTEST_REQUIRE_LT(...) ([] { return false; }) +#define DOCTEST_WARN_GE(...) ([] { return false; }) +#define DOCTEST_CHECK_GE(...) ([] { return false; }) +#define DOCTEST_REQUIRE_GE(...) ([] { return false; }) +#define DOCTEST_WARN_LE(...) ([] { return false; }) +#define DOCTEST_CHECK_LE(...) ([] { return false; }) +#define DOCTEST_REQUIRE_LE(...) ([] { return false; }) + +#define DOCTEST_WARN_UNARY(...) ([] { return false; }) +#define DOCTEST_CHECK_UNARY(...) ([] { return false; }) +#define DOCTEST_REQUIRE_UNARY(...) ([] { return false; }) +#define DOCTEST_WARN_UNARY_FALSE(...) ([] { return false; }) +#define DOCTEST_CHECK_UNARY_FALSE(...) ([] { return false; }) +#define DOCTEST_REQUIRE_UNARY_FALSE(...) ([] { return false; }) + +#endif // DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED + +// TODO: think about if these also need to work properly even when doctest is disabled +#define DOCTEST_WARN_THROWS(...) ([] { return false; }) +#define DOCTEST_CHECK_THROWS(...) ([] { return false; }) +#define DOCTEST_REQUIRE_THROWS(...) ([] { return false; }) +#define DOCTEST_WARN_THROWS_AS(expr, ...) ([] { return false; }) +#define DOCTEST_CHECK_THROWS_AS(expr, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) ([] { return false; }) +#define DOCTEST_WARN_THROWS_WITH(expr, ...) ([] { return false; }) +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) ([] { return false; }) +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) ([] { return false; }) +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) ([] { return false; }) +#define DOCTEST_WARN_NOTHROW(...) ([] { return false; }) +#define DOCTEST_CHECK_NOTHROW(...) ([] { return false; }) +#define DOCTEST_REQUIRE_NOTHROW(...) ([] { return false; }) + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) ([] { return false; }) +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) ([] { return false; }) +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) ([] { return false; }) +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) ([] { return false; }) +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) ([] { return false; }) +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) ([] { return false; }) +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) ([] { return false; }) +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) ([] { return false; }) +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) ([] { return false; }) +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) ([] { return false; }) +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) ([] { return false; }) + +#endif // DOCTEST_CONFIG_DISABLE + +// clang-format off +// KEPT FOR BACKWARDS COMPATIBILITY - FORWARDING TO THE RIGHT MACROS +#define DOCTEST_FAST_WARN_EQ DOCTEST_WARN_EQ +#define DOCTEST_FAST_CHECK_EQ DOCTEST_CHECK_EQ +#define DOCTEST_FAST_REQUIRE_EQ DOCTEST_REQUIRE_EQ +#define DOCTEST_FAST_WARN_NE DOCTEST_WARN_NE +#define DOCTEST_FAST_CHECK_NE DOCTEST_CHECK_NE +#define DOCTEST_FAST_REQUIRE_NE DOCTEST_REQUIRE_NE +#define DOCTEST_FAST_WARN_GT DOCTEST_WARN_GT +#define DOCTEST_FAST_CHECK_GT DOCTEST_CHECK_GT +#define DOCTEST_FAST_REQUIRE_GT DOCTEST_REQUIRE_GT +#define DOCTEST_FAST_WARN_LT DOCTEST_WARN_LT +#define DOCTEST_FAST_CHECK_LT DOCTEST_CHECK_LT +#define DOCTEST_FAST_REQUIRE_LT DOCTEST_REQUIRE_LT +#define DOCTEST_FAST_WARN_GE DOCTEST_WARN_GE +#define DOCTEST_FAST_CHECK_GE DOCTEST_CHECK_GE +#define DOCTEST_FAST_REQUIRE_GE DOCTEST_REQUIRE_GE +#define DOCTEST_FAST_WARN_LE DOCTEST_WARN_LE +#define DOCTEST_FAST_CHECK_LE DOCTEST_CHECK_LE +#define DOCTEST_FAST_REQUIRE_LE DOCTEST_REQUIRE_LE + +#define DOCTEST_FAST_WARN_UNARY DOCTEST_WARN_UNARY +#define DOCTEST_FAST_CHECK_UNARY DOCTEST_CHECK_UNARY +#define DOCTEST_FAST_REQUIRE_UNARY DOCTEST_REQUIRE_UNARY +#define DOCTEST_FAST_WARN_UNARY_FALSE DOCTEST_WARN_UNARY_FALSE +#define DOCTEST_FAST_CHECK_UNARY_FALSE DOCTEST_CHECK_UNARY_FALSE +#define DOCTEST_FAST_REQUIRE_UNARY_FALSE DOCTEST_REQUIRE_UNARY_FALSE + +#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id,__VA_ARGS__) +// clang-format on + +// BDD style macros +// clang-format off +#define DOCTEST_SCENARIO(name) DOCTEST_TEST_CASE(" Scenario: " name) +#define DOCTEST_SCENARIO_CLASS(name) DOCTEST_TEST_CASE_CLASS(" Scenario: " name) +#define DOCTEST_SCENARIO_TEMPLATE(name, T, ...) DOCTEST_TEST_CASE_TEMPLATE(" Scenario: " name, T, __VA_ARGS__) +#define DOCTEST_SCENARIO_TEMPLATE_DEFINE(name, T, id) DOCTEST_TEST_CASE_TEMPLATE_DEFINE(" Scenario: " name, T, id) + +#define DOCTEST_GIVEN(name) DOCTEST_SUBCASE(" Given: " name) +#define DOCTEST_WHEN(name) DOCTEST_SUBCASE(" When: " name) +#define DOCTEST_AND_WHEN(name) DOCTEST_SUBCASE("And when: " name) +#define DOCTEST_THEN(name) DOCTEST_SUBCASE(" Then: " name) +#define DOCTEST_AND_THEN(name) DOCTEST_SUBCASE(" And: " name) +// clang-format on + +// == SHORT VERSIONS OF THE MACROS +#if !defined(DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES) + +#define TEST_CASE(name) DOCTEST_TEST_CASE(name) +#define TEST_CASE_CLASS(name) DOCTEST_TEST_CASE_CLASS(name) +#define TEST_CASE_FIXTURE(x, name) DOCTEST_TEST_CASE_FIXTURE(x, name) +#define TYPE_TO_STRING(...) DOCTEST_TYPE_TO_STRING(__VA_ARGS__) +#define TEST_CASE_TEMPLATE(name, T, ...) DOCTEST_TEST_CASE_TEMPLATE(name, T, __VA_ARGS__) +#define TEST_CASE_TEMPLATE_DEFINE(name, T, id) DOCTEST_TEST_CASE_TEMPLATE_DEFINE(name, T, id) +#define TEST_CASE_TEMPLATE_INVOKE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, __VA_ARGS__) +#define TEST_CASE_TEMPLATE_APPLY(id, ...) DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, __VA_ARGS__) +#define SUBCASE(name) DOCTEST_SUBCASE(name) +#define TEST_SUITE(decorators) DOCTEST_TEST_SUITE(decorators) +#define TEST_SUITE_BEGIN(name) DOCTEST_TEST_SUITE_BEGIN(name) +#define TEST_SUITE_END DOCTEST_TEST_SUITE_END +#define REGISTER_EXCEPTION_TRANSLATOR(signature) DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) +#define REGISTER_REPORTER(name, priority, reporter) DOCTEST_REGISTER_REPORTER(name, priority, reporter) +#define REGISTER_LISTENER(name, priority, reporter) DOCTEST_REGISTER_LISTENER(name, priority, reporter) +#define INFO(...) DOCTEST_INFO(__VA_ARGS__) +#define CAPTURE(x) DOCTEST_CAPTURE(x) +#define ADD_MESSAGE_AT(file, line, ...) DOCTEST_ADD_MESSAGE_AT(file, line, __VA_ARGS__) +#define ADD_FAIL_CHECK_AT(file, line, ...) DOCTEST_ADD_FAIL_CHECK_AT(file, line, __VA_ARGS__) +#define ADD_FAIL_AT(file, line, ...) DOCTEST_ADD_FAIL_AT(file, line, __VA_ARGS__) +#define MESSAGE(...) DOCTEST_MESSAGE(__VA_ARGS__) +#define FAIL_CHECK(...) DOCTEST_FAIL_CHECK(__VA_ARGS__) +#define FAIL(...) DOCTEST_FAIL(__VA_ARGS__) +#define TO_LVALUE(...) DOCTEST_TO_LVALUE(__VA_ARGS__) + +#define WARN(...) DOCTEST_WARN(__VA_ARGS__) +#define WARN_FALSE(...) DOCTEST_WARN_FALSE(__VA_ARGS__) +#define WARN_THROWS(...) DOCTEST_WARN_THROWS(__VA_ARGS__) +#define WARN_THROWS_AS(expr, ...) DOCTEST_WARN_THROWS_AS(expr, __VA_ARGS__) +#define WARN_THROWS_WITH(expr, ...) DOCTEST_WARN_THROWS_WITH(expr, __VA_ARGS__) +#define WARN_THROWS_WITH_AS(expr, with, ...) DOCTEST_WARN_THROWS_WITH_AS(expr, with, __VA_ARGS__) +#define WARN_NOTHROW(...) DOCTEST_WARN_NOTHROW(__VA_ARGS__) +#define CHECK(...) DOCTEST_CHECK(__VA_ARGS__) +#define CHECK_FALSE(...) DOCTEST_CHECK_FALSE(__VA_ARGS__) +#define CHECK_THROWS(...) DOCTEST_CHECK_THROWS(__VA_ARGS__) +#define CHECK_THROWS_AS(expr, ...) DOCTEST_CHECK_THROWS_AS(expr, __VA_ARGS__) +#define CHECK_THROWS_WITH(expr, ...) DOCTEST_CHECK_THROWS_WITH(expr, __VA_ARGS__) +#define CHECK_THROWS_WITH_AS(expr, with, ...) DOCTEST_CHECK_THROWS_WITH_AS(expr, with, __VA_ARGS__) +#define CHECK_NOTHROW(...) DOCTEST_CHECK_NOTHROW(__VA_ARGS__) +#define REQUIRE(...) DOCTEST_REQUIRE(__VA_ARGS__) +#define REQUIRE_FALSE(...) DOCTEST_REQUIRE_FALSE(__VA_ARGS__) +#define REQUIRE_THROWS(...) DOCTEST_REQUIRE_THROWS(__VA_ARGS__) +#define REQUIRE_THROWS_AS(expr, ...) DOCTEST_REQUIRE_THROWS_AS(expr, __VA_ARGS__) +#define REQUIRE_THROWS_WITH(expr, ...) DOCTEST_REQUIRE_THROWS_WITH(expr, __VA_ARGS__) +#define REQUIRE_THROWS_WITH_AS(expr, with, ...) DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, __VA_ARGS__) +#define REQUIRE_NOTHROW(...) DOCTEST_REQUIRE_NOTHROW(__VA_ARGS__) + +#define WARN_MESSAGE(cond, ...) DOCTEST_WARN_MESSAGE(cond, __VA_ARGS__) +#define WARN_FALSE_MESSAGE(cond, ...) DOCTEST_WARN_FALSE_MESSAGE(cond, __VA_ARGS__) +#define WARN_THROWS_MESSAGE(expr, ...) DOCTEST_WARN_THROWS_MESSAGE(expr, __VA_ARGS__) +#define WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) +#define WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) +#define WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) +#define WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_WARN_NOTHROW_MESSAGE(expr, __VA_ARGS__) +#define CHECK_MESSAGE(cond, ...) DOCTEST_CHECK_MESSAGE(cond, __VA_ARGS__) +#define CHECK_FALSE_MESSAGE(cond, ...) DOCTEST_CHECK_FALSE_MESSAGE(cond, __VA_ARGS__) +#define CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_CHECK_THROWS_MESSAGE(expr, __VA_ARGS__) +#define CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) +#define CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) +#define CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) +#define CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_CHECK_NOTHROW_MESSAGE(expr, __VA_ARGS__) +#define REQUIRE_MESSAGE(cond, ...) DOCTEST_REQUIRE_MESSAGE(cond, __VA_ARGS__) +#define REQUIRE_FALSE_MESSAGE(cond, ...) DOCTEST_REQUIRE_FALSE_MESSAGE(cond, __VA_ARGS__) +#define REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_REQUIRE_THROWS_MESSAGE(expr, __VA_ARGS__) +#define REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) +#define REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) +#define REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) +#define REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, __VA_ARGS__) + +#define SCENARIO(name) DOCTEST_SCENARIO(name) +#define SCENARIO_CLASS(name) DOCTEST_SCENARIO_CLASS(name) +#define SCENARIO_TEMPLATE(name, T, ...) DOCTEST_SCENARIO_TEMPLATE(name, T, __VA_ARGS__) +#define SCENARIO_TEMPLATE_DEFINE(name, T, id) DOCTEST_SCENARIO_TEMPLATE_DEFINE(name, T, id) +#define GIVEN(name) DOCTEST_GIVEN(name) +#define WHEN(name) DOCTEST_WHEN(name) +#define AND_WHEN(name) DOCTEST_AND_WHEN(name) +#define THEN(name) DOCTEST_THEN(name) +#define AND_THEN(name) DOCTEST_AND_THEN(name) + +#define WARN_EQ(...) DOCTEST_WARN_EQ(__VA_ARGS__) +#define CHECK_EQ(...) DOCTEST_CHECK_EQ(__VA_ARGS__) +#define REQUIRE_EQ(...) DOCTEST_REQUIRE_EQ(__VA_ARGS__) +#define WARN_NE(...) DOCTEST_WARN_NE(__VA_ARGS__) +#define CHECK_NE(...) DOCTEST_CHECK_NE(__VA_ARGS__) +#define REQUIRE_NE(...) DOCTEST_REQUIRE_NE(__VA_ARGS__) +#define WARN_GT(...) DOCTEST_WARN_GT(__VA_ARGS__) +#define CHECK_GT(...) DOCTEST_CHECK_GT(__VA_ARGS__) +#define REQUIRE_GT(...) DOCTEST_REQUIRE_GT(__VA_ARGS__) +#define WARN_LT(...) DOCTEST_WARN_LT(__VA_ARGS__) +#define CHECK_LT(...) DOCTEST_CHECK_LT(__VA_ARGS__) +#define REQUIRE_LT(...) DOCTEST_REQUIRE_LT(__VA_ARGS__) +#define WARN_GE(...) DOCTEST_WARN_GE(__VA_ARGS__) +#define CHECK_GE(...) DOCTEST_CHECK_GE(__VA_ARGS__) +#define REQUIRE_GE(...) DOCTEST_REQUIRE_GE(__VA_ARGS__) +#define WARN_LE(...) DOCTEST_WARN_LE(__VA_ARGS__) +#define CHECK_LE(...) DOCTEST_CHECK_LE(__VA_ARGS__) +#define REQUIRE_LE(...) DOCTEST_REQUIRE_LE(__VA_ARGS__) +#define WARN_UNARY(...) DOCTEST_WARN_UNARY(__VA_ARGS__) +#define CHECK_UNARY(...) DOCTEST_CHECK_UNARY(__VA_ARGS__) +#define REQUIRE_UNARY(...) DOCTEST_REQUIRE_UNARY(__VA_ARGS__) +#define WARN_UNARY_FALSE(...) DOCTEST_WARN_UNARY_FALSE(__VA_ARGS__) +#define CHECK_UNARY_FALSE(...) DOCTEST_CHECK_UNARY_FALSE(__VA_ARGS__) +#define REQUIRE_UNARY_FALSE(...) DOCTEST_REQUIRE_UNARY_FALSE(__VA_ARGS__) + +// KEPT FOR BACKWARDS COMPATIBILITY +#define FAST_WARN_EQ(...) DOCTEST_FAST_WARN_EQ(__VA_ARGS__) +#define FAST_CHECK_EQ(...) DOCTEST_FAST_CHECK_EQ(__VA_ARGS__) +#define FAST_REQUIRE_EQ(...) DOCTEST_FAST_REQUIRE_EQ(__VA_ARGS__) +#define FAST_WARN_NE(...) DOCTEST_FAST_WARN_NE(__VA_ARGS__) +#define FAST_CHECK_NE(...) DOCTEST_FAST_CHECK_NE(__VA_ARGS__) +#define FAST_REQUIRE_NE(...) DOCTEST_FAST_REQUIRE_NE(__VA_ARGS__) +#define FAST_WARN_GT(...) DOCTEST_FAST_WARN_GT(__VA_ARGS__) +#define FAST_CHECK_GT(...) DOCTEST_FAST_CHECK_GT(__VA_ARGS__) +#define FAST_REQUIRE_GT(...) DOCTEST_FAST_REQUIRE_GT(__VA_ARGS__) +#define FAST_WARN_LT(...) DOCTEST_FAST_WARN_LT(__VA_ARGS__) +#define FAST_CHECK_LT(...) DOCTEST_FAST_CHECK_LT(__VA_ARGS__) +#define FAST_REQUIRE_LT(...) DOCTEST_FAST_REQUIRE_LT(__VA_ARGS__) +#define FAST_WARN_GE(...) DOCTEST_FAST_WARN_GE(__VA_ARGS__) +#define FAST_CHECK_GE(...) DOCTEST_FAST_CHECK_GE(__VA_ARGS__) +#define FAST_REQUIRE_GE(...) DOCTEST_FAST_REQUIRE_GE(__VA_ARGS__) +#define FAST_WARN_LE(...) DOCTEST_FAST_WARN_LE(__VA_ARGS__) +#define FAST_CHECK_LE(...) DOCTEST_FAST_CHECK_LE(__VA_ARGS__) +#define FAST_REQUIRE_LE(...) DOCTEST_FAST_REQUIRE_LE(__VA_ARGS__) + +#define FAST_WARN_UNARY(...) DOCTEST_FAST_WARN_UNARY(__VA_ARGS__) +#define FAST_CHECK_UNARY(...) DOCTEST_FAST_CHECK_UNARY(__VA_ARGS__) +#define FAST_REQUIRE_UNARY(...) DOCTEST_FAST_REQUIRE_UNARY(__VA_ARGS__) +#define FAST_WARN_UNARY_FALSE(...) DOCTEST_FAST_WARN_UNARY_FALSE(__VA_ARGS__) +#define FAST_CHECK_UNARY_FALSE(...) DOCTEST_FAST_CHECK_UNARY_FALSE(__VA_ARGS__) +#define FAST_REQUIRE_UNARY_FALSE(...) DOCTEST_FAST_REQUIRE_UNARY_FALSE(__VA_ARGS__) + +#define TEST_CASE_TEMPLATE_INSTANTIATE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE(id, __VA_ARGS__) + +#endif // DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES + +#if !defined(DOCTEST_CONFIG_DISABLE) + +// this is here to clear the 'current test suite' for the current translation unit - at the top +DOCTEST_TEST_SUITE_END(); + +// add stringification for primitive/fundamental types +namespace doctest { namespace detail { + DOCTEST_TYPE_TO_STRING_IMPL(bool) + DOCTEST_TYPE_TO_STRING_IMPL(float) + DOCTEST_TYPE_TO_STRING_IMPL(double) + DOCTEST_TYPE_TO_STRING_IMPL(long double) + DOCTEST_TYPE_TO_STRING_IMPL(char) + DOCTEST_TYPE_TO_STRING_IMPL(signed char) + DOCTEST_TYPE_TO_STRING_IMPL(unsigned char) +#if !DOCTEST_MSVC || defined(_NATIVE_WCHAR_T_DEFINED) + DOCTEST_TYPE_TO_STRING_IMPL(wchar_t) +#endif // not MSVC or wchar_t support enabled + DOCTEST_TYPE_TO_STRING_IMPL(short int) + DOCTEST_TYPE_TO_STRING_IMPL(unsigned short int) + DOCTEST_TYPE_TO_STRING_IMPL(int) + DOCTEST_TYPE_TO_STRING_IMPL(unsigned int) + DOCTEST_TYPE_TO_STRING_IMPL(long int) + DOCTEST_TYPE_TO_STRING_IMPL(unsigned long int) + DOCTEST_TYPE_TO_STRING_IMPL(long long int) + DOCTEST_TYPE_TO_STRING_IMPL(unsigned long long int) +}} // namespace doctest::detail + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_MSVC_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +DOCTEST_SUPPRESS_COMMON_WARNINGS_POP + +#endif // DOCTEST_LIBRARY_INCLUDED + +#ifndef DOCTEST_SINGLE_HEADER +#define DOCTEST_SINGLE_HEADER +#endif // DOCTEST_SINGLE_HEADER + +#if defined(DOCTEST_CONFIG_IMPLEMENT) || !defined(DOCTEST_SINGLE_HEADER) + +#ifndef DOCTEST_SINGLE_HEADER +#include "doctest_fwd.h" +#endif // DOCTEST_SINGLE_HEADER + +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-macros") + +#ifndef DOCTEST_LIBRARY_IMPLEMENTATION +#define DOCTEST_LIBRARY_IMPLEMENTATION + +DOCTEST_CLANG_SUPPRESS_WARNING_POP + +DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH + +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wglobal-constructors") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wshorten-64-to-32") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-variable-declarations") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch-enum") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wcovered-switch-default") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-noreturn") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wdisabled-macro-expansion") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-braces") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-field-initializers") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-member-function") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wnonportable-system-include-path") + +DOCTEST_GCC_SUPPRESS_WARNING_PUSH +DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-field-initializers") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-braces") +DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch") +DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-enum") +DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-default") +DOCTEST_GCC_SUPPRESS_WARNING("-Wunsafe-loop-optimizations") +DOCTEST_GCC_SUPPRESS_WARNING("-Wold-style-cast") +DOCTEST_GCC_SUPPRESS_WARNING("-Wunused-function") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmultiple-inheritance") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsuggest-attribute") + +DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +DOCTEST_MSVC_SUPPRESS_WARNING(4267) // 'var' : conversion from 'x' to 'y', possible loss of data +DOCTEST_MSVC_SUPPRESS_WARNING(4530) // C++ exception handler used, but unwind semantics not enabled +DOCTEST_MSVC_SUPPRESS_WARNING(4577) // 'noexcept' used with no exception handling mode specified +DOCTEST_MSVC_SUPPRESS_WARNING(4774) // format string expected in argument is not a string literal +DOCTEST_MSVC_SUPPRESS_WARNING(4365) // conversion from 'int' to 'unsigned', signed/unsigned mismatch +DOCTEST_MSVC_SUPPRESS_WARNING(5039) // pointer to potentially throwing function passed to extern C +DOCTEST_MSVC_SUPPRESS_WARNING(4800) // forcing value to bool 'true' or 'false' (performance warning) +DOCTEST_MSVC_SUPPRESS_WARNING(5245) // unreferenced function with internal linkage has been removed + +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN + +// required includes - will go only in one translation unit! +#include +#include +#include +// borland (Embarcadero) compiler requires math.h and not cmath - https://github.com/doctest/doctest/pull/37 +#ifdef __BORLANDC__ +#include +#endif // __BORLANDC__ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef DOCTEST_PLATFORM_MAC +#include +#include +#include +#endif // DOCTEST_PLATFORM_MAC + +#ifdef DOCTEST_PLATFORM_WINDOWS + +// defines for a leaner windows.h +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif // WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif // NOMINMAX + +// not sure what AfxWin.h is for - here I do what Catch does +#ifdef __AFXDLL +#include +#else +#include +#endif +#include + +#else // DOCTEST_PLATFORM_WINDOWS + +#include +#include + +#endif // DOCTEST_PLATFORM_WINDOWS + +// this is a fix for https://github.com/doctest/doctest/issues/348 +// https://mail.gnome.org/archives/xml/2012-January/msg00000.html +#if !defined(HAVE_UNISTD_H) && !defined(STDOUT_FILENO) +#define STDOUT_FILENO fileno(stdout) +#endif // HAVE_UNISTD_H + +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END + +// counts the number of elements in a C array +#define DOCTEST_COUNTOF(x) (sizeof(x) / sizeof(x[0])) + +#ifdef DOCTEST_CONFIG_DISABLE +#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_disabled +#else // DOCTEST_CONFIG_DISABLE +#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_not_disabled +#endif // DOCTEST_CONFIG_DISABLE + +#ifndef DOCTEST_CONFIG_OPTIONS_PREFIX +#define DOCTEST_CONFIG_OPTIONS_PREFIX "dt-" +#endif + +#ifndef DOCTEST_THREAD_LOCAL +#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_THREAD_LOCAL +#else // DOCTEST_MSVC +#define DOCTEST_THREAD_LOCAL thread_local +#endif // DOCTEST_MSVC +#endif // DOCTEST_THREAD_LOCAL + +#ifndef DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES +#define DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES 32 +#endif + +#ifndef DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE +#define DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE 64 +#endif + +#ifdef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS +#define DOCTEST_OPTIONS_PREFIX_DISPLAY DOCTEST_CONFIG_OPTIONS_PREFIX +#else +#define DOCTEST_OPTIONS_PREFIX_DISPLAY "" +#endif + +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) +#define DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS +#endif + +#ifndef DOCTEST_CDECL +#define DOCTEST_CDECL __cdecl +#endif + +namespace doctest { + +bool is_running_in_test = false; + +namespace { + using namespace detail; + + template + DOCTEST_NORETURN void throw_exception(Ex const& e) { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + throw e; +#else // DOCTEST_CONFIG_NO_EXCEPTIONS + std::cerr << "doctest will terminate because it needed to throw an exception.\n" + << "The message was: " << e.what() << '\n'; + std::terminate(); +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + } + +#ifndef DOCTEST_INTERNAL_ERROR +#define DOCTEST_INTERNAL_ERROR(msg) \ + throw_exception(std::logic_error( \ + __FILE__ ":" DOCTEST_TOSTR(__LINE__) ": Internal doctest error: " msg)) +#endif // DOCTEST_INTERNAL_ERROR + + // case insensitive strcmp + int stricmp(const char* a, const char* b) { + for(;; a++, b++) { + const int d = tolower(*a) - tolower(*b); + if(d != 0 || !*a) + return d; + } + } + + template + String fpToString(T value, int precision) { + std::ostringstream oss; + oss << std::setprecision(precision) << std::fixed << value; + std::string d = oss.str(); + size_t i = d.find_last_not_of('0'); + if(i != std::string::npos && i != d.size() - 1) { + if(d[i] == '.') + i++; + d = d.substr(0, i + 1); + } + return d.c_str(); + } + + struct Endianness + { + enum Arch + { + Big, + Little + }; + + static Arch which() { + int x = 1; + // casting any data pointer to char* is allowed + auto ptr = reinterpret_cast(&x); + if(*ptr) + return Little; + return Big; + } + }; +} // namespace + +namespace detail { + String rawMemoryToString(const void* object, unsigned size) { + // Reverse order for little endian architectures + int i = 0, end = static_cast(size), inc = 1; + if(Endianness::which() == Endianness::Little) { + i = end - 1; + end = inc = -1; + } + + unsigned const char* bytes = static_cast(object); + std::ostream* oss = tlssPush(); + *oss << "0x" << std::setfill('0') << std::hex; + for(; i != end; i += inc) + *oss << std::setw(2) << static_cast(bytes[i]); + return tlssPop(); + } + + DOCTEST_THREAD_LOCAL class + { + std::vector stack; + std::stringstream ss; + + public: + std::ostream* push() { + stack.push_back(ss.tellp()); + return &ss; + } + + String pop() { + if (stack.empty()) + DOCTEST_INTERNAL_ERROR("TLSS was empty when trying to pop!"); + + std::streampos pos = stack.back(); + stack.pop_back(); + unsigned sz = static_cast(ss.tellp() - pos); + ss.rdbuf()->pubseekpos(pos, std::ios::in | std::ios::out); + return String(ss, sz); + } + } g_oss; + + std::ostream* tlssPush() { + return g_oss.push(); + } + + String tlssPop() { + return g_oss.pop(); + } + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace timer_large_integer +{ + +#if defined(DOCTEST_PLATFORM_WINDOWS) + typedef ULONGLONG type; +#else // DOCTEST_PLATFORM_WINDOWS + typedef std::uint64_t type; +#endif // DOCTEST_PLATFORM_WINDOWS +} + +typedef timer_large_integer::type ticks_t; + +#ifdef DOCTEST_CONFIG_GETCURRENTTICKS + ticks_t getCurrentTicks() { return DOCTEST_CONFIG_GETCURRENTTICKS(); } +#elif defined(DOCTEST_PLATFORM_WINDOWS) + ticks_t getCurrentTicks() { + static LARGE_INTEGER hz = {0}, hzo = {0}; + if(!hz.QuadPart) { + QueryPerformanceFrequency(&hz); + QueryPerformanceCounter(&hzo); + } + LARGE_INTEGER t; + QueryPerformanceCounter(&t); + return ((t.QuadPart - hzo.QuadPart) * LONGLONG(1000000)) / hz.QuadPart; + } +#else // DOCTEST_PLATFORM_WINDOWS + ticks_t getCurrentTicks() { + timeval t; + gettimeofday(&t, nullptr); + return static_cast(t.tv_sec) * 1000000 + static_cast(t.tv_usec); + } +#endif // DOCTEST_PLATFORM_WINDOWS + + struct Timer + { + void start() { m_ticks = getCurrentTicks(); } + unsigned int getElapsedMicroseconds() const { + return static_cast(getCurrentTicks() - m_ticks); + } + //unsigned int getElapsedMilliseconds() const { + // return static_cast(getElapsedMicroseconds() / 1000); + //} + double getElapsedSeconds() const { return static_cast(getCurrentTicks() - m_ticks) / 1000000.0; } + + private: + ticks_t m_ticks = 0; + }; + +#ifdef DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS + template + using AtomicOrMultiLaneAtomic = std::atomic; +#else // DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS + // Provides a multilane implementation of an atomic variable that supports add, sub, load, + // store. Instead of using a single atomic variable, this splits up into multiple ones, + // each sitting on a separate cache line. The goal is to provide a speedup when most + // operations are modifying. It achieves this with two properties: + // + // * Multiple atomics are used, so chance of congestion from the same atomic is reduced. + // * Each atomic sits on a separate cache line, so false sharing is reduced. + // + // The disadvantage is that there is a small overhead due to the use of TLS, and load/store + // is slower because all atomics have to be accessed. + template + class MultiLaneAtomic + { + struct CacheLineAlignedAtomic + { + std::atomic atomic{}; + char padding[DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE - sizeof(std::atomic)]; + }; + CacheLineAlignedAtomic m_atomics[DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES]; + + static_assert(sizeof(CacheLineAlignedAtomic) == DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE, + "guarantee one atomic takes exactly one cache line"); + + public: + T operator++() DOCTEST_NOEXCEPT { return fetch_add(1) + 1; } + + T operator++(int) DOCTEST_NOEXCEPT { return fetch_add(1); } + + T fetch_add(T arg, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { + return myAtomic().fetch_add(arg, order); + } + + T fetch_sub(T arg, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { + return myAtomic().fetch_sub(arg, order); + } + + operator T() const DOCTEST_NOEXCEPT { return load(); } + + T load(std::memory_order order = std::memory_order_seq_cst) const DOCTEST_NOEXCEPT { + auto result = T(); + for(auto const& c : m_atomics) { + result += c.atomic.load(order); + } + return result; + } + + T operator=(T desired) DOCTEST_NOEXCEPT { // lgtm [cpp/assignment-does-not-return-this] + store(desired); + return desired; + } + + void store(T desired, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { + // first value becomes desired", all others become 0. + for(auto& c : m_atomics) { + c.atomic.store(desired, order); + desired = {}; + } + } + + private: + // Each thread has a different atomic that it operates on. If more than NumLanes threads + // use this, some will use the same atomic. So performance will degrade a bit, but still + // everything will work. + // + // The logic here is a bit tricky. The call should be as fast as possible, so that there + // is minimal to no overhead in determining the correct atomic for the current thread. + // + // 1. A global static counter laneCounter counts continuously up. + // 2. Each successive thread will use modulo operation of that counter so it gets an atomic + // assigned in a round-robin fashion. + // 3. This tlsLaneIdx is stored in the thread local data, so it is directly available with + // little overhead. + std::atomic& myAtomic() DOCTEST_NOEXCEPT { + static std::atomic laneCounter; + DOCTEST_THREAD_LOCAL size_t tlsLaneIdx = + laneCounter++ % DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES; + + return m_atomics[tlsLaneIdx].atomic; + } + }; + + template + using AtomicOrMultiLaneAtomic = MultiLaneAtomic; +#endif // DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS + + // this holds both parameters from the command line and runtime data for tests + struct ContextState : ContextOptions, TestRunStats, CurrentTestCaseStats + { + AtomicOrMultiLaneAtomic numAssertsCurrentTest_atomic; + AtomicOrMultiLaneAtomic numAssertsFailedCurrentTest_atomic; + + std::vector> filters = decltype(filters)(9); // 9 different filters + + std::vector reporters_currently_used; + + assert_handler ah = nullptr; + + Timer timer; + + std::vector stringifiedContexts; // logging from INFO() due to an exception + + // stuff for subcases + std::vector subcasesStack; + std::set subcasesPassed; + int subcasesCurrentMaxLevel; + bool should_reenter; + std::atomic shouldLogCurrentException; + + void resetRunData() { + numTestCases = 0; + numTestCasesPassingFilters = 0; + numTestSuitesPassingFilters = 0; + numTestCasesFailed = 0; + numAsserts = 0; + numAssertsFailed = 0; + numAssertsCurrentTest = 0; + numAssertsFailedCurrentTest = 0; + } + + void finalizeTestCaseData() { + seconds = timer.getElapsedSeconds(); + + // update the non-atomic counters + numAsserts += numAssertsCurrentTest_atomic; + numAssertsFailed += numAssertsFailedCurrentTest_atomic; + numAssertsCurrentTest = numAssertsCurrentTest_atomic; + numAssertsFailedCurrentTest = numAssertsFailedCurrentTest_atomic; + + if(numAssertsFailedCurrentTest) + failure_flags |= TestCaseFailureReason::AssertFailure; + + if(Approx(currentTest->m_timeout).epsilon(DBL_EPSILON) != 0 && + Approx(seconds).epsilon(DBL_EPSILON) > currentTest->m_timeout) + failure_flags |= TestCaseFailureReason::Timeout; + + if(currentTest->m_should_fail) { + if(failure_flags) { + failure_flags |= TestCaseFailureReason::ShouldHaveFailedAndDid; + } else { + failure_flags |= TestCaseFailureReason::ShouldHaveFailedButDidnt; + } + } else if(failure_flags && currentTest->m_may_fail) { + failure_flags |= TestCaseFailureReason::CouldHaveFailedAndDid; + } else if(currentTest->m_expected_failures > 0) { + if(numAssertsFailedCurrentTest == currentTest->m_expected_failures) { + failure_flags |= TestCaseFailureReason::FailedExactlyNumTimes; + } else { + failure_flags |= TestCaseFailureReason::DidntFailExactlyNumTimes; + } + } + + bool ok_to_fail = (TestCaseFailureReason::ShouldHaveFailedAndDid & failure_flags) || + (TestCaseFailureReason::CouldHaveFailedAndDid & failure_flags) || + (TestCaseFailureReason::FailedExactlyNumTimes & failure_flags); + + // if any subcase has failed - the whole test case has failed + testCaseSuccess = !(failure_flags && !ok_to_fail); + if(!testCaseSuccess) + numTestCasesFailed++; + } + }; + + ContextState* g_cs = nullptr; + + // used to avoid locks for the debug output + // TODO: figure out if this is indeed necessary/correct - seems like either there still + // could be a race or that there wouldn't be a race even if using the context directly + DOCTEST_THREAD_LOCAL bool g_no_colors; + +#endif // DOCTEST_CONFIG_DISABLE +} // namespace detail + +char* String::allocate(unsigned sz) { + if (sz <= last) { + buf[sz] = '\0'; + setLast(last - sz); + return buf; + } else { + setOnHeap(); + data.size = sz; + data.capacity = data.size + 1; + data.ptr = new char[data.capacity]; + data.ptr[sz] = '\0'; + return data.ptr; + } +} + +void String::setOnHeap() { *reinterpret_cast(&buf[last]) = 128; } +void String::setLast(unsigned in) { buf[last] = char(in); } + +void String::copy(const String& other) { + if(other.isOnStack()) { + memcpy(buf, other.buf, len); + } else { + memcpy(allocate(other.data.size), other.data.ptr, other.data.size); + } +} + +String::String() { + buf[0] = '\0'; + setLast(); +} + +String::~String() { + if(!isOnStack()) + delete[] data.ptr; + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) +} + +String::String(const char* in) + : String(in, strlen(in)) {} + +String::String(const char* in, unsigned in_size) { + memcpy(allocate(in_size), in, in_size); +} + +String::String(std::istream& in, unsigned in_size) { + in.read(allocate(in_size), in_size); +} + +String::String(const String& other) { copy(other); } + +String& String::operator=(const String& other) { + if(this != &other) { + if(!isOnStack()) + delete[] data.ptr; + + copy(other); + } + + return *this; +} + +String& String::operator+=(const String& other) { + const unsigned my_old_size = size(); + const unsigned other_size = other.size(); + const unsigned total_size = my_old_size + other_size; + if(isOnStack()) { + if(total_size < len) { + // append to the current stack space + memcpy(buf + my_old_size, other.c_str(), other_size + 1); + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) + setLast(last - total_size); + } else { + // alloc new chunk + char* temp = new char[total_size + 1]; + // copy current data to new location before writing in the union + memcpy(temp, buf, my_old_size); // skip the +1 ('\0') for speed + // update data in union + setOnHeap(); + data.size = total_size; + data.capacity = data.size + 1; + data.ptr = temp; + // transfer the rest of the data + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } + } else { + if(data.capacity > total_size) { + // append to the current heap block + data.size = total_size; + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } else { + // resize + data.capacity *= 2; + if(data.capacity <= total_size) + data.capacity = total_size + 1; + // alloc new chunk + char* temp = new char[data.capacity]; + // copy current data to new location before releasing it + memcpy(temp, data.ptr, my_old_size); // skip the +1 ('\0') for speed + // release old chunk + delete[] data.ptr; + // update the rest of the union members + data.size = total_size; + data.ptr = temp; + // transfer the rest of the data + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } + } + + return *this; +} + +String::String(String&& other) { + memcpy(buf, other.buf, len); + other.buf[0] = '\0'; + other.setLast(); +} + +String& String::operator=(String&& other) { + if(this != &other) { + if(!isOnStack()) + delete[] data.ptr; + memcpy(buf, other.buf, len); + other.buf[0] = '\0'; + other.setLast(); + } + return *this; +} + +char String::operator[](unsigned i) const { + return const_cast(this)->operator[](i); // NOLINT +} + +char& String::operator[](unsigned i) { + if(isOnStack()) + return reinterpret_cast(buf)[i]; + return data.ptr[i]; +} + +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wmaybe-uninitialized") +unsigned String::size() const { + if(isOnStack()) + return last - (unsigned(buf[last]) & 31); // using "last" would work only if "len" is 32 + return data.size; +} +DOCTEST_GCC_SUPPRESS_WARNING_POP + +unsigned String::capacity() const { + if(isOnStack()) + return len; + return data.capacity; +} + +int String::compare(const char* other, bool no_case) const { + if(no_case) + return doctest::stricmp(c_str(), other); + return std::strcmp(c_str(), other); +} + +int String::compare(const String& other, bool no_case) const { + return compare(other.c_str(), no_case); +} + +// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) +String operator+(const String& lhs, const String& rhs) { return String(lhs) += rhs; } + +// clang-format off +bool operator==(const String& lhs, const String& rhs) { return lhs.compare(rhs) == 0; } +bool operator!=(const String& lhs, const String& rhs) { return lhs.compare(rhs) != 0; } +bool operator< (const String& lhs, const String& rhs) { return lhs.compare(rhs) < 0; } +bool operator> (const String& lhs, const String& rhs) { return lhs.compare(rhs) > 0; } +bool operator<=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) < 0 : true; } +bool operator>=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) > 0 : true; } +// clang-format on + +std::ostream& operator<<(std::ostream& s, const String& in) { return s << in.c_str(); } + +namespace { + void color_to_stream(std::ostream&, Color::Enum) DOCTEST_BRANCH_ON_DISABLED({}, ;) +} // namespace + +namespace Color { + std::ostream& operator<<(std::ostream& s, Color::Enum code) { + color_to_stream(s, code); + return s; + } +} // namespace Color + +// clang-format off +const char* assertString(assertType::Enum at) { + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4062) // enum 'x' in switch of enum 'y' is not handled + switch(at) { //!OCLINT missing default in switch statements + case assertType::DT_WARN : return "WARN"; + case assertType::DT_CHECK : return "CHECK"; + case assertType::DT_REQUIRE : return "REQUIRE"; + + case assertType::DT_WARN_FALSE : return "WARN_FALSE"; + case assertType::DT_CHECK_FALSE : return "CHECK_FALSE"; + case assertType::DT_REQUIRE_FALSE : return "REQUIRE_FALSE"; + + case assertType::DT_WARN_THROWS : return "WARN_THROWS"; + case assertType::DT_CHECK_THROWS : return "CHECK_THROWS"; + case assertType::DT_REQUIRE_THROWS : return "REQUIRE_THROWS"; + + case assertType::DT_WARN_THROWS_AS : return "WARN_THROWS_AS"; + case assertType::DT_CHECK_THROWS_AS : return "CHECK_THROWS_AS"; + case assertType::DT_REQUIRE_THROWS_AS : return "REQUIRE_THROWS_AS"; + + case assertType::DT_WARN_THROWS_WITH : return "WARN_THROWS_WITH"; + case assertType::DT_CHECK_THROWS_WITH : return "CHECK_THROWS_WITH"; + case assertType::DT_REQUIRE_THROWS_WITH : return "REQUIRE_THROWS_WITH"; + + case assertType::DT_WARN_THROWS_WITH_AS : return "WARN_THROWS_WITH_AS"; + case assertType::DT_CHECK_THROWS_WITH_AS : return "CHECK_THROWS_WITH_AS"; + case assertType::DT_REQUIRE_THROWS_WITH_AS : return "REQUIRE_THROWS_WITH_AS"; + + case assertType::DT_WARN_NOTHROW : return "WARN_NOTHROW"; + case assertType::DT_CHECK_NOTHROW : return "CHECK_NOTHROW"; + case assertType::DT_REQUIRE_NOTHROW : return "REQUIRE_NOTHROW"; + + case assertType::DT_WARN_EQ : return "WARN_EQ"; + case assertType::DT_CHECK_EQ : return "CHECK_EQ"; + case assertType::DT_REQUIRE_EQ : return "REQUIRE_EQ"; + case assertType::DT_WARN_NE : return "WARN_NE"; + case assertType::DT_CHECK_NE : return "CHECK_NE"; + case assertType::DT_REQUIRE_NE : return "REQUIRE_NE"; + case assertType::DT_WARN_GT : return "WARN_GT"; + case assertType::DT_CHECK_GT : return "CHECK_GT"; + case assertType::DT_REQUIRE_GT : return "REQUIRE_GT"; + case assertType::DT_WARN_LT : return "WARN_LT"; + case assertType::DT_CHECK_LT : return "CHECK_LT"; + case assertType::DT_REQUIRE_LT : return "REQUIRE_LT"; + case assertType::DT_WARN_GE : return "WARN_GE"; + case assertType::DT_CHECK_GE : return "CHECK_GE"; + case assertType::DT_REQUIRE_GE : return "REQUIRE_GE"; + case assertType::DT_WARN_LE : return "WARN_LE"; + case assertType::DT_CHECK_LE : return "CHECK_LE"; + case assertType::DT_REQUIRE_LE : return "REQUIRE_LE"; + + case assertType::DT_WARN_UNARY : return "WARN_UNARY"; + case assertType::DT_CHECK_UNARY : return "CHECK_UNARY"; + case assertType::DT_REQUIRE_UNARY : return "REQUIRE_UNARY"; + case assertType::DT_WARN_UNARY_FALSE : return "WARN_UNARY_FALSE"; + case assertType::DT_CHECK_UNARY_FALSE : return "CHECK_UNARY_FALSE"; + case assertType::DT_REQUIRE_UNARY_FALSE : return "REQUIRE_UNARY_FALSE"; + } + DOCTEST_MSVC_SUPPRESS_WARNING_POP + return ""; +} +// clang-format on + +const char* failureString(assertType::Enum at) { + if(at & assertType::is_warn) //!OCLINT bitwise operator in conditional + return "WARNING"; + if(at & assertType::is_check) //!OCLINT bitwise operator in conditional + return "ERROR"; + if(at & assertType::is_require) //!OCLINT bitwise operator in conditional + return "FATAL ERROR"; + return ""; +} + +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference") +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference") +// depending on the current options this will remove the path of filenames +const char* skipPathFromFilename(const char* file) { +#ifndef DOCTEST_CONFIG_DISABLE + if(getContextOptions()->no_path_in_filenames) { + auto back = std::strrchr(file, '\\'); + auto forward = std::strrchr(file, '/'); + if(back || forward) { + if(back > forward) + forward = back; + return forward + 1; + } + } +#endif // DOCTEST_CONFIG_DISABLE + return file; +} +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +bool SubcaseSignature::operator<(const SubcaseSignature& other) const { + if(m_line != other.m_line) + return m_line < other.m_line; + if(std::strcmp(m_file, other.m_file) != 0) + return std::strcmp(m_file, other.m_file) < 0; + return m_name.compare(other.m_name) < 0; +} + +IContextScope::IContextScope() = default; +IContextScope::~IContextScope() = default; + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +String toString(char* in) { return toString(static_cast(in)); } +// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) +String toString(const char* in) { return String("\"") + (in ? in : "{null string}") + "\""; } +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +String toString(bool in) { return in ? "true" : "false"; } +String toString(float in) { return fpToString(in, 5) + "f"; } +String toString(double in) { return fpToString(in, 10); } +String toString(double long in) { return fpToString(in, 15); } + +#define DOCTEST_TO_STRING_OVERLOAD(type, fmt) \ + String toString(type in) { \ + char buf[64]; \ + std::sprintf(buf, fmt, in); \ + return buf; \ + } + +DOCTEST_TO_STRING_OVERLOAD(char, "%d") +DOCTEST_TO_STRING_OVERLOAD(char signed, "%d") +DOCTEST_TO_STRING_OVERLOAD(char unsigned, "%u") +DOCTEST_TO_STRING_OVERLOAD(int short, "%d") +DOCTEST_TO_STRING_OVERLOAD(int short unsigned, "%u") +DOCTEST_TO_STRING_OVERLOAD(int, "%d") +DOCTEST_TO_STRING_OVERLOAD(unsigned, "%u") +DOCTEST_TO_STRING_OVERLOAD(int long, "%ld") +DOCTEST_TO_STRING_OVERLOAD(int long unsigned, "%lu") +DOCTEST_TO_STRING_OVERLOAD(int long long, "%lld") +DOCTEST_TO_STRING_OVERLOAD(int long long unsigned, "%llu") + +String toString(std::nullptr_t) { return "NULL"; } + +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 +String toString(const std::string& in) { return in.c_str(); } +#endif // VS 2019 + +Approx::Approx(double value) + : m_epsilon(static_cast(std::numeric_limits::epsilon()) * 100) + , m_scale(1.0) + , m_value(value) {} + +Approx Approx::operator()(double value) const { + Approx approx(value); + approx.epsilon(m_epsilon); + approx.scale(m_scale); + return approx; +} + +Approx& Approx::epsilon(double newEpsilon) { + m_epsilon = newEpsilon; + return *this; +} +Approx& Approx::scale(double newScale) { + m_scale = newScale; + return *this; +} + +bool operator==(double lhs, const Approx& rhs) { + // Thanks to Richard Harris for his help refining this formula + return std::fabs(lhs - rhs.m_value) < + rhs.m_epsilon * (rhs.m_scale + std::max(std::fabs(lhs), std::fabs(rhs.m_value))); +} +bool operator==(const Approx& lhs, double rhs) { return operator==(rhs, lhs); } +bool operator!=(double lhs, const Approx& rhs) { return !operator==(lhs, rhs); } +bool operator!=(const Approx& lhs, double rhs) { return !operator==(rhs, lhs); } +bool operator<=(double lhs, const Approx& rhs) { return lhs < rhs.m_value || lhs == rhs; } +bool operator<=(const Approx& lhs, double rhs) { return lhs.m_value < rhs || lhs == rhs; } +bool operator>=(double lhs, const Approx& rhs) { return lhs > rhs.m_value || lhs == rhs; } +bool operator>=(const Approx& lhs, double rhs) { return lhs.m_value > rhs || lhs == rhs; } +bool operator<(double lhs, const Approx& rhs) { return lhs < rhs.m_value && lhs != rhs; } +bool operator<(const Approx& lhs, double rhs) { return lhs.m_value < rhs && lhs != rhs; } +bool operator>(double lhs, const Approx& rhs) { return lhs > rhs.m_value && lhs != rhs; } +bool operator>(const Approx& lhs, double rhs) { return lhs.m_value > rhs && lhs != rhs; } + +String toString(const Approx& in) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) + return "Approx( " + doctest::toString(in.m_value) + " )"; +} +const ContextOptions* getContextOptions() { return DOCTEST_BRANCH_ON_DISABLED(nullptr, g_cs); } + +} // namespace doctest + +#ifdef DOCTEST_CONFIG_DISABLE +namespace doctest { +Context::Context(int, const char* const*) {} +Context::~Context() = default; +void Context::applyCommandLine(int, const char* const*) {} +void Context::addFilter(const char*, const char*) {} +void Context::clearFilters() {} +void Context::setOption(const char*, bool) {} +void Context::setOption(const char*, int) {} +void Context::setOption(const char*, const char*) {} +bool Context::shouldExit() { return false; } +void Context::setAsDefaultForAssertsOutOfTestCases() {} +void Context::setAssertHandler(detail::assert_handler) {} +void Context::setCout(std::ostream* out) {} +int Context::run() { return 0; } + +IReporter::~IReporter() = default; + +int IReporter::get_num_active_contexts() { return 0; } +const IContextScope* const* IReporter::get_active_contexts() { return nullptr; } +int IReporter::get_num_stringified_contexts() { return 0; } +const String* IReporter::get_stringified_contexts() { return nullptr; } + +int registerReporter(const char*, int, IReporter*) { return 0; } + +} // namespace doctest +#else // DOCTEST_CONFIG_DISABLE + +#if !defined(DOCTEST_CONFIG_COLORS_NONE) +#if !defined(DOCTEST_CONFIG_COLORS_WINDOWS) && !defined(DOCTEST_CONFIG_COLORS_ANSI) +#ifdef DOCTEST_PLATFORM_WINDOWS +#define DOCTEST_CONFIG_COLORS_WINDOWS +#else // linux +#define DOCTEST_CONFIG_COLORS_ANSI +#endif // platform +#endif // DOCTEST_CONFIG_COLORS_WINDOWS && DOCTEST_CONFIG_COLORS_ANSI +#endif // DOCTEST_CONFIG_COLORS_NONE + +namespace doctest_detail_test_suite_ns { +// holds the current test suite +doctest::detail::TestSuite& getCurrentTestSuite() { + static doctest::detail::TestSuite data{}; + return data; +} +} // namespace doctest_detail_test_suite_ns + +namespace doctest { +namespace { + // the int (priority) is part of the key for automatic sorting - sadly one can register a + // reporter with a duplicate name and a different priority but hopefully that won't happen often :| + typedef std::map, reporterCreatorFunc> reporterMap; + + reporterMap& getReporters() { + static reporterMap data; + return data; + } + reporterMap& getListeners() { + static reporterMap data; + return data; + } +} // namespace +namespace detail { +#define DOCTEST_ITERATE_THROUGH_REPORTERS(function, ...) \ + for(auto& curr_rep : g_cs->reporters_currently_used) \ + curr_rep->function(__VA_ARGS__) + + bool checkIfShouldThrow(assertType::Enum at) { + if(at & assertType::is_require) //!OCLINT bitwise operator in conditional + return true; + + if((at & assertType::is_check) //!OCLINT bitwise operator in conditional + && getContextOptions()->abort_after > 0 && + (g_cs->numAssertsFailed + g_cs->numAssertsFailedCurrentTest_atomic) >= + getContextOptions()->abort_after) + return true; + + return false; + } + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + DOCTEST_NORETURN void throwException() { + g_cs->shouldLogCurrentException = false; + throw TestFailureException(); + } // NOLINT(cert-err60-cpp) +#else // DOCTEST_CONFIG_NO_EXCEPTIONS + void throwException() {} +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +} // namespace detail + +namespace { + using namespace detail; + // matching of a string against a wildcard mask (case sensitivity configurable) taken from + // https://www.codeproject.com/Articles/1088/Wildcard-string-compare-globbing + int wildcmp(const char* str, const char* wild, bool caseSensitive) { + const char* cp = str; + const char* mp = wild; + + while((*str) && (*wild != '*')) { + if((caseSensitive ? (*wild != *str) : (tolower(*wild) != tolower(*str))) && + (*wild != '?')) { + return 0; + } + wild++; + str++; + } + + while(*str) { + if(*wild == '*') { + if(!*++wild) { + return 1; + } + mp = wild; + cp = str + 1; + } else if((caseSensitive ? (*wild == *str) : (tolower(*wild) == tolower(*str))) || + (*wild == '?')) { + wild++; + str++; + } else { + wild = mp; //!OCLINT parameter reassignment + str = cp++; //!OCLINT parameter reassignment + } + } + + while(*wild == '*') { + wild++; + } + return !*wild; + } + + //// C string hash function (djb2) - taken from http://www.cse.yorku.ca/~oz/hash.html + //unsigned hashStr(unsigned const char* str) { + // unsigned long hash = 5381; + // char c; + // while((c = *str++)) + // hash = ((hash << 5) + hash) + c; // hash * 33 + c + // return hash; + //} + + // checks if the name matches any of the filters (and can be configured what to do when empty) + bool matchesAny(const char* name, const std::vector& filters, bool matchEmpty, + bool caseSensitive) { + if(filters.empty() && matchEmpty) + return true; + for(auto& curr : filters) + if(wildcmp(name, curr.c_str(), caseSensitive)) + return true; + return false; + } +} // namespace +namespace detail { + + Subcase::Subcase(const String& name, const char* file, int line) + : m_signature({name, file, line}) { + auto* s = g_cs; + + // check subcase filters + if(s->subcasesStack.size() < size_t(s->subcase_filter_levels)) { + if(!matchesAny(m_signature.m_name.c_str(), s->filters[6], true, s->case_sensitive)) + return; + if(matchesAny(m_signature.m_name.c_str(), s->filters[7], false, s->case_sensitive)) + return; + } + + // if a Subcase on the same level has already been entered + if(s->subcasesStack.size() < size_t(s->subcasesCurrentMaxLevel)) { + s->should_reenter = true; + return; + } + + // push the current signature to the stack so we can check if the + // current stack + the current new subcase have been traversed + s->subcasesStack.push_back(m_signature); + if(s->subcasesPassed.count(s->subcasesStack) != 0) { + // pop - revert to previous stack since we've already passed this + s->subcasesStack.pop_back(); + return; + } + + s->subcasesCurrentMaxLevel = s->subcasesStack.size(); + m_entered = true; + + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_start, m_signature); + } + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4996) // std::uncaught_exception is deprecated in C++17 + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + + Subcase::~Subcase() { + if(m_entered) { + // only mark the subcase stack as passed if no subcases have been skipped + if(g_cs->should_reenter == false) + g_cs->subcasesPassed.insert(g_cs->subcasesStack); + g_cs->subcasesStack.pop_back(); + +#if defined(__cpp_lib_uncaught_exceptions) && __cpp_lib_uncaught_exceptions >= 201411L && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200) + if(std::uncaught_exceptions() > 0 +#else + if(std::uncaught_exception() +#endif + && g_cs->shouldLogCurrentException) { + DOCTEST_ITERATE_THROUGH_REPORTERS( + test_case_exception, {"exception thrown in subcase - will translate later " + "when the whole test case has been exited (cannot " + "translate while there is an active exception)", + false}); + g_cs->shouldLogCurrentException = false; + } + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY); + } + } + + DOCTEST_CLANG_SUPPRESS_WARNING_POP + DOCTEST_GCC_SUPPRESS_WARNING_POP + DOCTEST_MSVC_SUPPRESS_WARNING_POP + + Subcase::operator bool() const { return m_entered; } + + Result::Result(bool passed, const String& decomposition) + : m_passed(passed) + , m_decomp(decomposition) {} + + ExpressionDecomposer::ExpressionDecomposer(assertType::Enum at) + : m_at(at) {} + + TestSuite& TestSuite::operator*(const char* in) { + m_test_suite = in; + return *this; + } + + TestCase::TestCase(funcType test, const char* file, unsigned line, const TestSuite& test_suite, + const char* type, int template_id) { + m_file = file; + m_line = line; + m_name = nullptr; // will be later overridden in operator* + m_test_suite = test_suite.m_test_suite; + m_description = test_suite.m_description; + m_skip = test_suite.m_skip; + m_no_breaks = test_suite.m_no_breaks; + m_no_output = test_suite.m_no_output; + m_may_fail = test_suite.m_may_fail; + m_should_fail = test_suite.m_should_fail; + m_expected_failures = test_suite.m_expected_failures; + m_timeout = test_suite.m_timeout; + + m_test = test; + m_type = type; + m_template_id = template_id; + } + + TestCase::TestCase(const TestCase& other) + : TestCaseData() { + *this = other; + } + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function + DOCTEST_MSVC_SUPPRESS_WARNING(26437) // Do not slice + TestCase& TestCase::operator=(const TestCase& other) { + static_cast(*this) = static_cast(other); + + m_test = other.m_test; + m_type = other.m_type; + m_template_id = other.m_template_id; + m_full_name = other.m_full_name; + + if(m_template_id != -1) + m_name = m_full_name.c_str(); + return *this; + } + DOCTEST_MSVC_SUPPRESS_WARNING_POP + + TestCase& TestCase::operator*(const char* in) { + m_name = in; + // make a new name with an appended type for templated test case + if(m_template_id != -1) { + m_full_name = String(m_name) + m_type; + // redirect the name to point to the newly constructed full name + m_name = m_full_name.c_str(); + } + return *this; + } + + bool TestCase::operator<(const TestCase& other) const { + // this will be used only to differentiate between test cases - not relevant for sorting + if(m_line != other.m_line) + return m_line < other.m_line; + const int name_cmp = strcmp(m_name, other.m_name); + if(name_cmp != 0) + return name_cmp < 0; + const int file_cmp = m_file.compare(other.m_file); + if(file_cmp != 0) + return file_cmp < 0; + return m_template_id < other.m_template_id; + } + + // all the registered tests + std::set& getRegisteredTests() { + static std::set data; + return data; + } +} // namespace detail +namespace { + using namespace detail; + // for sorting tests by file/line + bool fileOrderComparator(const TestCase* lhs, const TestCase* rhs) { + // this is needed because MSVC gives different case for drive letters + // for __FILE__ when evaluated in a header and a source file + const int res = lhs->m_file.compare(rhs->m_file, bool(DOCTEST_MSVC)); + if(res != 0) + return res < 0; + if(lhs->m_line != rhs->m_line) + return lhs->m_line < rhs->m_line; + return lhs->m_template_id < rhs->m_template_id; + } + + // for sorting tests by suite/file/line + bool suiteOrderComparator(const TestCase* lhs, const TestCase* rhs) { + const int res = std::strcmp(lhs->m_test_suite, rhs->m_test_suite); + if(res != 0) + return res < 0; + return fileOrderComparator(lhs, rhs); + } + + // for sorting tests by name/suite/file/line + bool nameOrderComparator(const TestCase* lhs, const TestCase* rhs) { + const int res = std::strcmp(lhs->m_name, rhs->m_name); + if(res != 0) + return res < 0; + return suiteOrderComparator(lhs, rhs); + } + + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + void color_to_stream(std::ostream& s, Color::Enum code) { + static_cast(s); // for DOCTEST_CONFIG_COLORS_NONE or DOCTEST_CONFIG_COLORS_WINDOWS + static_cast(code); // for DOCTEST_CONFIG_COLORS_NONE +#ifdef DOCTEST_CONFIG_COLORS_ANSI + if(g_no_colors || + (isatty(STDOUT_FILENO) == false && getContextOptions()->force_colors == false)) + return; + + auto col = ""; + // clang-format off + switch(code) { //!OCLINT missing break in switch statement / unnecessary default statement in covered switch statement + case Color::Red: col = "[0;31m"; break; + case Color::Green: col = "[0;32m"; break; + case Color::Blue: col = "[0;34m"; break; + case Color::Cyan: col = "[0;36m"; break; + case Color::Yellow: col = "[0;33m"; break; + case Color::Grey: col = "[1;30m"; break; + case Color::LightGrey: col = "[0;37m"; break; + case Color::BrightRed: col = "[1;31m"; break; + case Color::BrightGreen: col = "[1;32m"; break; + case Color::BrightWhite: col = "[1;37m"; break; + case Color::Bright: // invalid + case Color::None: + case Color::White: + default: col = "[0m"; + } + // clang-format on + s << "\033" << col; +#endif // DOCTEST_CONFIG_COLORS_ANSI + +#ifdef DOCTEST_CONFIG_COLORS_WINDOWS + if(g_no_colors || + (_isatty(_fileno(stdout)) == false && getContextOptions()->force_colors == false)) + return; + + static struct ConsoleHelper { + HANDLE stdoutHandle; + WORD origFgAttrs; + WORD origBgAttrs; + + ConsoleHelper() { + stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); + CONSOLE_SCREEN_BUFFER_INFO csbiInfo; + GetConsoleScreenBufferInfo(stdoutHandle, &csbiInfo); + origFgAttrs = csbiInfo.wAttributes & ~(BACKGROUND_GREEN | BACKGROUND_RED | + BACKGROUND_BLUE | BACKGROUND_INTENSITY); + origBgAttrs = csbiInfo.wAttributes & ~(FOREGROUND_GREEN | FOREGROUND_RED | + FOREGROUND_BLUE | FOREGROUND_INTENSITY); + } + } ch; + +#define DOCTEST_SET_ATTR(x) SetConsoleTextAttribute(ch.stdoutHandle, x | ch.origBgAttrs) + + // clang-format off + switch (code) { + case Color::White: DOCTEST_SET_ATTR(FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break; + case Color::Red: DOCTEST_SET_ATTR(FOREGROUND_RED); break; + case Color::Green: DOCTEST_SET_ATTR(FOREGROUND_GREEN); break; + case Color::Blue: DOCTEST_SET_ATTR(FOREGROUND_BLUE); break; + case Color::Cyan: DOCTEST_SET_ATTR(FOREGROUND_BLUE | FOREGROUND_GREEN); break; + case Color::Yellow: DOCTEST_SET_ATTR(FOREGROUND_RED | FOREGROUND_GREEN); break; + case Color::Grey: DOCTEST_SET_ATTR(0); break; + case Color::LightGrey: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY); break; + case Color::BrightRed: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_RED); break; + case Color::BrightGreen: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN); break; + case Color::BrightWhite: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break; + case Color::None: + case Color::Bright: // invalid + default: DOCTEST_SET_ATTR(ch.origFgAttrs); + } + // clang-format on +#endif // DOCTEST_CONFIG_COLORS_WINDOWS + } + DOCTEST_CLANG_SUPPRESS_WARNING_POP + + std::vector& getExceptionTranslators() { + static std::vector data; + return data; + } + + String translateActiveException() { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + String res; + auto& translators = getExceptionTranslators(); + for(auto& curr : translators) + if(curr->translate(res)) + return res; + // clang-format off + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wcatch-value") + try { + throw; + } catch(std::exception& ex) { + return ex.what(); + } catch(std::string& msg) { + return msg.c_str(); + } catch(const char* msg) { + return msg; + } catch(...) { + return "unknown exception"; + } + DOCTEST_GCC_SUPPRESS_WARNING_POP +// clang-format on +#else // DOCTEST_CONFIG_NO_EXCEPTIONS + return ""; +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + } +} // namespace + +namespace detail { + // used by the macros for registering tests + int regTest(const TestCase& tc) { + getRegisteredTests().insert(tc); + return 0; + } + + // sets the current test suite + int setTestSuite(const TestSuite& ts) { + doctest_detail_test_suite_ns::getCurrentTestSuite() = ts; + return 0; + } + +#ifdef DOCTEST_IS_DEBUGGER_ACTIVE + bool isDebuggerActive() { return DOCTEST_IS_DEBUGGER_ACTIVE(); } +#else // DOCTEST_IS_DEBUGGER_ACTIVE +#ifdef DOCTEST_PLATFORM_LINUX + class ErrnoGuard { + public: + ErrnoGuard() : m_oldErrno(errno) {} + ~ErrnoGuard() { errno = m_oldErrno; } + private: + int m_oldErrno; + }; + // See the comments in Catch2 for the reasoning behind this implementation: + // https://github.com/catchorg/Catch2/blob/v2.13.1/include/internal/catch_debugger.cpp#L79-L102 + bool isDebuggerActive() { + ErrnoGuard guard; + std::ifstream in("/proc/self/status"); + for(std::string line; std::getline(in, line);) { + static const int PREFIX_LEN = 11; + if(line.compare(0, PREFIX_LEN, "TracerPid:\t") == 0) { + return line.length() > PREFIX_LEN && line[PREFIX_LEN] != '0'; + } + } + return false; + } +#elif defined(DOCTEST_PLATFORM_MAC) + // The following function is taken directly from the following technical note: + // https://developer.apple.com/library/archive/qa/qa1361/_index.html + // Returns true if the current process is being debugged (either + // running under the debugger or has a debugger attached post facto). + bool isDebuggerActive() { + int mib[4]; + kinfo_proc info; + size_t size; + // Initialize the flags so that, if sysctl fails for some bizarre + // reason, we get a predictable result. + info.kp_proc.p_flag = 0; + // Initialize mib, which tells sysctl the info we want, in this case + // we're looking for information about a specific process ID. + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = getpid(); + // Call sysctl. + size = sizeof(info); + if(sysctl(mib, DOCTEST_COUNTOF(mib), &info, &size, 0, 0) != 0) { + std::cerr << "\nCall to sysctl failed - unable to determine if debugger is active **\n"; + return false; + } + // We're being debugged if the P_TRACED flag is set. + return ((info.kp_proc.p_flag & P_TRACED) != 0); + } +#elif DOCTEST_MSVC || defined(__MINGW32__) || defined(__MINGW64__) + bool isDebuggerActive() { return ::IsDebuggerPresent() != 0; } +#else + bool isDebuggerActive() { return false; } +#endif // Platform +#endif // DOCTEST_IS_DEBUGGER_ACTIVE + + void registerExceptionTranslatorImpl(const IExceptionTranslator* et) { + if(std::find(getExceptionTranslators().begin(), getExceptionTranslators().end(), et) == + getExceptionTranslators().end()) + getExceptionTranslators().push_back(et); + } + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + void toStream(std::ostream* s, char* in) { *s << in; } + void toStream(std::ostream* s, const char* in) { *s << in; } +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + void toStream(std::ostream* s, bool in) { *s << std::boolalpha << in << std::noboolalpha; } + void toStream(std::ostream* s, float in) { *s << in; } + void toStream(std::ostream* s, double in) { *s << in; } + void toStream(std::ostream* s, double long in) { *s << in; } + + void toStream(std::ostream* s, char in) { *s << in; } + void toStream(std::ostream* s, char signed in) { *s << in; } + void toStream(std::ostream* s, char unsigned in) { *s << in; } + void toStream(std::ostream* s, int short in) { *s << in; } + void toStream(std::ostream* s, int short unsigned in) { *s << in; } + void toStream(std::ostream* s, int in) { *s << in; } + void toStream(std::ostream* s, int unsigned in) { *s << in; } + void toStream(std::ostream* s, int long in) { *s << in; } + void toStream(std::ostream* s, int long unsigned in) { *s << in; } + void toStream(std::ostream* s, int long long in) { *s << in; } + void toStream(std::ostream* s, int long long unsigned in) { *s << in; } + + DOCTEST_THREAD_LOCAL std::vector g_infoContexts; // for logging with INFO() + + ContextScopeBase::ContextScopeBase() { + g_infoContexts.push_back(this); + } + + ContextScopeBase::ContextScopeBase(ContextScopeBase&& other) { + if (other.need_to_destroy) { + other.destroy(); + } + other.need_to_destroy = false; + g_infoContexts.push_back(this); + } + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4996) // std::uncaught_exception is deprecated in C++17 + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + + // destroy cannot be inlined into the destructor because that would mean calling stringify after + // ContextScope has been destroyed (base class destructors run after derived class destructors). + // Instead, ContextScope calls this method directly from its destructor. + void ContextScopeBase::destroy() { +#if defined(__cpp_lib_uncaught_exceptions) && __cpp_lib_uncaught_exceptions >= 201411L && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200) + if(std::uncaught_exceptions() > 0) { +#else + if(std::uncaught_exception()) { +#endif + std::ostringstream s; + this->stringify(&s); + g_cs->stringifiedContexts.push_back(s.str().c_str()); + } + g_infoContexts.pop_back(); + } + + DOCTEST_CLANG_SUPPRESS_WARNING_POP + DOCTEST_GCC_SUPPRESS_WARNING_POP + DOCTEST_MSVC_SUPPRESS_WARNING_POP +} // namespace detail +namespace { + using namespace detail; + +#if !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && !defined(DOCTEST_CONFIG_WINDOWS_SEH) + struct FatalConditionHandler + { + static void reset() {} + static void allocateAltStackMem() {} + static void freeAltStackMem() {} + }; +#else // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + + void reportFatal(const std::string&); + +#ifdef DOCTEST_PLATFORM_WINDOWS + + struct SignalDefs + { + DWORD id; + const char* name; + }; + // There is no 1-1 mapping between signals and windows exceptions. + // Windows can easily distinguish between SO and SigSegV, + // but SigInt, SigTerm, etc are handled differently. + SignalDefs signalDefs[] = { + {static_cast(EXCEPTION_ILLEGAL_INSTRUCTION), + "SIGILL - Illegal instruction signal"}, + {static_cast(EXCEPTION_STACK_OVERFLOW), "SIGSEGV - Stack overflow"}, + {static_cast(EXCEPTION_ACCESS_VIOLATION), + "SIGSEGV - Segmentation violation signal"}, + {static_cast(EXCEPTION_INT_DIVIDE_BY_ZERO), "Divide by zero error"}, + }; + + struct FatalConditionHandler + { + static LONG CALLBACK handleException(PEXCEPTION_POINTERS ExceptionInfo) { + // Multiple threads may enter this filter/handler at once. We want the error message to be printed on the + // console just once no matter how many threads have crashed. + static std::mutex mutex; + static bool execute = true; + { + std::lock_guard lock(mutex); + if(execute) { + bool reported = false; + for(size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + if(ExceptionInfo->ExceptionRecord->ExceptionCode == signalDefs[i].id) { + reportFatal(signalDefs[i].name); + reported = true; + break; + } + } + if(reported == false) + reportFatal("Unhandled SEH exception caught"); + if(isDebuggerActive() && !g_cs->no_breaks) + DOCTEST_BREAK_INTO_DEBUGGER(); + } + execute = false; + } + std::exit(EXIT_FAILURE); + } + + static void allocateAltStackMem() {} + static void freeAltStackMem() {} + + FatalConditionHandler() { + isSet = true; + // 32k seems enough for doctest to handle stack overflow, + // but the value was found experimentally, so there is no strong guarantee + guaranteeSize = 32 * 1024; + // Register an unhandled exception filter + previousTop = SetUnhandledExceptionFilter(handleException); + // Pass in guarantee size to be filled + SetThreadStackGuarantee(&guaranteeSize); + + // On Windows uncaught exceptions from another thread, exceptions from + // destructors, or calls to std::terminate are not a SEH exception + + // The terminal handler gets called when: + // - std::terminate is called FROM THE TEST RUNNER THREAD + // - an exception is thrown from a destructor FROM THE TEST RUNNER THREAD + original_terminate_handler = std::get_terminate(); + std::set_terminate([]() DOCTEST_NOEXCEPT { + reportFatal("Terminate handler called"); + if(isDebuggerActive() && !g_cs->no_breaks) + DOCTEST_BREAK_INTO_DEBUGGER(); + std::exit(EXIT_FAILURE); // explicitly exit - otherwise the SIGABRT handler may be called as well + }); + + // SIGABRT is raised when: + // - std::terminate is called FROM A DIFFERENT THREAD + // - an exception is thrown from a destructor FROM A DIFFERENT THREAD + // - an uncaught exception is thrown FROM A DIFFERENT THREAD + prev_sigabrt_handler = std::signal(SIGABRT, [](int signal) DOCTEST_NOEXCEPT { + if(signal == SIGABRT) { + reportFatal("SIGABRT - Abort (abnormal termination) signal"); + if(isDebuggerActive() && !g_cs->no_breaks) + DOCTEST_BREAK_INTO_DEBUGGER(); + std::exit(EXIT_FAILURE); + } + }); + + // The following settings are taken from google test, and more + // specifically from UnitTest::Run() inside of gtest.cc + + // the user does not want to see pop-up dialogs about crashes + prev_error_mode_1 = SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOALIGNMENTFAULTEXCEPT | + SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX); + // This forces the abort message to go to stderr in all circumstances. + prev_error_mode_2 = _set_error_mode(_OUT_TO_STDERR); + // In the debug version, Visual Studio pops up a separate dialog + // offering a choice to debug the aborted program - we want to disable that. + prev_abort_behavior = _set_abort_behavior(0x0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + // In debug mode, the Windows CRT can crash with an assertion over invalid + // input (e.g. passing an invalid file descriptor). The default handling + // for these assertions is to pop up a dialog and wait for user input. + // Instead ask the CRT to dump such assertions to stderr non-interactively. + prev_report_mode = _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG); + prev_report_file = _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR); + } + + static void reset() { + if(isSet) { + // Unregister handler and restore the old guarantee + SetUnhandledExceptionFilter(previousTop); + SetThreadStackGuarantee(&guaranteeSize); + std::set_terminate(original_terminate_handler); + std::signal(SIGABRT, prev_sigabrt_handler); + SetErrorMode(prev_error_mode_1); + _set_error_mode(prev_error_mode_2); + _set_abort_behavior(prev_abort_behavior, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + static_cast(_CrtSetReportMode(_CRT_ASSERT, prev_report_mode)); + static_cast(_CrtSetReportFile(_CRT_ASSERT, prev_report_file)); + isSet = false; + } + } + + ~FatalConditionHandler() { reset(); } + + private: + static UINT prev_error_mode_1; + static int prev_error_mode_2; + static unsigned int prev_abort_behavior; + static int prev_report_mode; + static _HFILE prev_report_file; + static void (DOCTEST_CDECL *prev_sigabrt_handler)(int); + static std::terminate_handler original_terminate_handler; + static bool isSet; + static ULONG guaranteeSize; + static LPTOP_LEVEL_EXCEPTION_FILTER previousTop; + }; + + UINT FatalConditionHandler::prev_error_mode_1; + int FatalConditionHandler::prev_error_mode_2; + unsigned int FatalConditionHandler::prev_abort_behavior; + int FatalConditionHandler::prev_report_mode; + _HFILE FatalConditionHandler::prev_report_file; + void (DOCTEST_CDECL *FatalConditionHandler::prev_sigabrt_handler)(int); + std::terminate_handler FatalConditionHandler::original_terminate_handler; + bool FatalConditionHandler::isSet = false; + ULONG FatalConditionHandler::guaranteeSize = 0; + LPTOP_LEVEL_EXCEPTION_FILTER FatalConditionHandler::previousTop = nullptr; + +#else // DOCTEST_PLATFORM_WINDOWS + + struct SignalDefs + { + int id; + const char* name; + }; + SignalDefs signalDefs[] = {{SIGINT, "SIGINT - Terminal interrupt signal"}, + {SIGILL, "SIGILL - Illegal instruction signal"}, + {SIGFPE, "SIGFPE - Floating point error signal"}, + {SIGSEGV, "SIGSEGV - Segmentation violation signal"}, + {SIGTERM, "SIGTERM - Termination request signal"}, + {SIGABRT, "SIGABRT - Abort (abnormal termination) signal"}}; + + struct FatalConditionHandler + { + static bool isSet; + static struct sigaction oldSigActions[DOCTEST_COUNTOF(signalDefs)]; + static stack_t oldSigStack; + static size_t altStackSize; + static char* altStackMem; + + static void handleSignal(int sig) { + const char* name = ""; + for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + SignalDefs& def = signalDefs[i]; + if(sig == def.id) { + name = def.name; + break; + } + } + reset(); + reportFatal(name); + raise(sig); + } + + static void allocateAltStackMem() { + altStackMem = new char[altStackSize]; + } + + static void freeAltStackMem() { + delete[] altStackMem; + } + + FatalConditionHandler() { + isSet = true; + stack_t sigStack; + sigStack.ss_sp = altStackMem; + sigStack.ss_size = altStackSize; + sigStack.ss_flags = 0; + sigaltstack(&sigStack, &oldSigStack); + struct sigaction sa = {}; + sa.sa_handler = handleSignal; // NOLINT + sa.sa_flags = SA_ONSTACK; + for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + sigaction(signalDefs[i].id, &sa, &oldSigActions[i]); + } + } + + ~FatalConditionHandler() { reset(); } + static void reset() { + if(isSet) { + // Set signals back to previous values -- hopefully nobody overwrote them in the meantime + for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + sigaction(signalDefs[i].id, &oldSigActions[i], nullptr); + } + // Return the old stack + sigaltstack(&oldSigStack, nullptr); + isSet = false; + } + } + }; + + bool FatalConditionHandler::isSet = false; + struct sigaction FatalConditionHandler::oldSigActions[DOCTEST_COUNTOF(signalDefs)] = {}; + stack_t FatalConditionHandler::oldSigStack = {}; + size_t FatalConditionHandler::altStackSize = 4 * SIGSTKSZ; + char* FatalConditionHandler::altStackMem = nullptr; + +#endif // DOCTEST_PLATFORM_WINDOWS +#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + +} // namespace + +namespace { + using namespace detail; + +#ifdef DOCTEST_PLATFORM_WINDOWS +#define DOCTEST_OUTPUT_DEBUG_STRING(text) ::OutputDebugStringA(text) +#else + // TODO: integration with XCode and other IDEs +#define DOCTEST_OUTPUT_DEBUG_STRING(text) // NOLINT(clang-diagnostic-unused-macros) +#endif // Platform + + void addAssert(assertType::Enum at) { + if((at & assertType::is_warn) == 0) //!OCLINT bitwise operator in conditional + g_cs->numAssertsCurrentTest_atomic++; + } + + void addFailedAssert(assertType::Enum at) { + if((at & assertType::is_warn) == 0) //!OCLINT bitwise operator in conditional + g_cs->numAssertsFailedCurrentTest_atomic++; + } + +#if defined(DOCTEST_CONFIG_POSIX_SIGNALS) || defined(DOCTEST_CONFIG_WINDOWS_SEH) + void reportFatal(const std::string& message) { + g_cs->failure_flags |= TestCaseFailureReason::Crash; + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception, {message.c_str(), true}); + + while(g_cs->subcasesStack.size()) { + g_cs->subcasesStack.pop_back(); + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY); + } + + g_cs->finalizeTestCaseData(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs); + } +#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH +} // namespace +namespace detail { + + ResultBuilder::ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type, const char* exception_string) { + m_test_case = g_cs->currentTest; + m_at = at; + m_file = file; + m_line = line; + m_expr = expr; + m_failed = true; + m_threw = false; + m_threw_as = false; + m_exception_type = exception_type; + m_exception_string = exception_string; +#if DOCTEST_MSVC + if(m_expr[0] == ' ') // this happens when variadic macros are disabled under MSVC + ++m_expr; +#endif // MSVC + } + + void ResultBuilder::setResult(const Result& res) { + m_decomp = res.m_decomp; + m_failed = !res.m_passed; + } + + void ResultBuilder::translateException() { + m_threw = true; + m_exception = translateActiveException(); + } + + bool ResultBuilder::log() { + if(m_at & assertType::is_throws) { //!OCLINT bitwise operator in conditional + m_failed = !m_threw; + } else if((m_at & assertType::is_throws_as) && (m_at & assertType::is_throws_with)) { //!OCLINT + m_failed = !m_threw_as || (m_exception != m_exception_string); + } else if(m_at & assertType::is_throws_as) { //!OCLINT bitwise operator in conditional + m_failed = !m_threw_as; + } else if(m_at & assertType::is_throws_with) { //!OCLINT bitwise operator in conditional + m_failed = m_exception != m_exception_string; + } else if(m_at & assertType::is_nothrow) { //!OCLINT bitwise operator in conditional + m_failed = m_threw; + } + + if(m_exception.size()) + m_exception = "\"" + m_exception + "\""; + + if(is_running_in_test) { + addAssert(m_at); + DOCTEST_ITERATE_THROUGH_REPORTERS(log_assert, *this); + + if(m_failed) + addFailedAssert(m_at); + } else if(m_failed) { + failed_out_of_a_testing_context(*this); + } + + return m_failed && isDebuggerActive() && !getContextOptions()->no_breaks && + (g_cs->currentTest == nullptr || !g_cs->currentTest->m_no_breaks); // break into debugger + } + + void ResultBuilder::react() const { + if(m_failed && checkIfShouldThrow(m_at)) + throwException(); + } + + void failed_out_of_a_testing_context(const AssertData& ad) { + if(g_cs->ah) + g_cs->ah(ad); + else + std::abort(); + } + + bool decomp_assert(assertType::Enum at, const char* file, int line, const char* expr, + Result result) { + bool failed = !result.m_passed; + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS(result.m_decomp); + DOCTEST_ASSERT_IN_TESTS(result.m_decomp); + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) + return !failed; + } + + MessageBuilder::MessageBuilder(const char* file, int line, assertType::Enum severity) { + m_stream = tlssPush(); + m_file = file; + m_line = line; + m_severity = severity; + } + + MessageBuilder::~MessageBuilder() { + if (!logged) + tlssPop(); + } + + IExceptionTranslator::IExceptionTranslator() = default; + IExceptionTranslator::~IExceptionTranslator() = default; + + bool MessageBuilder::log() { + if (!logged) { + m_string = tlssPop(); + logged = true; + } + + DOCTEST_ITERATE_THROUGH_REPORTERS(log_message, *this); + + const bool isWarn = m_severity & assertType::is_warn; + + // warn is just a message in this context so we don't treat it as an assert + if(!isWarn) { + addAssert(m_severity); + addFailedAssert(m_severity); + } + + return isDebuggerActive() && !getContextOptions()->no_breaks && !isWarn && + (g_cs->currentTest == nullptr || !g_cs->currentTest->m_no_breaks); // break into debugger + } + + void MessageBuilder::react() { + if(m_severity & assertType::is_require) //!OCLINT bitwise operator in conditional + throwException(); + } +} // namespace detail +namespace { + using namespace detail; + + // clang-format off + +// ================================================================================================= +// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.h/cpp +// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched. +// ================================================================================================= + + class XmlEncode { + public: + enum ForWhat { ForTextNodes, ForAttributes }; + + XmlEncode( std::string const& str, ForWhat forWhat = ForTextNodes ); + + void encodeTo( std::ostream& os ) const; + + friend std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ); + + private: + std::string m_str; + ForWhat m_forWhat; + }; + + class XmlWriter { + public: + + class ScopedElement { + public: + ScopedElement( XmlWriter* writer ); + + ScopedElement( ScopedElement&& other ) DOCTEST_NOEXCEPT; + ScopedElement& operator=( ScopedElement&& other ) DOCTEST_NOEXCEPT; + + ~ScopedElement(); + + ScopedElement& writeText( std::string const& text, bool indent = true ); + + template + ScopedElement& writeAttribute( std::string const& name, T const& attribute ) { + m_writer->writeAttribute( name, attribute ); + return *this; + } + + private: + mutable XmlWriter* m_writer = nullptr; + }; + + XmlWriter( std::ostream& os = std::cout ); + ~XmlWriter(); + + XmlWriter( XmlWriter const& ) = delete; + XmlWriter& operator=( XmlWriter const& ) = delete; + + XmlWriter& startElement( std::string const& name ); + + ScopedElement scopedElement( std::string const& name ); + + XmlWriter& endElement(); + + XmlWriter& writeAttribute( std::string const& name, std::string const& attribute ); + + XmlWriter& writeAttribute( std::string const& name, const char* attribute ); + + XmlWriter& writeAttribute( std::string const& name, bool attribute ); + + template + XmlWriter& writeAttribute( std::string const& name, T const& attribute ) { + std::stringstream rss; + rss << attribute; + return writeAttribute( name, rss.str() ); + } + + XmlWriter& writeText( std::string const& text, bool indent = true ); + + //XmlWriter& writeComment( std::string const& text ); + + //void writeStylesheetRef( std::string const& url ); + + //XmlWriter& writeBlankLine(); + + void ensureTagClosed(); + + private: + + void writeDeclaration(); + + void newlineIfNecessary(); + + bool m_tagIsOpen = false; + bool m_needsNewline = false; + std::vector m_tags; + std::string m_indent; + std::ostream& m_os; + }; + +// ================================================================================================= +// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.h/cpp +// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched. +// ================================================================================================= + +using uchar = unsigned char; + +namespace { + + size_t trailingBytes(unsigned char c) { + if ((c & 0xE0) == 0xC0) { + return 2; + } + if ((c & 0xF0) == 0xE0) { + return 3; + } + if ((c & 0xF8) == 0xF0) { + return 4; + } + DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered"); + } + + uint32_t headerValue(unsigned char c) { + if ((c & 0xE0) == 0xC0) { + return c & 0x1F; + } + if ((c & 0xF0) == 0xE0) { + return c & 0x0F; + } + if ((c & 0xF8) == 0xF0) { + return c & 0x07; + } + DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered"); + } + + void hexEscapeChar(std::ostream& os, unsigned char c) { + std::ios_base::fmtflags f(os.flags()); + os << "\\x" + << std::uppercase << std::hex << std::setfill('0') << std::setw(2) + << static_cast(c); + os.flags(f); + } + +} // anonymous namespace + + XmlEncode::XmlEncode( std::string const& str, ForWhat forWhat ) + : m_str( str ), + m_forWhat( forWhat ) + {} + + void XmlEncode::encodeTo( std::ostream& os ) const { + // Apostrophe escaping not necessary if we always use " to write attributes + // (see: https://www.w3.org/TR/xml/#syntax) + + for( std::size_t idx = 0; idx < m_str.size(); ++ idx ) { + uchar c = m_str[idx]; + switch (c) { + case '<': os << "<"; break; + case '&': os << "&"; break; + + case '>': + // See: https://www.w3.org/TR/xml/#syntax + if (idx > 2 && m_str[idx - 1] == ']' && m_str[idx - 2] == ']') + os << ">"; + else + os << c; + break; + + case '\"': + if (m_forWhat == ForAttributes) + os << """; + else + os << c; + break; + + default: + // Check for control characters and invalid utf-8 + + // Escape control characters in standard ascii + // see https://stackoverflow.com/questions/404107/why-are-control-characters-illegal-in-xml-1-0 + if (c < 0x09 || (c > 0x0D && c < 0x20) || c == 0x7F) { + hexEscapeChar(os, c); + break; + } + + // Plain ASCII: Write it to stream + if (c < 0x7F) { + os << c; + break; + } + + // UTF-8 territory + // Check if the encoding is valid and if it is not, hex escape bytes. + // Important: We do not check the exact decoded values for validity, only the encoding format + // First check that this bytes is a valid lead byte: + // This means that it is not encoded as 1111 1XXX + // Or as 10XX XXXX + if (c < 0xC0 || + c >= 0xF8) { + hexEscapeChar(os, c); + break; + } + + auto encBytes = trailingBytes(c); + // Are there enough bytes left to avoid accessing out-of-bounds memory? + if (idx + encBytes - 1 >= m_str.size()) { + hexEscapeChar(os, c); + break; + } + // The header is valid, check data + // The next encBytes bytes must together be a valid utf-8 + // This means: bitpattern 10XX XXXX and the extracted value is sane (ish) + bool valid = true; + uint32_t value = headerValue(c); + for (std::size_t n = 1; n < encBytes; ++n) { + uchar nc = m_str[idx + n]; + valid &= ((nc & 0xC0) == 0x80); + value = (value << 6) | (nc & 0x3F); + } + + if ( + // Wrong bit pattern of following bytes + (!valid) || + // Overlong encodings + (value < 0x80) || + ( value < 0x800 && encBytes > 2) || // removed "0x80 <= value &&" because redundant + (0x800 < value && value < 0x10000 && encBytes > 3) || + // Encoded value out of range + (value >= 0x110000) + ) { + hexEscapeChar(os, c); + break; + } + + // If we got here, this is in fact a valid(ish) utf-8 sequence + for (std::size_t n = 0; n < encBytes; ++n) { + os << m_str[idx + n]; + } + idx += encBytes - 1; + break; + } + } + } + + std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ) { + xmlEncode.encodeTo( os ); + return os; + } + + XmlWriter::ScopedElement::ScopedElement( XmlWriter* writer ) + : m_writer( writer ) + {} + + XmlWriter::ScopedElement::ScopedElement( ScopedElement&& other ) DOCTEST_NOEXCEPT + : m_writer( other.m_writer ){ + other.m_writer = nullptr; + } + XmlWriter::ScopedElement& XmlWriter::ScopedElement::operator=( ScopedElement&& other ) DOCTEST_NOEXCEPT { + if ( m_writer ) { + m_writer->endElement(); + } + m_writer = other.m_writer; + other.m_writer = nullptr; + return *this; + } + + + XmlWriter::ScopedElement::~ScopedElement() { + if( m_writer ) + m_writer->endElement(); + } + + XmlWriter::ScopedElement& XmlWriter::ScopedElement::writeText( std::string const& text, bool indent ) { + m_writer->writeText( text, indent ); + return *this; + } + + XmlWriter::XmlWriter( std::ostream& os ) : m_os( os ) + { + writeDeclaration(); + } + + XmlWriter::~XmlWriter() { + while( !m_tags.empty() ) + endElement(); + } + + XmlWriter& XmlWriter::startElement( std::string const& name ) { + ensureTagClosed(); + newlineIfNecessary(); + m_os << m_indent << '<' << name; + m_tags.push_back( name ); + m_indent += " "; + m_tagIsOpen = true; + return *this; + } + + XmlWriter::ScopedElement XmlWriter::scopedElement( std::string const& name ) { + ScopedElement scoped( this ); + startElement( name ); + return scoped; + } + + XmlWriter& XmlWriter::endElement() { + newlineIfNecessary(); + m_indent = m_indent.substr( 0, m_indent.size()-2 ); + if( m_tagIsOpen ) { + m_os << "/>"; + m_tagIsOpen = false; + } + else { + m_os << m_indent << ""; + } + m_os << std::endl; + m_tags.pop_back(); + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, std::string const& attribute ) { + if( !name.empty() && !attribute.empty() ) + m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, const char* attribute ) { + if( !name.empty() && attribute && attribute[0] != '\0' ) + m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, bool attribute ) { + m_os << ' ' << name << "=\"" << ( attribute ? "true" : "false" ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeText( std::string const& text, bool indent ) { + if( !text.empty() ){ + bool tagWasOpen = m_tagIsOpen; + ensureTagClosed(); + if( tagWasOpen && indent ) + m_os << m_indent; + m_os << XmlEncode( text ); + m_needsNewline = true; + } + return *this; + } + + //XmlWriter& XmlWriter::writeComment( std::string const& text ) { + // ensureTagClosed(); + // m_os << m_indent << ""; + // m_needsNewline = true; + // return *this; + //} + + //void XmlWriter::writeStylesheetRef( std::string const& url ) { + // m_os << "\n"; + //} + + //XmlWriter& XmlWriter::writeBlankLine() { + // ensureTagClosed(); + // m_os << '\n'; + // return *this; + //} + + void XmlWriter::ensureTagClosed() { + if( m_tagIsOpen ) { + m_os << ">" << std::endl; + m_tagIsOpen = false; + } + } + + void XmlWriter::writeDeclaration() { + m_os << "\n"; + } + + void XmlWriter::newlineIfNecessary() { + if( m_needsNewline ) { + m_os << std::endl; + m_needsNewline = false; + } + } + +// ================================================================================================= +// End of copy-pasted code from Catch +// ================================================================================================= + + // clang-format on + + struct XmlReporter : public IReporter + { + XmlWriter xml; + std::mutex mutex; + + // caching pointers/references to objects of these types - safe to do + const ContextOptions& opt; + const TestCaseData* tc = nullptr; + + XmlReporter(const ContextOptions& co) + : xml(*co.cout) + , opt(co) {} + + void log_contexts() { + int num_contexts = get_num_active_contexts(); + if(num_contexts) { + auto contexts = get_active_contexts(); + std::stringstream ss; + for(int i = 0; i < num_contexts; ++i) { + contexts[i]->stringify(&ss); + xml.scopedElement("Info").writeText(ss.str()); + ss.str(""); + } + } + } + + unsigned line(unsigned l) const { return opt.no_line_numbers ? 0 : l; } + + void test_case_start_impl(const TestCaseData& in) { + bool open_ts_tag = false; + if(tc != nullptr) { // we have already opened a test suite + if(std::strcmp(tc->m_test_suite, in.m_test_suite) != 0) { + xml.endElement(); + open_ts_tag = true; + } + } + else { + open_ts_tag = true; // first test case ==> first test suite + } + + if(open_ts_tag) { + xml.startElement("TestSuite"); + xml.writeAttribute("name", in.m_test_suite); + } + + tc = ∈ + xml.startElement("TestCase") + .writeAttribute("name", in.m_name) + .writeAttribute("filename", skipPathFromFilename(in.m_file.c_str())) + .writeAttribute("line", line(in.m_line)) + .writeAttribute("description", in.m_description); + + if(Approx(in.m_timeout) != 0) + xml.writeAttribute("timeout", in.m_timeout); + if(in.m_may_fail) + xml.writeAttribute("may_fail", true); + if(in.m_should_fail) + xml.writeAttribute("should_fail", true); + } + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData& in) override { + test_run_start(); + if(opt.list_reporters) { + for(auto& curr : getListeners()) + xml.scopedElement("Listener") + .writeAttribute("priority", curr.first.first) + .writeAttribute("name", curr.first.second); + for(auto& curr : getReporters()) + xml.scopedElement("Reporter") + .writeAttribute("priority", curr.first.first) + .writeAttribute("name", curr.first.second); + } else if(opt.count || opt.list_test_cases) { + for(unsigned i = 0; i < in.num_data; ++i) { + xml.scopedElement("TestCase").writeAttribute("name", in.data[i]->m_name) + .writeAttribute("testsuite", in.data[i]->m_test_suite) + .writeAttribute("filename", skipPathFromFilename(in.data[i]->m_file.c_str())) + .writeAttribute("line", line(in.data[i]->m_line)) + .writeAttribute("skipped", in.data[i]->m_skip); + } + xml.scopedElement("OverallResultsTestCases") + .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters); + } else if(opt.list_test_suites) { + for(unsigned i = 0; i < in.num_data; ++i) + xml.scopedElement("TestSuite").writeAttribute("name", in.data[i]->m_test_suite); + xml.scopedElement("OverallResultsTestCases") + .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters); + xml.scopedElement("OverallResultsTestSuites") + .writeAttribute("unskipped", in.run_stats->numTestSuitesPassingFilters); + } + xml.endElement(); + } + + void test_run_start() override { + // remove .exe extension - mainly to have the same output on UNIX and Windows + std::string binary_name = skipPathFromFilename(opt.binary_name.c_str()); +#ifdef DOCTEST_PLATFORM_WINDOWS + if(binary_name.rfind(".exe") != std::string::npos) + binary_name = binary_name.substr(0, binary_name.length() - 4); +#endif // DOCTEST_PLATFORM_WINDOWS + + xml.startElement("doctest").writeAttribute("binary", binary_name); + if(opt.no_version == false) + xml.writeAttribute("version", DOCTEST_VERSION_STR); + + // only the consequential ones (TODO: filters) + xml.scopedElement("Options") + .writeAttribute("order_by", opt.order_by.c_str()) + .writeAttribute("rand_seed", opt.rand_seed) + .writeAttribute("first", opt.first) + .writeAttribute("last", opt.last) + .writeAttribute("abort_after", opt.abort_after) + .writeAttribute("subcase_filter_levels", opt.subcase_filter_levels) + .writeAttribute("case_sensitive", opt.case_sensitive) + .writeAttribute("no_throw", opt.no_throw) + .writeAttribute("no_skip", opt.no_skip); + } + + void test_run_end(const TestRunStats& p) override { + if(tc) // the TestSuite tag - only if there has been at least 1 test case + xml.endElement(); + + xml.scopedElement("OverallResultsAsserts") + .writeAttribute("successes", p.numAsserts - p.numAssertsFailed) + .writeAttribute("failures", p.numAssertsFailed); + + xml.startElement("OverallResultsTestCases") + .writeAttribute("successes", + p.numTestCasesPassingFilters - p.numTestCasesFailed) + .writeAttribute("failures", p.numTestCasesFailed); + if(opt.no_skipped_summary == false) + xml.writeAttribute("skipped", p.numTestCases - p.numTestCasesPassingFilters); + xml.endElement(); + + xml.endElement(); + } + + void test_case_start(const TestCaseData& in) override { + test_case_start_impl(in); + xml.ensureTagClosed(); + } + + void test_case_reenter(const TestCaseData&) override {} + + void test_case_end(const CurrentTestCaseStats& st) override { + xml.startElement("OverallResultsAsserts") + .writeAttribute("successes", + st.numAssertsCurrentTest - st.numAssertsFailedCurrentTest) + .writeAttribute("failures", st.numAssertsFailedCurrentTest) + .writeAttribute("test_case_success", st.testCaseSuccess); + if(opt.duration) + xml.writeAttribute("duration", st.seconds); + if(tc->m_expected_failures) + xml.writeAttribute("expected_failures", tc->m_expected_failures); + xml.endElement(); + + xml.endElement(); + } + + void test_case_exception(const TestCaseException& e) override { + std::lock_guard lock(mutex); + + xml.scopedElement("Exception") + .writeAttribute("crash", e.is_crash) + .writeText(e.error_string.c_str()); + } + + void subcase_start(const SubcaseSignature& in) override { + xml.startElement("SubCase") + .writeAttribute("name", in.m_name) + .writeAttribute("filename", skipPathFromFilename(in.m_file)) + .writeAttribute("line", line(in.m_line)); + xml.ensureTagClosed(); + } + + void subcase_end() override { xml.endElement(); } + + void log_assert(const AssertData& rb) override { + if(!rb.m_failed && !opt.success) + return; + + std::lock_guard lock(mutex); + + xml.startElement("Expression") + .writeAttribute("success", !rb.m_failed) + .writeAttribute("type", assertString(rb.m_at)) + .writeAttribute("filename", skipPathFromFilename(rb.m_file)) + .writeAttribute("line", line(rb.m_line)); + + xml.scopedElement("Original").writeText(rb.m_expr); + + if(rb.m_threw) + xml.scopedElement("Exception").writeText(rb.m_exception.c_str()); + + if(rb.m_at & assertType::is_throws_as) + xml.scopedElement("ExpectedException").writeText(rb.m_exception_type); + if(rb.m_at & assertType::is_throws_with) + xml.scopedElement("ExpectedExceptionString").writeText(rb.m_exception_string); + if((rb.m_at & assertType::is_normal) && !rb.m_threw) + xml.scopedElement("Expanded").writeText(rb.m_decomp.c_str()); + + log_contexts(); + + xml.endElement(); + } + + void log_message(const MessageData& mb) override { + std::lock_guard lock(mutex); + + xml.startElement("Message") + .writeAttribute("type", failureString(mb.m_severity)) + .writeAttribute("filename", skipPathFromFilename(mb.m_file)) + .writeAttribute("line", line(mb.m_line)); + + xml.scopedElement("Text").writeText(mb.m_string.c_str()); + + log_contexts(); + + xml.endElement(); + } + + void test_case_skipped(const TestCaseData& in) override { + if(opt.no_skipped_summary == false) { + test_case_start_impl(in); + xml.writeAttribute("skipped", "true"); + xml.endElement(); + } + } + }; + + DOCTEST_REGISTER_REPORTER("xml", 0, XmlReporter); + + void fulltext_log_assert_to_stream(std::ostream& s, const AssertData& rb) { + if((rb.m_at & (assertType::is_throws_as | assertType::is_throws_with)) == + 0) //!OCLINT bitwise operator in conditional + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << " ) " + << Color::None; + + if(rb.m_at & assertType::is_throws) { //!OCLINT bitwise operator in conditional + s << (rb.m_threw ? "threw as expected!" : "did NOT throw at all!") << "\n"; + } else if((rb.m_at & assertType::is_throws_as) && + (rb.m_at & assertType::is_throws_with)) { //!OCLINT + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \"" + << rb.m_exception_string << "\", " << rb.m_exception_type << " ) " << Color::None; + if(rb.m_threw) { + if(!rb.m_failed) { + s << "threw as expected!\n"; + } else { + s << "threw a DIFFERENT exception! (contents: " << rb.m_exception << ")\n"; + } + } else { + s << "did NOT throw at all!\n"; + } + } else if(rb.m_at & + assertType::is_throws_as) { //!OCLINT bitwise operator in conditional + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", " + << rb.m_exception_type << " ) " << Color::None + << (rb.m_threw ? (rb.m_threw_as ? "threw as expected!" : + "threw a DIFFERENT exception: ") : + "did NOT throw at all!") + << Color::Cyan << rb.m_exception << "\n"; + } else if(rb.m_at & + assertType::is_throws_with) { //!OCLINT bitwise operator in conditional + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \"" + << rb.m_exception_string << "\" ) " << Color::None + << (rb.m_threw ? (!rb.m_failed ? "threw as expected!" : + "threw a DIFFERENT exception: ") : + "did NOT throw at all!") + << Color::Cyan << rb.m_exception << "\n"; + } else if(rb.m_at & assertType::is_nothrow) { //!OCLINT bitwise operator in conditional + s << (rb.m_threw ? "THREW exception: " : "didn't throw!") << Color::Cyan + << rb.m_exception << "\n"; + } else { + s << (rb.m_threw ? "THREW exception: " : + (!rb.m_failed ? "is correct!\n" : "is NOT correct!\n")); + if(rb.m_threw) + s << rb.m_exception << "\n"; + else + s << " values: " << assertString(rb.m_at) << "( " << rb.m_decomp << " )\n"; + } + } + + // TODO: + // - log_message() + // - respond to queries + // - honor remaining options + // - more attributes in tags + struct JUnitReporter : public IReporter + { + XmlWriter xml; + std::mutex mutex; + Timer timer; + std::vector deepestSubcaseStackNames; + + struct JUnitTestCaseData + { + static std::string getCurrentTimestamp() { + // Beware, this is not reentrant because of backward compatibility issues + // Also, UTC only, again because of backward compatibility (%z is C++11) + time_t rawtime; + std::time(&rawtime); + auto const timeStampSize = sizeof("2017-01-16T17:06:45Z"); + + std::tm timeInfo; +#ifdef DOCTEST_PLATFORM_WINDOWS + gmtime_s(&timeInfo, &rawtime); +#else // DOCTEST_PLATFORM_WINDOWS + gmtime_r(&rawtime, &timeInfo); +#endif // DOCTEST_PLATFORM_WINDOWS + + char timeStamp[timeStampSize]; + const char* const fmt = "%Y-%m-%dT%H:%M:%SZ"; + + std::strftime(timeStamp, timeStampSize, fmt, &timeInfo); + return std::string(timeStamp); + } + + struct JUnitTestMessage + { + JUnitTestMessage(const std::string& _message, const std::string& _type, const std::string& _details) + : message(_message), type(_type), details(_details) {} + + JUnitTestMessage(const std::string& _message, const std::string& _details) + : message(_message), type(), details(_details) {} + + std::string message, type, details; + }; + + struct JUnitTestCase + { + JUnitTestCase(const std::string& _classname, const std::string& _name) + : classname(_classname), name(_name), time(0), failures() {} + + std::string classname, name; + double time; + std::vector failures, errors; + }; + + void add(const std::string& classname, const std::string& name) { + testcases.emplace_back(classname, name); + } + + void appendSubcaseNamesToLastTestcase(std::vector nameStack) { + for(auto& curr: nameStack) + if(curr.size()) + testcases.back().name += std::string("/") + curr.c_str(); + } + + void addTime(double time) { + if(time < 1e-4) + time = 0; + testcases.back().time = time; + totalSeconds += time; + } + + void addFailure(const std::string& message, const std::string& type, const std::string& details) { + testcases.back().failures.emplace_back(message, type, details); + ++totalFailures; + } + + void addError(const std::string& message, const std::string& details) { + testcases.back().errors.emplace_back(message, details); + ++totalErrors; + } + + std::vector testcases; + double totalSeconds = 0; + int totalErrors = 0, totalFailures = 0; + }; + + JUnitTestCaseData testCaseData; + + // caching pointers/references to objects of these types - safe to do + const ContextOptions& opt; + const TestCaseData* tc = nullptr; + + JUnitReporter(const ContextOptions& co) + : xml(*co.cout) + , opt(co) {} + + unsigned line(unsigned l) const { return opt.no_line_numbers ? 0 : l; } + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData&) override {} + + void test_run_start() override {} + + void test_run_end(const TestRunStats& p) override { + // remove .exe extension - mainly to have the same output on UNIX and Windows + std::string binary_name = skipPathFromFilename(opt.binary_name.c_str()); +#ifdef DOCTEST_PLATFORM_WINDOWS + if(binary_name.rfind(".exe") != std::string::npos) + binary_name = binary_name.substr(0, binary_name.length() - 4); +#endif // DOCTEST_PLATFORM_WINDOWS + xml.startElement("testsuites"); + xml.startElement("testsuite").writeAttribute("name", binary_name) + .writeAttribute("errors", testCaseData.totalErrors) + .writeAttribute("failures", testCaseData.totalFailures) + .writeAttribute("tests", p.numAsserts); + if(opt.no_time_in_output == false) { + xml.writeAttribute("time", testCaseData.totalSeconds); + xml.writeAttribute("timestamp", JUnitTestCaseData::getCurrentTimestamp()); + } + if(opt.no_version == false) + xml.writeAttribute("doctest_version", DOCTEST_VERSION_STR); + + for(const auto& testCase : testCaseData.testcases) { + xml.startElement("testcase") + .writeAttribute("classname", testCase.classname) + .writeAttribute("name", testCase.name); + if(opt.no_time_in_output == false) + xml.writeAttribute("time", testCase.time); + // This is not ideal, but it should be enough to mimic gtest's junit output. + xml.writeAttribute("status", "run"); + + for(const auto& failure : testCase.failures) { + xml.scopedElement("failure") + .writeAttribute("message", failure.message) + .writeAttribute("type", failure.type) + .writeText(failure.details, false); + } + + for(const auto& error : testCase.errors) { + xml.scopedElement("error") + .writeAttribute("message", error.message) + .writeText(error.details); + } + + xml.endElement(); + } + xml.endElement(); + xml.endElement(); + } + + void test_case_start(const TestCaseData& in) override { + testCaseData.add(skipPathFromFilename(in.m_file.c_str()), in.m_name); + timer.start(); + } + + void test_case_reenter(const TestCaseData& in) override { + testCaseData.addTime(timer.getElapsedSeconds()); + testCaseData.appendSubcaseNamesToLastTestcase(deepestSubcaseStackNames); + deepestSubcaseStackNames.clear(); + + timer.start(); + testCaseData.add(skipPathFromFilename(in.m_file.c_str()), in.m_name); + } + + void test_case_end(const CurrentTestCaseStats&) override { + testCaseData.addTime(timer.getElapsedSeconds()); + testCaseData.appendSubcaseNamesToLastTestcase(deepestSubcaseStackNames); + deepestSubcaseStackNames.clear(); + } + + void test_case_exception(const TestCaseException& e) override { + std::lock_guard lock(mutex); + testCaseData.addError("exception", e.error_string.c_str()); + } + + void subcase_start(const SubcaseSignature& in) override { + deepestSubcaseStackNames.push_back(in.m_name); + } + + void subcase_end() override {} + + void log_assert(const AssertData& rb) override { + if(!rb.m_failed) // report only failures & ignore the `success` option + return; + + std::lock_guard lock(mutex); + + std::ostringstream os; + os << skipPathFromFilename(rb.m_file) << (opt.gnu_file_line ? ":" : "(") + << line(rb.m_line) << (opt.gnu_file_line ? ":" : "):") << std::endl; + + fulltext_log_assert_to_stream(os, rb); + log_contexts(os); + testCaseData.addFailure(rb.m_decomp.c_str(), assertString(rb.m_at), os.str()); + } + + void log_message(const MessageData&) override {} + + void test_case_skipped(const TestCaseData&) override {} + + void log_contexts(std::ostringstream& s) { + int num_contexts = get_num_active_contexts(); + if(num_contexts) { + auto contexts = get_active_contexts(); + + s << " logged: "; + for(int i = 0; i < num_contexts; ++i) { + s << (i == 0 ? "" : " "); + contexts[i]->stringify(&s); + s << std::endl; + } + } + } + }; + + DOCTEST_REGISTER_REPORTER("junit", 0, JUnitReporter); + + struct Whitespace + { + int nrSpaces; + explicit Whitespace(int nr) + : nrSpaces(nr) {} + }; + + std::ostream& operator<<(std::ostream& out, const Whitespace& ws) { + if(ws.nrSpaces != 0) + out << std::setw(ws.nrSpaces) << ' '; + return out; + } + + struct ConsoleReporter : public IReporter + { + std::ostream& s; + bool hasLoggedCurrentTestStart; + std::vector subcasesStack; + size_t currentSubcaseLevel; + std::mutex mutex; + + // caching pointers/references to objects of these types - safe to do + const ContextOptions& opt; + const TestCaseData* tc; + + ConsoleReporter(const ContextOptions& co) + : s(*co.cout) + , opt(co) {} + + ConsoleReporter(const ContextOptions& co, std::ostream& ostr) + : s(ostr) + , opt(co) {} + + // ========================================================================================= + // WHAT FOLLOWS ARE HELPERS USED BY THE OVERRIDES OF THE VIRTUAL METHODS OF THE INTERFACE + // ========================================================================================= + + void separator_to_stream() { + s << Color::Yellow + << "===============================================================================" + "\n"; + } + + const char* getSuccessOrFailString(bool success, assertType::Enum at, + const char* success_str) { + if(success) + return success_str; + return failureString(at); + } + + Color::Enum getSuccessOrFailColor(bool success, assertType::Enum at) { + return success ? Color::BrightGreen : + (at & assertType::is_warn) ? Color::Yellow : Color::Red; + } + + void successOrFailColoredStringToStream(bool success, assertType::Enum at, + const char* success_str = "SUCCESS") { + s << getSuccessOrFailColor(success, at) + << getSuccessOrFailString(success, at, success_str) << ": "; + } + + void log_contexts() { + int num_contexts = get_num_active_contexts(); + if(num_contexts) { + auto contexts = get_active_contexts(); + + s << Color::None << " logged: "; + for(int i = 0; i < num_contexts; ++i) { + s << (i == 0 ? "" : " "); + contexts[i]->stringify(&s); + s << "\n"; + } + } + + s << "\n"; + } + + // this was requested to be made virtual so users could override it + virtual void file_line_to_stream(const char* file, int line, + const char* tail = "") { + s << Color::LightGrey << skipPathFromFilename(file) << (opt.gnu_file_line ? ":" : "(") + << (opt.no_line_numbers ? 0 : line) // 0 or the real num depending on the option + << (opt.gnu_file_line ? ":" : "):") << tail; + } + + void logTestStart() { + if(hasLoggedCurrentTestStart) + return; + + separator_to_stream(); + file_line_to_stream(tc->m_file.c_str(), tc->m_line, "\n"); + if(tc->m_description) + s << Color::Yellow << "DESCRIPTION: " << Color::None << tc->m_description << "\n"; + if(tc->m_test_suite && tc->m_test_suite[0] != '\0') + s << Color::Yellow << "TEST SUITE: " << Color::None << tc->m_test_suite << "\n"; + if(strncmp(tc->m_name, " Scenario:", 11) != 0) + s << Color::Yellow << "TEST CASE: "; + s << Color::None << tc->m_name << "\n"; + + for(size_t i = 0; i < currentSubcaseLevel; ++i) { + if(subcasesStack[i].m_name[0] != '\0') + s << " " << subcasesStack[i].m_name << "\n"; + } + + if(currentSubcaseLevel != subcasesStack.size()) { + s << Color::Yellow << "\nDEEPEST SUBCASE STACK REACHED (DIFFERENT FROM THE CURRENT ONE):\n" << Color::None; + for(size_t i = 0; i < subcasesStack.size(); ++i) { + if(subcasesStack[i].m_name[0] != '\0') + s << " " << subcasesStack[i].m_name << "\n"; + } + } + + s << "\n"; + + hasLoggedCurrentTestStart = true; + } + + void printVersion() { + if(opt.no_version == false) + s << Color::Cyan << "[doctest] " << Color::None << "doctest version is \"" + << DOCTEST_VERSION_STR << "\"\n"; + } + + void printIntro() { + if(opt.no_intro == false) { + printVersion(); + s << Color::Cyan << "[doctest] " << Color::None + << "run with \"--" DOCTEST_OPTIONS_PREFIX_DISPLAY "help\" for options\n"; + } + } + + void printHelp() { + int sizePrefixDisplay = static_cast(strlen(DOCTEST_OPTIONS_PREFIX_DISPLAY)); + printVersion(); + // clang-format off + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "boolean values: \"1/on/yes/true\" or \"0/off/no/false\"\n"; + s << Color::Cyan << "[doctest] " << Color::None; + s << "filter values: \"str1,str2,str3\" (comma separated strings)\n"; + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "filters use wildcards for matching strings\n"; + s << Color::Cyan << "[doctest] " << Color::None; + s << "something passes a filter if any of the strings in a filter matches\n"; +#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "ALL FLAGS, OPTIONS AND FILTERS ALSO AVAILABLE WITH A \"" DOCTEST_CONFIG_OPTIONS_PREFIX "\" PREFIX!!!\n"; +#endif + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "Query flags - the program quits after them. Available:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "?, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "help, -" DOCTEST_OPTIONS_PREFIX_DISPLAY "h " + << Whitespace(sizePrefixDisplay*0) << "prints this message\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "v, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "version " + << Whitespace(sizePrefixDisplay*1) << "prints the version\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "c, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "count " + << Whitespace(sizePrefixDisplay*1) << "prints the number of matching tests\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ltc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-cases " + << Whitespace(sizePrefixDisplay*1) << "lists all matching tests by name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-suites " + << Whitespace(sizePrefixDisplay*1) << "lists all matching test suites\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-reporters " + << Whitespace(sizePrefixDisplay*1) << "lists all registered reporters\n\n"; + // ================================================================================== << 79 + s << Color::Cyan << "[doctest] " << Color::None; + s << "The available / options/filters are:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case= " + << Whitespace(sizePrefixDisplay*1) << "filters tests by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file= " + << Whitespace(sizePrefixDisplay*1) << "filters tests by their file\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sfe, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their file\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite= " + << Whitespace(sizePrefixDisplay*1) << "filters tests by their test suite\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tse, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their test suite\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase= " + << Whitespace(sizePrefixDisplay*1) << "filters subcases by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT subcases by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "r, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "reporters= " + << Whitespace(sizePrefixDisplay*1) << "reporters to use (console is default)\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "o, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "out= " + << Whitespace(sizePrefixDisplay*1) << "output filename\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ob, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "order-by= " + << Whitespace(sizePrefixDisplay*1) << "how the tests should be ordered\n"; + s << Whitespace(sizePrefixDisplay*3) << " - [file/suite/name/rand/none]\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "rs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "rand-seed= " + << Whitespace(sizePrefixDisplay*1) << "seed for random ordering\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "f, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "first= " + << Whitespace(sizePrefixDisplay*1) << "the first test passing the filters to\n"; + s << Whitespace(sizePrefixDisplay*3) << " execute - for range-based execution\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "l, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "last= " + << Whitespace(sizePrefixDisplay*1) << "the last test passing the filters to\n"; + s << Whitespace(sizePrefixDisplay*3) << " execute - for range-based execution\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "aa, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "abort-after= " + << Whitespace(sizePrefixDisplay*1) << "stop after failed assertions\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "scfl,--" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-filter-levels= " + << Whitespace(sizePrefixDisplay*1) << "apply filters for the first levels\n"; + s << Color::Cyan << "\n[doctest] " << Color::None; + s << "Bool options - can be used like flags and true is assumed. Available:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "s, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "success= " + << Whitespace(sizePrefixDisplay*1) << "include successful assertions in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "cs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "case-sensitive= " + << Whitespace(sizePrefixDisplay*1) << "filters being treated as case sensitive\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "e, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "exit= " + << Whitespace(sizePrefixDisplay*1) << "exits after the tests finish\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "d, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "duration= " + << Whitespace(sizePrefixDisplay*1) << "prints the time duration of each test\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "m, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "minimal= " + << Whitespace(sizePrefixDisplay*1) << "minimal console output (only failures)\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "q, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "quiet= " + << Whitespace(sizePrefixDisplay*1) << "no console output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nt, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-throw= " + << Whitespace(sizePrefixDisplay*1) << "skips exceptions-related assert checks\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ne, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-exitcode= " + << Whitespace(sizePrefixDisplay*1) << "returns (or exits) always with success\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-run= " + << Whitespace(sizePrefixDisplay*1) << "skips all runtime doctest operations\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ni, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-intro= " + << Whitespace(sizePrefixDisplay*1) << "omit the framework intro in the output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nv, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-version= " + << Whitespace(sizePrefixDisplay*1) << "omit the framework version in the output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-colors= " + << Whitespace(sizePrefixDisplay*1) << "disables colors in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "fc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "force-colors= " + << Whitespace(sizePrefixDisplay*1) << "use colors even when not in a tty\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nb, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-breaks= " + << Whitespace(sizePrefixDisplay*1) << "disables breakpoints in debuggers\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ns, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-skip= " + << Whitespace(sizePrefixDisplay*1) << "don't skip test cases marked as skip\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "gfl, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "gnu-file-line= " + << Whitespace(sizePrefixDisplay*1) << ":n: vs (n): for line numbers in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "npf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-path-filenames= " + << Whitespace(sizePrefixDisplay*1) << "only filenames and no paths in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nln, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-line-numbers= " + << Whitespace(sizePrefixDisplay*1) << "0 instead of real line numbers in output\n"; + // ================================================================================== << 79 + // clang-format on + + s << Color::Cyan << "\n[doctest] " << Color::None; + s << "for more information visit the project documentation\n\n"; + } + + void printRegisteredReporters() { + printVersion(); + auto printReporters = [this] (const reporterMap& reporters, const char* type) { + if(reporters.size()) { + s << Color::Cyan << "[doctest] " << Color::None << "listing all registered " << type << "\n"; + for(auto& curr : reporters) + s << "priority: " << std::setw(5) << curr.first.first + << " name: " << curr.first.second << "\n"; + } + }; + printReporters(getListeners(), "listeners"); + printReporters(getReporters(), "reporters"); + } + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData& in) override { + if(opt.version) { + printVersion(); + } else if(opt.help) { + printHelp(); + } else if(opt.list_reporters) { + printRegisteredReporters(); + } else if(opt.count || opt.list_test_cases) { + if(opt.list_test_cases) { + s << Color::Cyan << "[doctest] " << Color::None + << "listing all test case names\n"; + separator_to_stream(); + } + + for(unsigned i = 0; i < in.num_data; ++i) + s << Color::None << in.data[i]->m_name << "\n"; + + separator_to_stream(); + + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " + << g_cs->numTestCasesPassingFilters << "\n"; + + } else if(opt.list_test_suites) { + s << Color::Cyan << "[doctest] " << Color::None << "listing all test suites\n"; + separator_to_stream(); + + for(unsigned i = 0; i < in.num_data; ++i) + s << Color::None << in.data[i]->m_test_suite << "\n"; + + separator_to_stream(); + + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " + << g_cs->numTestCasesPassingFilters << "\n"; + s << Color::Cyan << "[doctest] " << Color::None + << "test suites with unskipped test cases passing the current filters: " + << g_cs->numTestSuitesPassingFilters << "\n"; + } + } + + void test_run_start() override { + if(!opt.minimal) + printIntro(); + } + + void test_run_end(const TestRunStats& p) override { + if(opt.minimal && p.numTestCasesFailed == 0) + return; + + separator_to_stream(); + s << std::dec; + + auto totwidth = int(std::ceil(log10((std::max(p.numTestCasesPassingFilters, static_cast(p.numAsserts))) + 1))); + auto passwidth = int(std::ceil(log10((std::max(p.numTestCasesPassingFilters - p.numTestCasesFailed, static_cast(p.numAsserts - p.numAssertsFailed))) + 1))); + auto failwidth = int(std::ceil(log10((std::max(p.numTestCasesFailed, static_cast(p.numAssertsFailed))) + 1))); + const bool anythingFailed = p.numTestCasesFailed > 0 || p.numAssertsFailed > 0; + s << Color::Cyan << "[doctest] " << Color::None << "test cases: " << std::setw(totwidth) + << p.numTestCasesPassingFilters << " | " + << ((p.numTestCasesPassingFilters == 0 || anythingFailed) ? Color::None : + Color::Green) + << std::setw(passwidth) << p.numTestCasesPassingFilters - p.numTestCasesFailed << " passed" + << Color::None << " | " << (p.numTestCasesFailed > 0 ? Color::Red : Color::None) + << std::setw(failwidth) << p.numTestCasesFailed << " failed" << Color::None << " |"; + if(opt.no_skipped_summary == false) { + const int numSkipped = p.numTestCases - p.numTestCasesPassingFilters; + s << " " << (numSkipped == 0 ? Color::None : Color::Yellow) << numSkipped + << " skipped" << Color::None; + } + s << "\n"; + s << Color::Cyan << "[doctest] " << Color::None << "assertions: " << std::setw(totwidth) + << p.numAsserts << " | " + << ((p.numAsserts == 0 || anythingFailed) ? Color::None : Color::Green) + << std::setw(passwidth) << (p.numAsserts - p.numAssertsFailed) << " passed" << Color::None + << " | " << (p.numAssertsFailed > 0 ? Color::Red : Color::None) << std::setw(failwidth) + << p.numAssertsFailed << " failed" << Color::None << " |\n"; + s << Color::Cyan << "[doctest] " << Color::None + << "Status: " << (p.numTestCasesFailed > 0 ? Color::Red : Color::Green) + << ((p.numTestCasesFailed > 0) ? "FAILURE!" : "SUCCESS!") << Color::None << std::endl; + } + + void test_case_start(const TestCaseData& in) override { + hasLoggedCurrentTestStart = false; + tc = ∈ + subcasesStack.clear(); + currentSubcaseLevel = 0; + } + + void test_case_reenter(const TestCaseData&) override { + subcasesStack.clear(); + } + + void test_case_end(const CurrentTestCaseStats& st) override { + if(tc->m_no_output) + return; + + // log the preamble of the test case only if there is something + // else to print - something other than that an assert has failed + if(opt.duration || + (st.failure_flags && st.failure_flags != TestCaseFailureReason::AssertFailure)) + logTestStart(); + + if(opt.duration) + s << Color::None << std::setprecision(6) << std::fixed << st.seconds + << " s: " << tc->m_name << "\n"; + + if(st.failure_flags & TestCaseFailureReason::Timeout) + s << Color::Red << "Test case exceeded time limit of " << std::setprecision(6) + << std::fixed << tc->m_timeout << "!\n"; + + if(st.failure_flags & TestCaseFailureReason::ShouldHaveFailedButDidnt) { + s << Color::Red << "Should have failed but didn't! Marking it as failed!\n"; + } else if(st.failure_flags & TestCaseFailureReason::ShouldHaveFailedAndDid) { + s << Color::Yellow << "Failed as expected so marking it as not failed\n"; + } else if(st.failure_flags & TestCaseFailureReason::CouldHaveFailedAndDid) { + s << Color::Yellow << "Allowed to fail so marking it as not failed\n"; + } else if(st.failure_flags & TestCaseFailureReason::DidntFailExactlyNumTimes) { + s << Color::Red << "Didn't fail exactly " << tc->m_expected_failures + << " times so marking it as failed!\n"; + } else if(st.failure_flags & TestCaseFailureReason::FailedExactlyNumTimes) { + s << Color::Yellow << "Failed exactly " << tc->m_expected_failures + << " times as expected so marking it as not failed!\n"; + } + if(st.failure_flags & TestCaseFailureReason::TooManyFailedAsserts) { + s << Color::Red << "Aborting - too many failed asserts!\n"; + } + s << Color::None; // lgtm [cpp/useless-expression] + } + + void test_case_exception(const TestCaseException& e) override { + std::lock_guard lock(mutex); + if(tc->m_no_output) + return; + + logTestStart(); + + file_line_to_stream(tc->m_file.c_str(), tc->m_line, " "); + successOrFailColoredStringToStream(false, e.is_crash ? assertType::is_require : + assertType::is_check); + s << Color::Red << (e.is_crash ? "test case CRASHED: " : "test case THREW exception: ") + << Color::Cyan << e.error_string << "\n"; + + int num_stringified_contexts = get_num_stringified_contexts(); + if(num_stringified_contexts) { + auto stringified_contexts = get_stringified_contexts(); + s << Color::None << " logged: "; + for(int i = num_stringified_contexts; i > 0; --i) { + s << (i == num_stringified_contexts ? "" : " ") + << stringified_contexts[i - 1] << "\n"; + } + } + s << "\n" << Color::None; + } + + void subcase_start(const SubcaseSignature& subc) override { + subcasesStack.push_back(subc); + ++currentSubcaseLevel; + hasLoggedCurrentTestStart = false; + } + + void subcase_end() override { + --currentSubcaseLevel; + hasLoggedCurrentTestStart = false; + } + + void log_assert(const AssertData& rb) override { + if((!rb.m_failed && !opt.success) || tc->m_no_output) + return; + + std::lock_guard lock(mutex); + + logTestStart(); + + file_line_to_stream(rb.m_file, rb.m_line, " "); + successOrFailColoredStringToStream(!rb.m_failed, rb.m_at); + + fulltext_log_assert_to_stream(s, rb); + + log_contexts(); + } + + void log_message(const MessageData& mb) override { + if(tc->m_no_output) + return; + + std::lock_guard lock(mutex); + + logTestStart(); + + file_line_to_stream(mb.m_file, mb.m_line, " "); + s << getSuccessOrFailColor(false, mb.m_severity) + << getSuccessOrFailString(mb.m_severity & assertType::is_warn, mb.m_severity, + "MESSAGE") << ": "; + s << Color::None << mb.m_string << "\n"; + log_contexts(); + } + + void test_case_skipped(const TestCaseData&) override {} + }; + + DOCTEST_REGISTER_REPORTER("console", 0, ConsoleReporter); + +#ifdef DOCTEST_PLATFORM_WINDOWS + struct DebugOutputWindowReporter : public ConsoleReporter + { + DOCTEST_THREAD_LOCAL static std::ostringstream oss; + + DebugOutputWindowReporter(const ContextOptions& co) + : ConsoleReporter(co, oss) {} + +#define DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(func, type, arg) \ + void func(type arg) override { \ + bool with_col = g_no_colors; \ + g_no_colors = false; \ + ConsoleReporter::func(arg); \ + if(oss.tellp() != std::streampos{}) { \ + DOCTEST_OUTPUT_DEBUG_STRING(oss.str().c_str()); \ + oss.str(""); \ + } \ + g_no_colors = with_col; \ + } + + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_start, DOCTEST_EMPTY, DOCTEST_EMPTY) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_end, const TestRunStats&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_start, const TestCaseData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_reenter, const TestCaseData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_end, const CurrentTestCaseStats&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_exception, const TestCaseException&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_start, const SubcaseSignature&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_end, DOCTEST_EMPTY, DOCTEST_EMPTY) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_assert, const AssertData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_message, const MessageData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_skipped, const TestCaseData&, in) + }; + + DOCTEST_THREAD_LOCAL std::ostringstream DebugOutputWindowReporter::oss; +#endif // DOCTEST_PLATFORM_WINDOWS + + // the implementation of parseOption() + bool parseOptionImpl(int argc, const char* const* argv, const char* pattern, String* value) { + // going from the end to the beginning and stopping on the first occurrence from the end + for(int i = argc; i > 0; --i) { + auto index = i - 1; + auto temp = std::strstr(argv[index], pattern); + if(temp && (value || strlen(temp) == strlen(pattern))) { //!OCLINT prefer early exits and continue + // eliminate matches in which the chars before the option are not '-' + bool noBadCharsFound = true; + auto curr = argv[index]; + while(curr != temp) { + if(*curr++ != '-') { + noBadCharsFound = false; + break; + } + } + if(noBadCharsFound && argv[index][0] == '-') { + if(value) { + // parsing the value of an option + temp += strlen(pattern); + const unsigned len = strlen(temp); + if(len) { + *value = temp; + return true; + } + } else { + // just a flag - no value + return true; + } + } + } + } + return false; + } + + // parses an option and returns the string after the '=' character + bool parseOption(int argc, const char* const* argv, const char* pattern, String* value = nullptr, + const String& defaultVal = String()) { + if(value) + *value = defaultVal; +#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + // offset (normally 3 for "dt-") to skip prefix + if(parseOptionImpl(argc, argv, pattern + strlen(DOCTEST_CONFIG_OPTIONS_PREFIX), value)) + return true; +#endif // DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + return parseOptionImpl(argc, argv, pattern, value); + } + + // locates a flag on the command line + bool parseFlag(int argc, const char* const* argv, const char* pattern) { + return parseOption(argc, argv, pattern); + } + + // parses a comma separated list of words after a pattern in one of the arguments in argv + bool parseCommaSepArgs(int argc, const char* const* argv, const char* pattern, + std::vector& res) { + String filtersString; + if(parseOption(argc, argv, pattern, &filtersString)) { + // tokenize with "," as a separator, unless escaped with backslash + std::ostringstream s; + auto flush = [&s, &res]() { + auto string = s.str(); + if(string.size() > 0) { + res.push_back(string.c_str()); + } + s.str(""); + }; + + bool seenBackslash = false; + const char* current = filtersString.c_str(); + const char* end = current + strlen(current); + while(current != end) { + char character = *current++; + if(seenBackslash) { + seenBackslash = false; + if(character == ',') { + s.put(','); + continue; + } + s.put('\\'); + } + if(character == '\\') { + seenBackslash = true; + } else if(character == ',') { + flush(); + } else { + s.put(character); + } + } + + if(seenBackslash) { + s.put('\\'); + } + flush(); + return true; + } + return false; + } + + enum optionType + { + option_bool, + option_int + }; + + // parses an int/bool option from the command line + bool parseIntOption(int argc, const char* const* argv, const char* pattern, optionType type, + int& res) { + String parsedValue; + if(!parseOption(argc, argv, pattern, &parsedValue)) + return false; + + if(type == 0) { + // boolean + const char positive[][5] = {"1", "true", "on", "yes"}; // 5 - strlen("true") + 1 + const char negative[][6] = {"0", "false", "off", "no"}; // 6 - strlen("false") + 1 + + // if the value matches any of the positive/negative possibilities + for(unsigned i = 0; i < 4; i++) { + if(parsedValue.compare(positive[i], true) == 0) { + res = 1; //!OCLINT parameter reassignment + return true; + } + if(parsedValue.compare(negative[i], true) == 0) { + res = 0; //!OCLINT parameter reassignment + return true; + } + } + } else { + // integer + // TODO: change this to use std::stoi or something else! currently it uses undefined behavior - assumes '0' on failed parse... + int theInt = std::atoi(parsedValue.c_str()); // NOLINT + if(theInt != 0) { + res = theInt; //!OCLINT parameter reassignment + return true; + } + } + return false; + } +} // namespace + +Context::Context(int argc, const char* const* argv) + : p(new detail::ContextState) { + parseArgs(argc, argv, true); + if(argc) + p->binary_name = argv[0]; +} + +Context::~Context() { + if(g_cs == p) + g_cs = nullptr; + delete p; +} + +void Context::applyCommandLine(int argc, const char* const* argv) { + parseArgs(argc, argv); + if(argc) + p->binary_name = argv[0]; +} + +// parses args +void Context::parseArgs(int argc, const char* const* argv, bool withDefaults) { + using namespace detail; + + // clang-format off + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file=", p->filters[0]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sf=", p->filters[0]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file-exclude=",p->filters[1]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sfe=", p->filters[1]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite=", p->filters[2]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ts=", p->filters[2]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite-exclude=", p->filters[3]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tse=", p->filters[3]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case=", p->filters[4]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tc=", p->filters[4]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case-exclude=", p->filters[5]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tce=", p->filters[5]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase=", p->filters[6]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sc=", p->filters[6]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase-exclude=", p->filters[7]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sce=", p->filters[7]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "reporters=", p->filters[8]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "r=", p->filters[8]); + // clang-format on + + int intRes = 0; + String strRes; + +#define DOCTEST_PARSE_AS_BOOL_OR_FLAG(name, sname, var, default) \ + if(parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_bool, intRes) || \ + parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_bool, intRes)) \ + p->var = static_cast(intRes); \ + else if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name) || \ + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname)) \ + p->var = true; \ + else if(withDefaults) \ + p->var = default + +#define DOCTEST_PARSE_INT_OPTION(name, sname, var, default) \ + if(parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_int, intRes) || \ + parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_int, intRes)) \ + p->var = intRes; \ + else if(withDefaults) \ + p->var = default + +#define DOCTEST_PARSE_STR_OPTION(name, sname, var, default) \ + if(parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", &strRes, default) || \ + parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", &strRes, default) || \ + withDefaults) \ + p->var = strRes + + // clang-format off + DOCTEST_PARSE_STR_OPTION("out", "o", out, ""); + DOCTEST_PARSE_STR_OPTION("order-by", "ob", order_by, "file"); + DOCTEST_PARSE_INT_OPTION("rand-seed", "rs", rand_seed, 0); + + DOCTEST_PARSE_INT_OPTION("first", "f", first, 0); + DOCTEST_PARSE_INT_OPTION("last", "l", last, UINT_MAX); + + DOCTEST_PARSE_INT_OPTION("abort-after", "aa", abort_after, 0); + DOCTEST_PARSE_INT_OPTION("subcase-filter-levels", "scfl", subcase_filter_levels, INT_MAX); + + DOCTEST_PARSE_AS_BOOL_OR_FLAG("success", "s", success, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("case-sensitive", "cs", case_sensitive, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("exit", "e", exit, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("duration", "d", duration, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("minimal", "m", minimal, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("quiet", "q", quiet, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-throw", "nt", no_throw, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-exitcode", "ne", no_exitcode, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-run", "nr", no_run, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-intro", "ni", no_intro, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-version", "nv", no_version, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-colors", "nc", no_colors, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("force-colors", "fc", force_colors, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-breaks", "nb", no_breaks, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skip", "ns", no_skip, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("gnu-file-line", "gfl", gnu_file_line, !bool(DOCTEST_MSVC)); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-path-filenames", "npf", no_path_in_filenames, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-line-numbers", "nln", no_line_numbers, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-debug-output", "ndo", no_debug_output, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skipped-summary", "nss", no_skipped_summary, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-time-in-output", "ntio", no_time_in_output, false); + // clang-format on + + if(withDefaults) { + p->help = false; + p->version = false; + p->count = false; + p->list_test_cases = false; + p->list_test_suites = false; + p->list_reporters = false; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "help") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "h") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "?")) { + p->help = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "version") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "v")) { + p->version = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "count") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "c")) { + p->count = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-cases") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ltc")) { + p->list_test_cases = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-suites") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lts")) { + p->list_test_suites = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-reporters") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lr")) { + p->list_reporters = true; + p->exit = true; + } +} + +// allows the user to add procedurally to the filters from the command line +void Context::addFilter(const char* filter, const char* value) { setOption(filter, value); } + +// allows the user to clear all filters from the command line +void Context::clearFilters() { + for(auto& curr : p->filters) + curr.clear(); +} + +// allows the user to override procedurally the bool options from the command line +void Context::setOption(const char* option, bool value) { + setOption(option, value ? "true" : "false"); +} + +// allows the user to override procedurally the int options from the command line +void Context::setOption(const char* option, int value) { + setOption(option, toString(value).c_str()); + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) +} + +// allows the user to override procedurally the string options from the command line +void Context::setOption(const char* option, const char* value) { + auto argv = String("-") + option + "=" + value; + auto lvalue = argv.c_str(); + parseArgs(1, &lvalue); +} + +// users should query this in their main() and exit the program if true +bool Context::shouldExit() { return p->exit; } + +void Context::setAsDefaultForAssertsOutOfTestCases() { g_cs = p; } + +void Context::setAssertHandler(detail::assert_handler ah) { p->ah = ah; } + +void Context::setCout(std::ostream* out) { p->cout = out; } + +static class DiscardOStream : public std::ostream +{ +private: + class : public std::streambuf + { + private: + // allowing some buffering decreases the amount of calls to overflow + char buf[1024]; + + protected: + std::streamsize xsputn(const char_type*, std::streamsize count) override { return count; } + + int_type overflow(int_type ch) override { + setp(std::begin(buf), std::end(buf)); + return traits_type::not_eof(ch); + } + } discardBuf; + +public: + DiscardOStream() + : std::ostream(&discardBuf) {} +} discardOut; + +// the main function that does all the filtering and test running +int Context::run() { + using namespace detail; + + // save the old context state in case such was setup - for using asserts out of a testing context + auto old_cs = g_cs; + // this is the current contest + g_cs = p; + is_running_in_test = true; + + g_no_colors = p->no_colors; + p->resetRunData(); + + std::fstream fstr; + if(p->cout == nullptr) { + if(p->quiet) { + p->cout = &discardOut; + } else if(p->out.size()) { + // to a file if specified + fstr.open(p->out.c_str(), std::fstream::out); + p->cout = &fstr; + } else { + // stdout by default + p->cout = &std::cout; + } + } + + FatalConditionHandler::allocateAltStackMem(); + + auto cleanup_and_return = [&]() { + FatalConditionHandler::freeAltStackMem(); + + if(fstr.is_open()) + fstr.close(); + + // restore context + g_cs = old_cs; + is_running_in_test = false; + + // we have to free the reporters which were allocated when the run started + for(auto& curr : p->reporters_currently_used) + delete curr; + p->reporters_currently_used.clear(); + + if(p->numTestCasesFailed && !p->no_exitcode) + return EXIT_FAILURE; + return EXIT_SUCCESS; + }; + + // setup default reporter if none is given through the command line + if(p->filters[8].empty()) + p->filters[8].push_back("console"); + + // check to see if any of the registered reporters has been selected + for(auto& curr : getReporters()) { + if(matchesAny(curr.first.second.c_str(), p->filters[8], false, p->case_sensitive)) + p->reporters_currently_used.push_back(curr.second(*g_cs)); + } + + // TODO: check if there is nothing in reporters_currently_used + + // prepend all listeners + for(auto& curr : getListeners()) + p->reporters_currently_used.insert(p->reporters_currently_used.begin(), curr.second(*g_cs)); + +#ifdef DOCTEST_PLATFORM_WINDOWS + if(isDebuggerActive() && p->no_debug_output == false) + p->reporters_currently_used.push_back(new DebugOutputWindowReporter(*g_cs)); +#endif // DOCTEST_PLATFORM_WINDOWS + + // handle version, help and no_run + if(p->no_run || p->version || p->help || p->list_reporters) { + DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, QueryData()); + + return cleanup_and_return(); + } + + std::vector testArray; + for(auto& curr : getRegisteredTests()) + testArray.push_back(&curr); + p->numTestCases = testArray.size(); + + // sort the collected records + if(!testArray.empty()) { + if(p->order_by.compare("file", true) == 0) { + std::sort(testArray.begin(), testArray.end(), fileOrderComparator); + } else if(p->order_by.compare("suite", true) == 0) { + std::sort(testArray.begin(), testArray.end(), suiteOrderComparator); + } else if(p->order_by.compare("name", true) == 0) { + std::sort(testArray.begin(), testArray.end(), nameOrderComparator); + } else if(p->order_by.compare("rand", true) == 0) { + std::srand(p->rand_seed); + + // random_shuffle implementation + const auto first = &testArray[0]; + for(size_t i = testArray.size() - 1; i > 0; --i) { + int idxToSwap = std::rand() % (i + 1); // NOLINT + + const auto temp = first[i]; + + first[i] = first[idxToSwap]; + first[idxToSwap] = temp; + } + } else if(p->order_by.compare("none", true) == 0) { + // means no sorting - beneficial for death tests which call into the executable + // with a specific test case in mind - we don't want to slow down the startup times + } + } + + std::set testSuitesPassingFilt; + + bool query_mode = p->count || p->list_test_cases || p->list_test_suites; + std::vector queryResults; + + if(!query_mode) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_start, DOCTEST_EMPTY); + + // invoke the registered functions if they match the filter criteria (or just count them) + for(auto& curr : testArray) { + const auto& tc = *curr; + + bool skip_me = false; + if(tc.m_skip && !p->no_skip) + skip_me = true; + + if(!matchesAny(tc.m_file.c_str(), p->filters[0], true, p->case_sensitive)) + skip_me = true; + if(matchesAny(tc.m_file.c_str(), p->filters[1], false, p->case_sensitive)) + skip_me = true; + if(!matchesAny(tc.m_test_suite, p->filters[2], true, p->case_sensitive)) + skip_me = true; + if(matchesAny(tc.m_test_suite, p->filters[3], false, p->case_sensitive)) + skip_me = true; + if(!matchesAny(tc.m_name, p->filters[4], true, p->case_sensitive)) + skip_me = true; + if(matchesAny(tc.m_name, p->filters[5], false, p->case_sensitive)) + skip_me = true; + + if(!skip_me) + p->numTestCasesPassingFilters++; + + // skip the test if it is not in the execution range + if((p->last < p->numTestCasesPassingFilters && p->first <= p->last) || + (p->first > p->numTestCasesPassingFilters)) + skip_me = true; + + if(skip_me) { + if(!query_mode) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_skipped, tc); + continue; + } + + // do not execute the test if we are to only count the number of filter passing tests + if(p->count) + continue; + + // print the name of the test and don't execute it + if(p->list_test_cases) { + queryResults.push_back(&tc); + continue; + } + + // print the name of the test suite if not done already and don't execute it + if(p->list_test_suites) { + if((testSuitesPassingFilt.count(tc.m_test_suite) == 0) && tc.m_test_suite[0] != '\0') { + queryResults.push_back(&tc); + testSuitesPassingFilt.insert(tc.m_test_suite); + p->numTestSuitesPassingFilters++; + } + continue; + } + + // execute the test if it passes all the filtering + { + p->currentTest = &tc; + + p->failure_flags = TestCaseFailureReason::None; + p->seconds = 0; + + // reset atomic counters + p->numAssertsFailedCurrentTest_atomic = 0; + p->numAssertsCurrentTest_atomic = 0; + + p->subcasesPassed.clear(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_start, tc); + + p->timer.start(); + + bool run_test = true; + + do { + // reset some of the fields for subcases (except for the set of fully passed ones) + p->should_reenter = false; + p->subcasesCurrentMaxLevel = 0; + p->subcasesStack.clear(); + + p->shouldLogCurrentException = true; + + // reset stuff for logging with INFO() + p->stringifiedContexts.clear(); + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + try { +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +// MSVC 2015 diagnoses fatalConditionHandler as unused (because reset() is a static method) +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4101) // unreferenced local variable + FatalConditionHandler fatalConditionHandler; // Handle signals + // execute the test + tc.m_test(); + fatalConditionHandler.reset(); +DOCTEST_MSVC_SUPPRESS_WARNING_POP +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + } catch(const TestFailureException&) { + p->failure_flags |= TestCaseFailureReason::AssertFailure; + } catch(...) { + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception, + {translateActiveException(), false}); + p->failure_flags |= TestCaseFailureReason::Exception; + } +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + + // exit this loop if enough assertions have failed - even if there are more subcases + if(p->abort_after > 0 && + p->numAssertsFailed + p->numAssertsFailedCurrentTest_atomic >= p->abort_after) { + run_test = false; + p->failure_flags |= TestCaseFailureReason::TooManyFailedAsserts; + } + + if(p->should_reenter && run_test) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_reenter, tc); + if(!p->should_reenter) + run_test = false; + } while(run_test); + + p->finalizeTestCaseData(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs); + + p->currentTest = nullptr; + + // stop executing tests if enough assertions have failed + if(p->abort_after > 0 && p->numAssertsFailed >= p->abort_after) + break; + } + } + + if(!query_mode) { + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs); + } else { + QueryData qdata; + qdata.run_stats = g_cs; + qdata.data = queryResults.data(); + qdata.num_data = unsigned(queryResults.size()); + DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, qdata); + } + + return cleanup_and_return(); +} + +IReporter::~IReporter() = default; + +int IReporter::get_num_active_contexts() { return detail::g_infoContexts.size(); } +const IContextScope* const* IReporter::get_active_contexts() { + return get_num_active_contexts() ? &detail::g_infoContexts[0] : nullptr; +} + +int IReporter::get_num_stringified_contexts() { return detail::g_cs->stringifiedContexts.size(); } +const String* IReporter::get_stringified_contexts() { + return get_num_stringified_contexts() ? &detail::g_cs->stringifiedContexts[0] : nullptr; +} + +namespace detail { + void registerReporterImpl(const char* name, int priority, reporterCreatorFunc c, bool isReporter) { + if(isReporter) + getReporters().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c)); + else + getListeners().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c)); + } +} // namespace detail + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +#ifdef DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4007) // 'function' : must be 'attribute' - see issue #182 +int main(int argc, char** argv) { return doctest::Context(argc, argv).run(); } +DOCTEST_MSVC_SUPPRESS_WARNING_POP +#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_MSVC_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +DOCTEST_SUPPRESS_COMMON_WARNINGS_POP + +#endif // DOCTEST_LIBRARY_IMPLEMENTATION +#endif // DOCTEST_CONFIG_IMPLEMENT diff --git a/binding-tests/uuid.cpp b/binding-tests/uuid.cpp new file mode 100644 index 000000000..3c7979b2f --- /dev/null +++ b/binding-tests/uuid.cpp @@ -0,0 +1,7 @@ +#include "doctest.h" +#include "taskchampion.h" + +TEST_CASE("creating a UUID") { + StoragePtr *storage = storage_new_in_memory(); + storage_free(storage); +} diff --git a/lib/Cargo.toml b/lib/Cargo.toml new file mode 100644 index 000000000..9d941a6d7 --- /dev/null +++ b/lib/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "taskchampion-lib" +version = "0.1.0" +edition = "2021" +build = "build.rs" + +[lib] +name = "taskchampion" +crate-type = ["cdylib"] + +[dependencies] +taskchampion = { path = "../taskchampion" } + +[build-dependencies] +cbindgen = "0.20.0" diff --git a/lib/Makefile b/lib/Makefile new file mode 100644 index 000000000..d2dbe101b --- /dev/null +++ b/lib/Makefile @@ -0,0 +1,2 @@ +taskchampion.h: cbindgen.toml ../target/debug/libtaskchampion.so + cbindgen --config cbindgen.toml --crate taskchampion-lib --output $@ diff --git a/lib/build.rs b/lib/build.rs new file mode 100644 index 000000000..8d9db2f1f --- /dev/null +++ b/lib/build.rs @@ -0,0 +1,18 @@ +use cbindgen::*; + +use std::env; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + + Builder::new() + .with_crate(crate_dir) + .with_language(Language::C) + .with_config(Config { + cpp_compat: true, + ..Default::default() + }) + .generate() + .expect("Unable to generate bindings") + .write_to_file("taskchampion.h"); +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs new file mode 100644 index 000000000..30f61eb69 --- /dev/null +++ b/lib/src/lib.rs @@ -0,0 +1 @@ +pub mod storage; diff --git a/lib/src/storage.rs b/lib/src/storage.rs new file mode 100644 index 000000000..96950dcd9 --- /dev/null +++ b/lib/src/storage.rs @@ -0,0 +1,16 @@ +use taskchampion::{storage::Storage, StorageConfig}; + +pub struct StoragePtr(Box); + +#[no_mangle] +pub extern "C" fn storage_new_in_memory() -> *mut StoragePtr { + // TODO: this is a box containing a fat pointer + Box::into_raw(Box::new(StoragePtr( + StorageConfig::InMemory.into_storage().unwrap(), + ))) +} + +#[no_mangle] +pub extern "C" fn storage_free(storage: *mut StoragePtr) { + drop(unsafe { Box::from_raw(storage) }); +} diff --git a/lib/taskchampion.h b/lib/taskchampion.h new file mode 100644 index 000000000..dc8631325 --- /dev/null +++ b/lib/taskchampion.h @@ -0,0 +1,15 @@ +#include +#include +#include +#include +#include + +struct StoragePtr; + +extern "C" { + +StoragePtr *storage_new_in_memory(); + +void storage_free(StoragePtr *storage); + +} // extern "C" From ce56127bbfa3fe2b47942b97bd059b8deb629998 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 23 Jan 2022 17:24:54 +0000 Subject: [PATCH 02/95] create / free replicas, plus error handling --- Cargo.lock | 5 ++- binding-tests/Makefile | 2 +- binding-tests/replica.cpp | 12 +++++ binding-tests/uuid.cpp | 7 --- lib/Cargo.toml | 1 + lib/src/lib.rs | 2 +- lib/src/replica.rs | 92 +++++++++++++++++++++++++++++++++++++++ lib/src/storage.rs | 16 ------- lib/taskchampion.h | 25 +++++++++-- 9 files changed, 132 insertions(+), 30 deletions(-) create mode 100644 binding-tests/replica.cpp delete mode 100644 binding-tests/uuid.cpp create mode 100644 lib/src/replica.rs delete mode 100644 lib/src/storage.rs diff --git a/Cargo.lock b/Cargo.lock index caa3684ab..91cc866d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1617,9 +1617,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.97" +version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" +checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9" [[package]] name = "libgit2-sys" @@ -3032,6 +3032,7 @@ name = "taskchampion-lib" version = "0.1.0" dependencies = [ "cbindgen", + "libc", "taskchampion", ] diff --git a/binding-tests/Makefile b/binding-tests/Makefile index e5f09d787..3599821f4 100644 --- a/binding-tests/Makefile +++ b/binding-tests/Makefile @@ -3,7 +3,7 @@ INC=-I ../lib LIB=-L ../target/debug RPATH=-Wl,-rpath,../target/debug -TESTS = uuid.cpp +TESTS = replica.cpp .PHONY: all test diff --git a/binding-tests/replica.cpp b/binding-tests/replica.cpp new file mode 100644 index 000000000..c8e11cedc --- /dev/null +++ b/binding-tests/replica.cpp @@ -0,0 +1,12 @@ +#include +#include "doctest.h" +#include "taskchampion.h" + +TEST_CASE("creating an in-memory Replica does not crash") { + Replica *rep = tc_replica_new(NULL); + CHECK(tc_replica_error(rep) == NULL); + uhoh(rep); + REQUIRE(tc_replica_error(rep) != NULL); + CHECK(strcmp(tc_replica_error(rep), "uhoh!") == 0); + tc_replica_free(rep); +} diff --git a/binding-tests/uuid.cpp b/binding-tests/uuid.cpp deleted file mode 100644 index 3c7979b2f..000000000 --- a/binding-tests/uuid.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include "doctest.h" -#include "taskchampion.h" - -TEST_CASE("creating a UUID") { - StoragePtr *storage = storage_new_in_memory(); - storage_free(storage); -} diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 9d941a6d7..956c5cbef 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -9,6 +9,7 @@ name = "taskchampion" crate-type = ["cdylib"] [dependencies] +libc = "0.2.113" taskchampion = { path = "../taskchampion" } [build-dependencies] diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 30f61eb69..78004cf88 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1 +1 @@ -pub mod storage; +pub mod replica; diff --git a/lib/src/replica.rs b/lib/src/replica.rs new file mode 100644 index 000000000..81656519b --- /dev/null +++ b/lib/src/replica.rs @@ -0,0 +1,92 @@ +use libc::c_char; +use std::ffi::{CStr, CString, OsStr}; +use std::path::PathBuf; +use taskchampion::Replica as TCReplica; +use taskchampion::StorageConfig; + +// TODO: unix-only +use std::os::unix::ffi::OsStrExt; + +/// A replica represents an instance of a user's task data, providing an easy interface +/// for querying and modifying that data. +pub struct Replica { + inner: TCReplica, + error: Option, +} + +/// Create a new Replica. +/// +/// If path is NULL, then an in-memory replica is created. Otherwise, path is the path to the +/// on-disk storage for this replica. The path argument is no longer referenced after return. +/// +/// Returns NULL on error; see tc_replica_error. +/// +/// Replicas are not threadsafe. +#[no_mangle] +pub extern "C" fn tc_replica_new<'a>(path: *const c_char) -> *mut Replica { + let storage_res = if path.is_null() { + StorageConfig::InMemory.into_storage() + } else { + let path: &'a [u8] = unsafe { CStr::from_ptr(path) }.to_bytes(); + let path: &OsStr = OsStr::from_bytes(path); + let path: PathBuf = path.to_os_string().into(); + StorageConfig::OnDisk { taskdb_dir: path }.into_storage() + }; + + let storage = match storage_res { + Ok(storage) => storage, + // TODO: report errors somehow + Err(_) => return std::ptr::null_mut(), + }; + + Box::into_raw(Box::new(Replica { + inner: TCReplica::new(storage), + error: None, + })) +} + +/// Utility function to safely convert *mut Replica into &mut Replica +fn rep_ref(rep: *mut Replica) -> &'static mut Replica { + debug_assert!(!rep.is_null()); + unsafe { &mut *rep } +} + +fn wrap(rep: *mut Replica, f: F, err_value: T) -> T +where + F: FnOnce(&mut Replica) -> Result, +{ + debug_assert!(!rep.is_null()); + let rep = unsafe { &mut *rep }; + match f(rep) { + Ok(v) => v, + Err(e) => { + rep.error = Some(CString::new(e.as_bytes()).unwrap()); + err_value + } + } +} + +/// temporary (testing errors) +#[no_mangle] +pub extern "C" fn uhoh<'a>(rep: *mut Replica) -> u32 { + wrap(rep, |rep| Err("uhoh!"), 0) +} + +/// Get the latest error for a replica, or NULL if the last operation succeeded. +/// +/// The returned string is valid until the next replica operation. +#[no_mangle] +pub extern "C" fn tc_replica_error<'a>(rep: *mut Replica) -> *const c_char { + let rep: &'a Replica = rep_ref(rep); + if let Some(ref e) = rep.error { + e.as_ptr() + } else { + std::ptr::null() + } +} + +/// Free a Replica. +#[no_mangle] +pub extern "C" fn tc_replica_free(rep: *mut Replica) { + drop(unsafe { Box::from_raw(rep) }); +} diff --git a/lib/src/storage.rs b/lib/src/storage.rs deleted file mode 100644 index 96950dcd9..000000000 --- a/lib/src/storage.rs +++ /dev/null @@ -1,16 +0,0 @@ -use taskchampion::{storage::Storage, StorageConfig}; - -pub struct StoragePtr(Box); - -#[no_mangle] -pub extern "C" fn storage_new_in_memory() -> *mut StoragePtr { - // TODO: this is a box containing a fat pointer - Box::into_raw(Box::new(StoragePtr( - StorageConfig::InMemory.into_storage().unwrap(), - ))) -} - -#[no_mangle] -pub extern "C" fn storage_free(storage: *mut StoragePtr) { - drop(unsafe { Box::from_raw(storage) }); -} diff --git a/lib/taskchampion.h b/lib/taskchampion.h index dc8631325..ac28be560 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -4,12 +4,31 @@ #include #include -struct StoragePtr; +/// A replica represents an instance of a user's task data, providing an easy interface +/// for querying and modifying that data. +struct Replica; extern "C" { -StoragePtr *storage_new_in_memory(); +/// Create a new Replica. +/// +/// If path is NULL, then an in-memory replica is created. Otherwise, path is the path to the +/// on-disk storage for this replica. The path argument is no longer referenced after return. +/// +/// Returns NULL on error; see tc_replica_error. +/// +/// Replicas are not threadsafe. +Replica *tc_replica_new(const char *path); -void storage_free(StoragePtr *storage); +/// temporary (testing errors) +uint32_t uhoh(Replica *rep); + +/// Get the latest error for a replica, or NULL if the last operation succeeded. +/// +/// The returned string is valid until the next replica operation. +const char *tc_replica_error(Replica *rep); + +/// Free a Replica. +void tc_replica_free(Replica *rep); } // extern "C" From e590dc7c9894f0fe642d1f4bd67f63b6c6137028 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 23 Jan 2022 17:34:59 +0000 Subject: [PATCH 03/95] add tc_replica_undo --- Cargo.lock | 1 + binding-tests/replica.cpp | 12 +++++++++--- lib/Cargo.toml | 1 + lib/src/replica.rs | 26 +++++++++++++++++--------- lib/taskchampion.h | 7 +++++-- 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 91cc866d9..b2f438354 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3031,6 +3031,7 @@ dependencies = [ name = "taskchampion-lib" version = "0.1.0" dependencies = [ + "anyhow", "cbindgen", "libc", "taskchampion", diff --git a/binding-tests/replica.cpp b/binding-tests/replica.cpp index c8e11cedc..6400b8002 100644 --- a/binding-tests/replica.cpp +++ b/binding-tests/replica.cpp @@ -5,8 +5,14 @@ TEST_CASE("creating an in-memory Replica does not crash") { Replica *rep = tc_replica_new(NULL); CHECK(tc_replica_error(rep) == NULL); - uhoh(rep); - REQUIRE(tc_replica_error(rep) != NULL); - CHECK(strcmp(tc_replica_error(rep), "uhoh!") == 0); + tc_replica_free(rep); +} + +TEST_CASE("undo on an empty in-memory Replica does nothing") { + Replica *rep = tc_replica_new(NULL); + CHECK(tc_replica_error(rep) == NULL); + int rv = tc_replica_undo(rep); + CHECK(rv == 0); + CHECK(tc_replica_error(rep) == NULL); tc_replica_free(rep); } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 956c5cbef..5d9b44fc6 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["cdylib"] [dependencies] libc = "0.2.113" taskchampion = { path = "../taskchampion" } +anyhow = "1.0" [build-dependencies] cbindgen = "0.20.0" diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 81656519b..98b38e389 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -10,6 +10,7 @@ use std::os::unix::ffi::OsStrExt; /// A replica represents an instance of a user's task data, providing an easy interface /// for querying and modifying that data. pub struct Replica { + // TODO: make this an option so that it can be take()n when holding a mut ref inner: TCReplica, error: Option, } @@ -51,25 +52,32 @@ fn rep_ref(rep: *mut Replica) -> &'static mut Replica { unsafe { &mut *rep } } -fn wrap(rep: *mut Replica, f: F, err_value: T) -> T +fn wrap<'a, T, F>(rep: *mut Replica, f: F, err_value: T) -> T where - F: FnOnce(&mut Replica) -> Result, + F: FnOnce(&mut TCReplica) -> anyhow::Result, { - debug_assert!(!rep.is_null()); - let rep = unsafe { &mut *rep }; - match f(rep) { + let rep: &'a mut Replica = rep_ref(rep); + match f(&mut rep.inner) { Ok(v) => v, Err(e) => { - rep.error = Some(CString::new(e.as_bytes()).unwrap()); + let error = e.to_string(); + let error = match CString::new(error.as_bytes()) { + Ok(e) => e, + Err(_) => CString::new("(invalid error message)".as_bytes()).unwrap(), + }; + rep.error = Some(error); err_value } } } -/// temporary (testing errors) +/// Undo local operations until the most recent UndoPoint. +/// +/// Returns -1 on error, 0 if there are no local operations to undo, and 1 if operations were +/// undone. #[no_mangle] -pub extern "C" fn uhoh<'a>(rep: *mut Replica) -> u32 { - wrap(rep, |rep| Err("uhoh!"), 0) +pub extern "C" fn tc_replica_undo<'a>(rep: *mut Replica) -> i32 { + wrap(rep, |rep| Ok(if rep.undo()? { 1 } else { 0 }), -1) } /// Get the latest error for a replica, or NULL if the last operation succeeded. diff --git a/lib/taskchampion.h b/lib/taskchampion.h index ac28be560..081dd09cc 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -20,8 +20,11 @@ extern "C" { /// Replicas are not threadsafe. Replica *tc_replica_new(const char *path); -/// temporary (testing errors) -uint32_t uhoh(Replica *rep); +/// Undo local operations until the most recent UndoPoint. +/// +/// Returns -1 on error, 0 if there are no local operations to undo, and 1 if operations were +/// undone. +int32_t tc_replica_undo(Replica *rep); /// Get the latest error for a replica, or NULL if the last operation succeeded. /// From 46e08bc04006117819cd7b5030327e6d52240f68 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 23 Jan 2022 19:45:39 +0000 Subject: [PATCH 04/95] add some UUID support --- Cargo.lock | 1 + binding-tests/Makefile | 2 +- binding-tests/uuid.cpp | 38 +++++++++++++++++++++++++ lib/Cargo.toml | 1 + lib/build.rs | 10 +++++++ lib/src/lib.rs | 1 + lib/src/uuid.rs | 63 ++++++++++++++++++++++++++++++++++++++++++ lib/taskchampion.h | 21 ++++++++++++++ 8 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 binding-tests/uuid.cpp create mode 100644 lib/src/uuid.rs diff --git a/Cargo.lock b/Cargo.lock index b2f438354..4f577c437 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3035,6 +3035,7 @@ dependencies = [ "cbindgen", "libc", "taskchampion", + "uuid", ] [[package]] diff --git a/binding-tests/Makefile b/binding-tests/Makefile index 3599821f4..620befab5 100644 --- a/binding-tests/Makefile +++ b/binding-tests/Makefile @@ -3,7 +3,7 @@ INC=-I ../lib LIB=-L ../target/debug RPATH=-Wl,-rpath,../target/debug -TESTS = replica.cpp +TESTS = replica.cpp uuid.cpp .PHONY: all test diff --git a/binding-tests/uuid.cpp b/binding-tests/uuid.cpp new file mode 100644 index 000000000..949d081f0 --- /dev/null +++ b/binding-tests/uuid.cpp @@ -0,0 +1,38 @@ +#include +#include "doctest.h" +#include "taskchampion.h" + +TEST_CASE("creating UUIDs does not crash") { + Uuid u1 = tc_uuid_new_v4(); + Uuid u2 = tc_uuid_nil(); +} + +TEST_CASE("converting UUIDs to string works") { + Uuid u2 = tc_uuid_nil(); + REQUIRE(TC_UUID_STRING_BYTES == 36); + + char u2str[TC_UUID_STRING_BYTES]; + tc_uuid_to_str(u2, u2str); + CHECK(strncmp(u2str, "00000000-0000-0000-0000-000000000000", TC_UUID_STRING_BYTES) == 0); +} + +TEST_CASE("converting UUIDs from string works") { + Uuid u; + char ustr[TC_UUID_STRING_BYTES] = "fdc314b7-f938-4845-b8d1-95716e4eb762"; + CHECK(tc_uuid_from_str(ustr, &u)); + CHECK(u._0[0] == 0xfd); + // .. if these two are correct, probably it worked :) + CHECK(u._0[15] == 0x62); +} + +TEST_CASE("converting invalid UUIDs from string fails as expected") { + Uuid u; + char ustr[TC_UUID_STRING_BYTES] = "not-a-valid-uuid"; + CHECK(!tc_uuid_from_str(ustr, &u)); +} + +TEST_CASE("converting invalid UTF-8 UUIDs from string fails as expected") { + Uuid u; + char ustr[TC_UUID_STRING_BYTES] = "\xf0\x28\x8c\xbc"; + CHECK(!tc_uuid_from_str(ustr, &u)); +} diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 5d9b44fc6..b01d1bcbd 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["cdylib"] [dependencies] libc = "0.2.113" taskchampion = { path = "../taskchampion" } +uuid = { version = "^0.8.2", features = ["serde", "v4"] } anyhow = "1.0" [build-dependencies] diff --git a/lib/build.rs b/lib/build.rs index 8d9db2f1f..74dfe0fc8 100644 --- a/lib/build.rs +++ b/lib/build.rs @@ -10,6 +10,16 @@ fn main() { .with_language(Language::C) .with_config(Config { cpp_compat: true, + export: ExportConfig { + item_types: vec![ + ItemType::Structs, + ItemType::Globals, + ItemType::Functions, + ItemType::Constants, + ItemType::OpaqueItems, + ], + ..Default::default() + }, ..Default::default() }) .generate() diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 78004cf88..98b7c0124 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1 +1,2 @@ pub mod replica; +pub mod uuid; diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs new file mode 100644 index 000000000..5fbbe8d7a --- /dev/null +++ b/lib/src/uuid.rs @@ -0,0 +1,63 @@ +use libc; +use taskchampion::Uuid as TcUuid; + +/// Uuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. +#[repr(C)] +pub struct Uuid([u8; 16]); + +impl From for Uuid { + fn from(uuid: TcUuid) -> Uuid { + // TODO: can we avoid clone here? + Uuid(uuid.as_bytes().clone()) + } +} + +impl From for TcUuid { + fn from(uuid: Uuid) -> TcUuid { + TcUuid::from_bytes(uuid.0) + } +} + +/// Create a new, randomly-generated UUID. +#[no_mangle] +pub extern "C" fn tc_uuid_new_v4() -> Uuid { + TcUuid::new_v4().into() +} + +/// Create a new UUID with the nil value. +#[no_mangle] +pub extern "C" fn tc_uuid_nil() -> Uuid { + TcUuid::nil().into() +} + +/// Length, in bytes, of a C string containing a Uuid. +#[no_mangle] +pub static TC_UUID_STRING_BYTES: usize = ::uuid::adapter::Hyphenated::LENGTH; + +/// Write the string representation of a Uuid into the given buffer, which must be +/// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. +#[no_mangle] +pub extern "C" fn tc_uuid_to_str<'a>(uuid: Uuid, out: *mut libc::c_char) { + debug_assert!(!out.is_null()); + let buf: &'a mut [u8] = unsafe { + std::slice::from_raw_parts_mut(out as *mut u8, ::uuid::adapter::Hyphenated::LENGTH) + }; + let uuid: TcUuid = uuid.into(); + uuid.to_hyphenated().encode_lower(buf); +} + +/// Parse the given value as a UUID. The value must be exactly TC_UUID_STRING_BYTES long. Returns +/// false on failure. +#[no_mangle] +pub extern "C" fn tc_uuid_from_str<'a>(val: *const libc::c_char, out: *mut Uuid) -> bool { + debug_assert!(!val.is_null()); + debug_assert!(!out.is_null()); + let slice = unsafe { std::slice::from_raw_parts(val as *const u8, TC_UUID_STRING_BYTES) }; + if let Ok(s) = std::str::from_utf8(slice) { + if let Ok(u) = TcUuid::parse_str(s) { + unsafe { *out = u.into() }; + return true; + } + } + false +} diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 081dd09cc..e5ad1de99 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -8,8 +8,15 @@ /// for querying and modifying that data. struct Replica; +/// Uuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. +struct Uuid { + uint8_t _0[16]; +}; + extern "C" { +extern const uintptr_t TC_UUID_STRING_BYTES; + /// Create a new Replica. /// /// If path is NULL, then an in-memory replica is created. Otherwise, path is the path to the @@ -34,4 +41,18 @@ const char *tc_replica_error(Replica *rep); /// Free a Replica. void tc_replica_free(Replica *rep); +/// Create a new, randomly-generated UUID. +Uuid tc_uuid_new_v4(); + +/// Create a new UUID with the nil value. +Uuid tc_uuid_nil(); + +/// Write the string representation of a Uuid into the given buffer, which must be +/// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. +void tc_uuid_to_str(Uuid uuid, char *out); + +/// Parse the given value as a UUID. The value must be exactly TC_UUID_STRING_BYTES long. Returns +/// false on failure. +bool tc_uuid_from_str(const char *val, Uuid *out); + } // extern "C" From 821118106aae65e3ad8676ea2303ae8cbab7e658 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 23 Jan 2022 19:57:42 +0000 Subject: [PATCH 05/95] add TC prefix to types, too --- binding-tests/replica.cpp | 8 ++++---- binding-tests/uuid.cpp | 18 ++++++++--------- lib/build.rs | 10 ---------- lib/src/replica.rs | 37 +++++++++++++++++------------------ lib/src/uuid.rs | 41 +++++++++++++++++++++------------------ lib/taskchampion.h | 32 +++++++++++++++--------------- 6 files changed, 69 insertions(+), 77 deletions(-) diff --git a/binding-tests/replica.cpp b/binding-tests/replica.cpp index 6400b8002..4e1053d8c 100644 --- a/binding-tests/replica.cpp +++ b/binding-tests/replica.cpp @@ -2,14 +2,14 @@ #include "doctest.h" #include "taskchampion.h" -TEST_CASE("creating an in-memory Replica does not crash") { - Replica *rep = tc_replica_new(NULL); +TEST_CASE("creating an in-memory TCReplica does not crash") { + TCReplica *rep = tc_replica_new(NULL); CHECK(tc_replica_error(rep) == NULL); tc_replica_free(rep); } -TEST_CASE("undo on an empty in-memory Replica does nothing") { - Replica *rep = tc_replica_new(NULL); +TEST_CASE("undo on an empty in-memory TCReplica does nothing") { + TCReplica *rep = tc_replica_new(NULL); CHECK(tc_replica_error(rep) == NULL); int rv = tc_replica_undo(rep); CHECK(rv == 0); diff --git a/binding-tests/uuid.cpp b/binding-tests/uuid.cpp index 949d081f0..aba07d867 100644 --- a/binding-tests/uuid.cpp +++ b/binding-tests/uuid.cpp @@ -3,12 +3,12 @@ #include "taskchampion.h" TEST_CASE("creating UUIDs does not crash") { - Uuid u1 = tc_uuid_new_v4(); - Uuid u2 = tc_uuid_nil(); + TCUuid u1 = tc_uuid_new_v4(); + TCUuid u2 = tc_uuid_nil(); } TEST_CASE("converting UUIDs to string works") { - Uuid u2 = tc_uuid_nil(); + TCUuid u2 = tc_uuid_nil(); REQUIRE(TC_UUID_STRING_BYTES == 36); char u2str[TC_UUID_STRING_BYTES]; @@ -17,22 +17,22 @@ TEST_CASE("converting UUIDs to string works") { } TEST_CASE("converting UUIDs from string works") { - Uuid u; + TCUuid u; char ustr[TC_UUID_STRING_BYTES] = "fdc314b7-f938-4845-b8d1-95716e4eb762"; CHECK(tc_uuid_from_str(ustr, &u)); - CHECK(u._0[0] == 0xfd); - // .. if these two are correct, probably it worked :) - CHECK(u._0[15] == 0x62); + CHECK(u.bytes[0] == 0xfd); + // .. if these two bytes are correct, then it probably worked :) + CHECK(u.bytes[15] == 0x62); } TEST_CASE("converting invalid UUIDs from string fails as expected") { - Uuid u; + TCUuid u; char ustr[TC_UUID_STRING_BYTES] = "not-a-valid-uuid"; CHECK(!tc_uuid_from_str(ustr, &u)); } TEST_CASE("converting invalid UTF-8 UUIDs from string fails as expected") { - Uuid u; + TCUuid u; char ustr[TC_UUID_STRING_BYTES] = "\xf0\x28\x8c\xbc"; CHECK(!tc_uuid_from_str(ustr, &u)); } diff --git a/lib/build.rs b/lib/build.rs index 74dfe0fc8..8d9db2f1f 100644 --- a/lib/build.rs +++ b/lib/build.rs @@ -10,16 +10,6 @@ fn main() { .with_language(Language::C) .with_config(Config { cpp_compat: true, - export: ExportConfig { - item_types: vec![ - ItemType::Structs, - ItemType::Globals, - ItemType::Functions, - ItemType::Constants, - ItemType::OpaqueItems, - ], - ..Default::default() - }, ..Default::default() }) .generate() diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 98b38e389..44066507e 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -1,30 +1,29 @@ use libc::c_char; use std::ffi::{CStr, CString, OsStr}; use std::path::PathBuf; -use taskchampion::Replica as TCReplica; -use taskchampion::StorageConfig; +use taskchampion::{Replica, StorageConfig}; // TODO: unix-only use std::os::unix::ffi::OsStrExt; /// A replica represents an instance of a user's task data, providing an easy interface /// for querying and modifying that data. -pub struct Replica { +pub struct TCReplica { // TODO: make this an option so that it can be take()n when holding a mut ref - inner: TCReplica, + inner: Replica, error: Option, } -/// Create a new Replica. +/// Create a new TCReplica. /// /// If path is NULL, then an in-memory replica is created. Otherwise, path is the path to the /// on-disk storage for this replica. The path argument is no longer referenced after return. /// /// Returns NULL on error; see tc_replica_error. /// -/// Replicas are not threadsafe. +/// TCReplicas are not threadsafe. #[no_mangle] -pub extern "C" fn tc_replica_new<'a>(path: *const c_char) -> *mut Replica { +pub extern "C" fn tc_replica_new<'a>(path: *const c_char) -> *mut TCReplica { let storage_res = if path.is_null() { StorageConfig::InMemory.into_storage() } else { @@ -40,23 +39,23 @@ pub extern "C" fn tc_replica_new<'a>(path: *const c_char) -> *mut Replica { Err(_) => return std::ptr::null_mut(), }; - Box::into_raw(Box::new(Replica { - inner: TCReplica::new(storage), + Box::into_raw(Box::new(TCReplica { + inner: Replica::new(storage), error: None, })) } -/// Utility function to safely convert *mut Replica into &mut Replica -fn rep_ref(rep: *mut Replica) -> &'static mut Replica { +/// Utility function to safely convert *mut TCReplica into &mut TCReplica +fn rep_ref(rep: *mut TCReplica) -> &'static mut TCReplica { debug_assert!(!rep.is_null()); unsafe { &mut *rep } } -fn wrap<'a, T, F>(rep: *mut Replica, f: F, err_value: T) -> T +fn wrap<'a, T, F>(rep: *mut TCReplica, f: F, err_value: T) -> T where - F: FnOnce(&mut TCReplica) -> anyhow::Result, + F: FnOnce(&mut Replica) -> anyhow::Result, { - let rep: &'a mut Replica = rep_ref(rep); + let rep: &'a mut TCReplica = rep_ref(rep); match f(&mut rep.inner) { Ok(v) => v, Err(e) => { @@ -76,7 +75,7 @@ where /// Returns -1 on error, 0 if there are no local operations to undo, and 1 if operations were /// undone. #[no_mangle] -pub extern "C" fn tc_replica_undo<'a>(rep: *mut Replica) -> i32 { +pub extern "C" fn tc_replica_undo<'a>(rep: *mut TCReplica) -> i32 { wrap(rep, |rep| Ok(if rep.undo()? { 1 } else { 0 }), -1) } @@ -84,8 +83,8 @@ pub extern "C" fn tc_replica_undo<'a>(rep: *mut Replica) -> i32 { /// /// The returned string is valid until the next replica operation. #[no_mangle] -pub extern "C" fn tc_replica_error<'a>(rep: *mut Replica) -> *const c_char { - let rep: &'a Replica = rep_ref(rep); +pub extern "C" fn tc_replica_error<'a>(rep: *mut TCReplica) -> *const c_char { + let rep: &'a TCReplica = rep_ref(rep); if let Some(ref e) = rep.error { e.as_ptr() } else { @@ -93,8 +92,8 @@ pub extern "C" fn tc_replica_error<'a>(rep: *mut Replica) -> *const c_char { } } -/// Free a Replica. +/// Free a TCReplica. #[no_mangle] -pub extern "C" fn tc_replica_free(rep: *mut Replica) { +pub extern "C" fn tc_replica_free(rep: *mut TCReplica) { drop(unsafe { Box::from_raw(rep) }); } diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index 5fbbe8d7a..8ce407afc 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -1,60 +1,63 @@ use libc; -use taskchampion::Uuid as TcUuid; +use taskchampion::Uuid; -/// Uuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. +/// TCUuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. +/// Uuids are typically treated as opaque, but the bytes are available in big-endian format. +/// +/// cbindgen:field-names=[bytes] #[repr(C)] -pub struct Uuid([u8; 16]); +pub struct TCUuid([u8; 16]); -impl From for Uuid { - fn from(uuid: TcUuid) -> Uuid { +impl From for TCUuid { + fn from(uuid: Uuid) -> TCUuid { // TODO: can we avoid clone here? - Uuid(uuid.as_bytes().clone()) + TCUuid(uuid.as_bytes().clone()) } } -impl From for TcUuid { - fn from(uuid: Uuid) -> TcUuid { - TcUuid::from_bytes(uuid.0) +impl From for Uuid { + fn from(uuid: TCUuid) -> Uuid { + Uuid::from_bytes(uuid.0) } } /// Create a new, randomly-generated UUID. #[no_mangle] -pub extern "C" fn tc_uuid_new_v4() -> Uuid { - TcUuid::new_v4().into() +pub extern "C" fn tc_uuid_new_v4() -> TCUuid { + Uuid::new_v4().into() } /// Create a new UUID with the nil value. #[no_mangle] -pub extern "C" fn tc_uuid_nil() -> Uuid { - TcUuid::nil().into() +pub extern "C" fn tc_uuid_nil() -> TCUuid { + Uuid::nil().into() } -/// Length, in bytes, of a C string containing a Uuid. +/// Length, in bytes, of a C string containing a TCUuid. #[no_mangle] pub static TC_UUID_STRING_BYTES: usize = ::uuid::adapter::Hyphenated::LENGTH; -/// Write the string representation of a Uuid into the given buffer, which must be +/// Write the string representation of a TCUuid into the given buffer, which must be /// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. #[no_mangle] -pub extern "C" fn tc_uuid_to_str<'a>(uuid: Uuid, out: *mut libc::c_char) { +pub extern "C" fn tc_uuid_to_str<'a>(uuid: TCUuid, out: *mut libc::c_char) { debug_assert!(!out.is_null()); let buf: &'a mut [u8] = unsafe { std::slice::from_raw_parts_mut(out as *mut u8, ::uuid::adapter::Hyphenated::LENGTH) }; - let uuid: TcUuid = uuid.into(); + let uuid: Uuid = uuid.into(); uuid.to_hyphenated().encode_lower(buf); } /// Parse the given value as a UUID. The value must be exactly TC_UUID_STRING_BYTES long. Returns /// false on failure. #[no_mangle] -pub extern "C" fn tc_uuid_from_str<'a>(val: *const libc::c_char, out: *mut Uuid) -> bool { +pub extern "C" fn tc_uuid_from_str<'a>(val: *const libc::c_char, out: *mut TCUuid) -> bool { debug_assert!(!val.is_null()); debug_assert!(!out.is_null()); let slice = unsafe { std::slice::from_raw_parts(val as *const u8, TC_UUID_STRING_BYTES) }; if let Ok(s) = std::str::from_utf8(slice) { - if let Ok(u) = TcUuid::parse_str(s) { + if let Ok(u) = Uuid::parse_str(s) { unsafe { *out = u.into() }; return true; } diff --git a/lib/taskchampion.h b/lib/taskchampion.h index e5ad1de99..c018143d5 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -6,53 +6,53 @@ /// A replica represents an instance of a user's task data, providing an easy interface /// for querying and modifying that data. -struct Replica; +struct TCReplica; -/// Uuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. -struct Uuid { - uint8_t _0[16]; +/// TCUuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. +struct TCUuid { + uint8_t bytes[16]; }; extern "C" { extern const uintptr_t TC_UUID_STRING_BYTES; -/// Create a new Replica. +/// Create a new TCReplica. /// /// If path is NULL, then an in-memory replica is created. Otherwise, path is the path to the /// on-disk storage for this replica. The path argument is no longer referenced after return. /// /// Returns NULL on error; see tc_replica_error. /// -/// Replicas are not threadsafe. -Replica *tc_replica_new(const char *path); +/// TCReplicas are not threadsafe. +TCReplica *tc_replica_new(const char *path); /// Undo local operations until the most recent UndoPoint. /// /// Returns -1 on error, 0 if there are no local operations to undo, and 1 if operations were /// undone. -int32_t tc_replica_undo(Replica *rep); +int32_t tc_replica_undo(TCReplica *rep); /// Get the latest error for a replica, or NULL if the last operation succeeded. /// /// The returned string is valid until the next replica operation. -const char *tc_replica_error(Replica *rep); +const char *tc_replica_error(TCReplica *rep); -/// Free a Replica. -void tc_replica_free(Replica *rep); +/// Free a TCReplica. +void tc_replica_free(TCReplica *rep); /// Create a new, randomly-generated UUID. -Uuid tc_uuid_new_v4(); +TCUuid tc_uuid_new_v4(); /// Create a new UUID with the nil value. -Uuid tc_uuid_nil(); +TCUuid tc_uuid_nil(); -/// Write the string representation of a Uuid into the given buffer, which must be +/// Write the string representation of a TCUuid into the given buffer, which must be /// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. -void tc_uuid_to_str(Uuid uuid, char *out); +void tc_uuid_to_str(TCUuid uuid, char *out); /// Parse the given value as a UUID. The value must be exactly TC_UUID_STRING_BYTES long. Returns /// false on failure. -bool tc_uuid_from_str(const char *val, Uuid *out); +bool tc_uuid_from_str(const char *val, TCUuid *out); } // extern "C" From bb722325fe4a23aa24093fc25c0646a10586ad97 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 23 Jan 2022 22:45:57 +0000 Subject: [PATCH 06/95] more task functionality --- binding-tests/Makefile | 2 +- binding-tests/task.cpp | 35 ++++++++++++ lib/build.rs | 5 ++ lib/src/lib.rs | 3 ++ lib/src/replica.rs | 86 +++++++++++++++++++++-------- lib/src/status.rs | 36 +++++++++++++ lib/src/string.rs | 48 +++++++++++++++++ lib/src/task.rs | 96 +++++++++++++++++++++++++++++++++ lib/taskchampion.h | 45 ++++++++++++++++ taskchampion/src/task/status.rs | 1 + 10 files changed, 333 insertions(+), 24 deletions(-) create mode 100644 binding-tests/task.cpp create mode 100644 lib/src/status.rs create mode 100644 lib/src/string.rs create mode 100644 lib/src/task.rs diff --git a/binding-tests/Makefile b/binding-tests/Makefile index 620befab5..121d942cc 100644 --- a/binding-tests/Makefile +++ b/binding-tests/Makefile @@ -3,7 +3,7 @@ INC=-I ../lib LIB=-L ../target/debug RPATH=-Wl,-rpath,../target/debug -TESTS = replica.cpp uuid.cpp +TESTS = replica.cpp uuid.cpp task.cpp .PHONY: all test diff --git a/binding-tests/task.cpp b/binding-tests/task.cpp new file mode 100644 index 000000000..0592b6b23 --- /dev/null +++ b/binding-tests/task.cpp @@ -0,0 +1,35 @@ +#include +#include "doctest.h" +#include "taskchampion.h" + +TEST_CASE("creating a Task does not crash") { + TCReplica *rep = tc_replica_new(NULL); + CHECK(tc_replica_error(rep) == NULL); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_new("my task")); + REQUIRE(task != NULL); + + CHECK(tc_task_get_status(task) == TC_STATUS_PENDING); + + TCString *desc = tc_task_get_description(task); + REQUIRE(desc != NULL); + CHECK(strcmp(tc_string_content(desc), "my task") == 0); + tc_string_free(desc); + + tc_task_free(task); + + tc_replica_free(rep); +} + +TEST_CASE("undo on an empty in-memory TCReplica does nothing") { + TCReplica *rep = tc_replica_new(NULL); + CHECK(tc_replica_error(rep) == NULL); + int rv = tc_replica_undo(rep); + CHECK(rv == 0); + CHECK(tc_replica_error(rep) == NULL); + tc_replica_free(rep); +} + diff --git a/lib/build.rs b/lib/build.rs index 8d9db2f1f..13eda1bc5 100644 --- a/lib/build.rs +++ b/lib/build.rs @@ -10,6 +10,11 @@ fn main() { .with_language(Language::C) .with_config(Config { cpp_compat: true, + enumeration: EnumConfig { + // this appears to still default to true for C + enum_class: false, + ..Default::default() + }, ..Default::default() }) .generate() diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 98b7c0124..a97d0f732 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,2 +1,5 @@ pub mod replica; +pub mod status; +pub mod string; +pub mod task; pub mod uuid; diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 44066507e..81a387ca5 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -1,3 +1,4 @@ +use crate::{status::TCStatus, string::TCString, task::TCTask}; use libc::c_char; use std::ffi::{CStr, CString, OsStr}; use std::path::PathBuf; @@ -9,11 +10,37 @@ use std::os::unix::ffi::OsStrExt; /// A replica represents an instance of a user's task data, providing an easy interface /// for querying and modifying that data. pub struct TCReplica { - // TODO: make this an option so that it can be take()n when holding a mut ref + // TODO: make this a RefCell so that it can be take()n when holding a mut ref inner: Replica, error: Option, } +/// Utility function to safely convert *mut TCReplica into &mut TCReplica +fn rep_ref(rep: *mut TCReplica) -> &'static mut TCReplica { + debug_assert!(!rep.is_null()); + unsafe { &mut *rep } +} + +/// Utility function to allow using `?` notation to return an error value. +fn wrap<'a, T, F>(rep: *mut TCReplica, f: F, err_value: T) -> T +where + F: FnOnce(&mut Replica) -> anyhow::Result, +{ + let rep: &'a mut TCReplica = rep_ref(rep); + match f(&mut rep.inner) { + Ok(v) => v, + Err(e) => { + let error = e.to_string(); + let error = match CString::new(error.as_bytes()) { + Ok(e) => e, + Err(_) => CString::new("(invalid error message)".as_bytes()).unwrap(), + }; + rep.error = Some(error); + err_value + } + } +} + /// Create a new TCReplica. /// /// If path is NULL, then an in-memory replica is created. Otherwise, path is the path to the @@ -45,30 +72,37 @@ pub extern "C" fn tc_replica_new<'a>(path: *const c_char) -> *mut TCReplica { })) } -/// Utility function to safely convert *mut TCReplica into &mut TCReplica -fn rep_ref(rep: *mut TCReplica) -> &'static mut TCReplica { - debug_assert!(!rep.is_null()); - unsafe { &mut *rep } +/* + * TODO: + * - tc_replica_all_tasks + * - tc_replica_all_task_uuids + * - tc_replica_working_set + * - tc_replica_get_task + */ + +/// Create a new task. The task must not already exist. +/// +/// Returns the task, or NULL on error. +#[no_mangle] +pub extern "C" fn tc_replica_new_task<'a>( + rep: *mut TCReplica, + status: TCStatus, + description: *mut TCString, +) -> *mut TCTask { + wrap( + rep, + |rep| { + let description = TCString::from_arg(description); + let task = rep.new_task(status.into(), description.as_str()?.to_string())?; + Ok(TCTask::as_ptr(task)) + }, + std::ptr::null_mut(), + ) } -fn wrap<'a, T, F>(rep: *mut TCReplica, f: F, err_value: T) -> T -where - F: FnOnce(&mut Replica) -> anyhow::Result, -{ - let rep: &'a mut TCReplica = rep_ref(rep); - match f(&mut rep.inner) { - Ok(v) => v, - Err(e) => { - let error = e.to_string(); - let error = match CString::new(error.as_bytes()) { - Ok(e) => e, - Err(_) => CString::new("(invalid error message)".as_bytes()).unwrap(), - }; - rep.error = Some(error); - err_value - } - } -} +/* - tc_replica_import_task_with_uuid + * - tc_replica_sync + */ /// Undo local operations until the most recent UndoPoint. /// @@ -95,5 +129,11 @@ pub extern "C" fn tc_replica_error<'a>(rep: *mut TCReplica) -> *const c_char { /// Free a TCReplica. #[no_mangle] pub extern "C" fn tc_replica_free(rep: *mut TCReplica) { + debug_assert!(!rep.is_null()); drop(unsafe { Box::from_raw(rep) }); } + +/* + * - tc_replica_rebuild_working_set + * - tc_replica_add_undo_point + */ diff --git a/lib/src/status.rs b/lib/src/status.rs new file mode 100644 index 000000000..306f27630 --- /dev/null +++ b/lib/src/status.rs @@ -0,0 +1,36 @@ +pub use taskchampion::Status; + +/// The status of a task, as defined by the task data model. +/// cbindgen:prefix-with-name +/// cbindgen:rename-all=ScreamingSnakeCase +#[repr(C)] +pub enum TCStatus { + Pending, + Completed, + Deleted, + /// Unknown signifies a status in the task DB that was not + /// recognized. + Unknown, +} + +impl From for Status { + fn from(status: TCStatus) -> Status { + match status { + TCStatus::Pending => Status::Pending, + TCStatus::Completed => Status::Completed, + TCStatus::Deleted => Status::Deleted, + TCStatus::Unknown => Status::Unknown("unknown".to_string()), + } + } +} + +impl From for TCStatus { + fn from(status: Status) -> TCStatus { + match status { + Status::Pending => TCStatus::Pending, + Status::Completed => TCStatus::Completed, + Status::Deleted => TCStatus::Deleted, + Status::Unknown(_) => TCStatus::Unknown, + } + } +} diff --git a/lib/src/string.rs b/lib/src/string.rs new file mode 100644 index 000000000..39e17be13 --- /dev/null +++ b/lib/src/string.rs @@ -0,0 +1,48 @@ +use std::ffi::{CStr, CString, NulError}; + +// thinking: +// - TCString ownership always taken when passed in +// - TCString ownership always falls to C when passed out +// - accept that bytes must be copied to get owned string +// - Can we do this with an enum of some sort? + +/// TCString supports passing strings into and out of the TaskChampion API. +pub struct TCString(CString); + +impl TCString { + /// Take a TCString from C as an argument. + pub(crate) fn from_arg(tcstring: *mut TCString) -> Self { + debug_assert!(!tcstring.is_null()); + *(unsafe { Box::from_raw(tcstring) }) + } + + /// Get a regular Rust &str for this value. + pub(crate) fn as_str(&self) -> Result<&str, std::str::Utf8Error> { + self.0.as_c_str().to_str() + } + + /// Construct a *mut TCString from a string for returning to C. + pub(crate) fn return_string(string: impl Into>) -> Result<*mut TCString, NulError> { + let tcstring = TCString(CString::new(string)?); + Ok(Box::into_raw(Box::new(tcstring))) + } +} + +#[no_mangle] +pub extern "C" fn tc_string_new(cstr: *const libc::c_char) -> *mut TCString { + let cstring = unsafe { CStr::from_ptr(cstr) }.into(); + Box::into_raw(Box::new(TCString(cstring))) +} + +#[no_mangle] +pub extern "C" fn tc_string_content(string: *mut TCString) -> *const libc::c_char { + debug_assert!(!string.is_null()); + let string: &CString = unsafe { &(*string).0 }; + string.as_ptr() +} + +#[no_mangle] +pub extern "C" fn tc_string_free(string: *mut TCString) { + debug_assert!(!string.is_null()); + drop(unsafe { Box::from_raw(string) }); +} diff --git a/lib/src/task.rs b/lib/src/task.rs new file mode 100644 index 000000000..781b5d3d0 --- /dev/null +++ b/lib/src/task.rs @@ -0,0 +1,96 @@ +use crate::{status::TCStatus, string::TCString, uuid::TCUuid}; +use taskchampion::Task; + +/// A task, as publicly exposed by this library. +/// +/// A task carries no reference to the replica that created it, and can +/// be used until it is freed or converted to a TaskMut. +pub struct TCTask { + inner: Task, +} + +impl TCTask { + pub(crate) fn as_ptr(task: Task) -> *mut TCTask { + Box::into_raw(Box::new(TCTask { inner: task })) + } +} + +/// Utility function to allow using `?` notation to return an error value. +fn wrap<'a, T, F>(task: *const TCTask, f: F, err_value: T) -> T +where + F: FnOnce(&Task) -> anyhow::Result, +{ + let task: &'a Task = task_ref(task); + match f(task) { + Ok(v) => v, + Err(e) => { + /* + let error = e.to_string(); + let error = match CString::new(error.as_bytes()) { + Ok(e) => e, + Err(_) => CString::new("(invalid error message)".as_bytes()).unwrap(), + }; + */ + //task.error = Some(error); + err_value + } + } +} + +/// Utility function to safely convert *const TCTask into &Task +fn task_ref(task: *const TCTask) -> &'static Task { + debug_assert!(!task.is_null()); + unsafe { &(*task).inner } +} + +/// Get a task's UUID. +#[no_mangle] +pub extern "C" fn tc_task_get_uuid<'a>(task: *const TCTask) -> TCUuid { + let task: &'a Task = task_ref(task); + let uuid = task.get_uuid(); + uuid.into() +} + +/// Get a task's status. +#[no_mangle] +pub extern "C" fn tc_task_get_status<'a>(task: *const TCTask) -> TCStatus { + let task: &'a Task = task_ref(task); + task.get_status().into() +} + +/* TODO + * into_mut + * get_taskmap + */ + +/// Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it +/// contains embedded NUL characters). +#[no_mangle] +pub extern "C" fn tc_task_get_description<'a>(task: *const TCTask) -> *mut TCString { + wrap( + task, + |task| Ok(TCString::return_string(task.get_description())?), + std::ptr::null_mut(), + ) +} + +/* TODO + * get_wait + * is_waiting + * is_active + * has_tag + * get_tags + * get_annotations + * get_uda + * get_udas + * get_legacy_uda + * get_legacy_udas + * get_modified + */ + +/// Free a task. +#[no_mangle] +pub extern "C" fn tc_task_free<'a>(task: *mut TCTask) { + debug_assert!(!task.is_null()); + drop(unsafe { Box::from_raw(task) }); +} diff --git a/lib/taskchampion.h b/lib/taskchampion.h index c018143d5..5d9691d70 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -4,11 +4,32 @@ #include #include +/// The status of a task, as defined by the task data model. +enum TCStatus { + TC_STATUS_PENDING, + TC_STATUS_COMPLETED, + TC_STATUS_DELETED, + /// Unknown signifies a status in the task DB that was not + /// recognized. + TC_STATUS_UNKNOWN, +}; + /// A replica represents an instance of a user's task data, providing an easy interface /// for querying and modifying that data. struct TCReplica; +/// TCString supports passing strings into and out of the TaskChampion API. +struct TCString; + +/// A task, as publicly exposed by this library. +/// +/// A task carries no reference to the replica that created it, and can +/// be used until it is freed or converted to a TaskMut. +struct TCTask; + /// TCUuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. +/// Uuids are typically treated as opaque, but the bytes are available in big-endian format. +/// struct TCUuid { uint8_t bytes[16]; }; @@ -27,6 +48,11 @@ extern const uintptr_t TC_UUID_STRING_BYTES; /// TCReplicas are not threadsafe. TCReplica *tc_replica_new(const char *path); +/// Create a new task. The task must not already exist. +/// +/// Returns the task, or NULL on error. +TCTask *tc_replica_new_task(TCReplica *rep, TCStatus status, TCString *description); + /// Undo local operations until the most recent UndoPoint. /// /// Returns -1 on error, 0 if there are no local operations to undo, and 1 if operations were @@ -41,6 +67,25 @@ const char *tc_replica_error(TCReplica *rep); /// Free a TCReplica. void tc_replica_free(TCReplica *rep); +TCString *tc_string_new(const char *cstr); + +const char *tc_string_content(TCString *string); + +void tc_string_free(TCString *string); + +/// Get a task's UUID. +TCUuid tc_task_get_uuid(const TCTask *task); + +/// Get a task's status. +TCStatus tc_task_get_status(const TCTask *task); + +/// Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it +/// contains embedded NUL characters). +TCString *tc_task_get_description(const TCTask *task); + +/// Free a task. +void tc_task_free(TCTask *task); + /// Create a new, randomly-generated UUID. TCUuid tc_uuid_new_v4(); diff --git a/taskchampion/src/task/status.rs b/taskchampion/src/task/status.rs index 2b2afb6ba..31fee9cf7 100644 --- a/taskchampion/src/task/status.rs +++ b/taskchampion/src/task/status.rs @@ -1,5 +1,6 @@ /// The status of a task, as defined by the task data model. #[derive(Debug, PartialEq, Clone, strum_macros::Display)] +#[repr(C)] pub enum Status { Pending, Completed, From 65082c26e7314a2d91dbb0dc9dd25a00570cec73 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 23 Jan 2022 23:58:47 +0000 Subject: [PATCH 07/95] improved TCString support --- binding-tests/.gitignore | 1 + binding-tests/Makefile | 4 +- binding-tests/replica.cpp | 6 ++ binding-tests/string.cpp | 29 ++++++++ binding-tests/task.cpp | 10 --- lib/src/replica.rs | 17 ++--- lib/src/string.rs | 138 +++++++++++++++++++++++++++++++------- lib/src/task.rs | 32 ++------- lib/taskchampion.h | 22 +++++- 9 files changed, 183 insertions(+), 76 deletions(-) create mode 100644 binding-tests/string.cpp diff --git a/binding-tests/.gitignore b/binding-tests/.gitignore index fb93ddffb..aa82f31b4 100644 --- a/binding-tests/.gitignore +++ b/binding-tests/.gitignore @@ -1,2 +1,3 @@ *.o doctest +test-db diff --git a/binding-tests/Makefile b/binding-tests/Makefile index 121d942cc..26f621596 100644 --- a/binding-tests/Makefile +++ b/binding-tests/Makefile @@ -3,14 +3,16 @@ INC=-I ../lib LIB=-L ../target/debug RPATH=-Wl,-rpath,../target/debug -TESTS = replica.cpp uuid.cpp task.cpp +TESTS = replica.cpp string.cpp uuid.cpp task.cpp .PHONY: all test all: test test: doctest + @rm -rf test-db @./doctest --no-version --no-intro + @rm -rf test-db %.o: %.cpp ../lib/taskchampion.h $(CXX) $(INC) -c $< -o $@ diff --git a/binding-tests/replica.cpp b/binding-tests/replica.cpp index 4e1053d8c..18364a653 100644 --- a/binding-tests/replica.cpp +++ b/binding-tests/replica.cpp @@ -8,6 +8,12 @@ TEST_CASE("creating an in-memory TCReplica does not crash") { tc_replica_free(rep); } +TEST_CASE("creating an on-disk TCReplica does not crash") { + TCReplica *rep = tc_replica_new(tc_string_new("test-db")); + CHECK(tc_replica_error(rep) == NULL); + tc_replica_free(rep); +} + TEST_CASE("undo on an empty in-memory TCReplica does nothing") { TCReplica *rep = tc_replica_new(NULL); CHECK(tc_replica_error(rep) == NULL); diff --git a/binding-tests/string.cpp b/binding-tests/string.cpp new file mode 100644 index 000000000..dd726e862 --- /dev/null +++ b/binding-tests/string.cpp @@ -0,0 +1,29 @@ +#include +#include "doctest.h" +#include "taskchampion.h" + +TEST_CASE("creating borrowed strings does not crash") { + TCString *s = tc_string_new("abcdef"); + tc_string_free(s); +} + +TEST_CASE("creating cloned strings does not crash") { + char *abcdef = strdup("abcdef"); + TCString *s = tc_string_clone(abcdef); + free(abcdef); + CHECK(strcmp(tc_string_content(s), "abcdef") == 0); + tc_string_free(s); +} + +TEST_CASE("strings echo back their content") { + TCString *s = tc_string_new("abcdef"); + CHECK(strcmp(tc_string_content(s), "abcdef") == 0); + tc_string_free(s); +} + +TEST_CASE("tc_string_content returns NULL for strings containing embedded NULs") { + TCString *s = tc_string_clone_with_len("ab\0de", 5); + REQUIRE(s != NULL); + CHECK(tc_string_content(s) == NULL); + tc_string_free(s); +} diff --git a/binding-tests/task.cpp b/binding-tests/task.cpp index 0592b6b23..9d82767ca 100644 --- a/binding-tests/task.cpp +++ b/binding-tests/task.cpp @@ -23,13 +23,3 @@ TEST_CASE("creating a Task does not crash") { tc_replica_free(rep); } - -TEST_CASE("undo on an empty in-memory TCReplica does nothing") { - TCReplica *rep = tc_replica_new(NULL); - CHECK(tc_replica_error(rep) == NULL); - int rv = tc_replica_undo(rep); - CHECK(rv == 0); - CHECK(tc_replica_error(rep) == NULL); - tc_replica_free(rep); -} - diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 81a387ca5..c137b105a 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -1,12 +1,8 @@ use crate::{status::TCStatus, string::TCString, task::TCTask}; use libc::c_char; -use std::ffi::{CStr, CString, OsStr}; -use std::path::PathBuf; +use std::ffi::CString; use taskchampion::{Replica, StorageConfig}; -// TODO: unix-only -use std::os::unix::ffi::OsStrExt; - /// A replica represents an instance of a user's task data, providing an easy interface /// for querying and modifying that data. pub struct TCReplica { @@ -50,14 +46,15 @@ where /// /// TCReplicas are not threadsafe. #[no_mangle] -pub extern "C" fn tc_replica_new<'a>(path: *const c_char) -> *mut TCReplica { +pub extern "C" fn tc_replica_new<'a>(path: *mut TCString) -> *mut TCReplica { let storage_res = if path.is_null() { StorageConfig::InMemory.into_storage() } else { - let path: &'a [u8] = unsafe { CStr::from_ptr(path) }.to_bytes(); - let path: &OsStr = OsStr::from_bytes(path); - let path: PathBuf = path.to_os_string().into(); - StorageConfig::OnDisk { taskdb_dir: path }.into_storage() + let path = TCString::from_arg(path); + StorageConfig::OnDisk { + taskdb_dir: path.to_path_buf(), + } + .into_storage() }; let storage = match storage_res { diff --git a/lib/src/string.rs b/lib/src/string.rs index 39e17be13..3f9086d0e 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -1,46 +1,134 @@ -use std::ffi::{CStr, CString, NulError}; - -// thinking: -// - TCString ownership always taken when passed in -// - TCString ownership always falls to C when passed out -// - accept that bytes must be copied to get owned string -// - Can we do this with an enum of some sort? +use std::ffi::{CStr, CString, OsStr}; +use std::os::unix::ffi::OsStrExt; +use std::path::PathBuf; /// TCString supports passing strings into and out of the TaskChampion API. -pub struct TCString(CString); +/// +/// Unless specified otherwise, functions in this API take ownership of a TCString when it appears +/// as a function argument, and transfer ownership to the caller when the TCString appears as a +/// return value or otput argument. +pub enum TCString<'a> { + CString(CString), + CStr(&'a CStr), + String(String), +} -impl TCString { - /// Take a TCString from C as an argument. - pub(crate) fn from_arg(tcstring: *mut TCString) -> Self { +impl<'a> TCString<'a> { + /// Take a TCString from C as an argument. C callers generally expect TC functions to take + /// ownership of a string, which is what this function does. + pub(crate) fn from_arg(tcstring: *mut TCString<'a>) -> Self { debug_assert!(!tcstring.is_null()); *(unsafe { Box::from_raw(tcstring) }) } + /// Borrow a TCString from C as an argument. + pub(crate) fn from_arg_ref(tcstring: *mut TCString<'a>) -> &'a mut Self { + debug_assert!(!tcstring.is_null()); + unsafe { &mut *tcstring } + } + /// Get a regular Rust &str for this value. pub(crate) fn as_str(&self) -> Result<&str, std::str::Utf8Error> { - self.0.as_c_str().to_str() + match self { + TCString::CString(cstring) => cstring.as_c_str().to_str(), + TCString::CStr(cstr) => cstr.to_str(), + TCString::String(string) => Ok(string.as_ref()), + } } - /// Construct a *mut TCString from a string for returning to C. - pub(crate) fn return_string(string: impl Into>) -> Result<*mut TCString, NulError> { - let tcstring = TCString(CString::new(string)?); - Ok(Box::into_raw(Box::new(tcstring))) + pub(crate) fn as_bytes(&self) -> &[u8] { + match self { + TCString::CString(cstring) => cstring.as_bytes(), + TCString::CStr(cstr) => cstr.to_bytes(), + TCString::String(string) => string.as_bytes(), + } + } + + pub(crate) fn to_path_buf(&self) -> PathBuf { + // TODO: this is UNIX-specific. + let path: &OsStr = OsStr::from_bytes(self.as_bytes()); + path.to_os_string().into() + } + + /// Convert this to a return value for handing off to C. + pub(crate) fn return_val(self) -> *mut TCString<'a> { + Box::into_raw(Box::new(self)) } } -#[no_mangle] -pub extern "C" fn tc_string_new(cstr: *const libc::c_char) -> *mut TCString { - let cstring = unsafe { CStr::from_ptr(cstr) }.into(); - Box::into_raw(Box::new(TCString(cstring))) +impl<'a> From for TCString<'a> { + fn from(string: String) -> TCString<'a> { + TCString::String(string) + } } -#[no_mangle] -pub extern "C" fn tc_string_content(string: *mut TCString) -> *const libc::c_char { - debug_assert!(!string.is_null()); - let string: &CString = unsafe { &(*string).0 }; - string.as_ptr() +impl<'a> From<&str> for TCString<'a> { + fn from(string: &str) -> TCString<'a> { + TCString::String(string.to_string()) + } } +/// Create a new TCString referencing the given C string. The C string must remain valid until +/// after the TCString is freed. It's typically easiest to ensure this by using a static string. +#[no_mangle] +pub extern "C" fn tc_string_new(cstr: *const libc::c_char) -> *mut TCString<'static> { + let cstr: &CStr = unsafe { CStr::from_ptr(cstr) }; + TCString::CStr(cstr).return_val() +} + +/// Create a new TCString by cloning the content of the given C string. +#[no_mangle] +pub extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> *mut TCString<'static> { + let cstr: &CStr = unsafe { CStr::from_ptr(cstr) }; + TCString::CString(cstr.into()).return_val() +} + +/// Create a new TCString containing the given string with the given length. This allows creation +/// of strings containing embedded NUL characters. If the given string is not valid UTF-8, this +/// function will return NULL. +#[no_mangle] +pub extern "C" fn tc_string_clone_with_len( + buf: *const libc::c_char, + len: usize, +) -> *mut TCString<'static> { + let slice = unsafe { std::slice::from_raw_parts(buf as *const u8, len) }; + let vec = slice.to_vec(); + if let Ok(string) = String::from_utf8(vec) { + TCString::String(string).return_val() + } else { + std::ptr::null_mut() + } +} + +/// Get the content of the string as a regular C string. The given string must not be NULL. The +/// returned value may be NULL if the string contains NUL bytes. +/// This function does _not_ take ownership of the TCString. +#[no_mangle] +pub extern "C" fn tc_string_content(tcstring: *mut TCString) -> *const libc::c_char { + let tcstring = TCString::from_arg_ref(tcstring); + // if we have a String, we need to consume it and turn it into + // a CString. + if let TCString::String(string) = tcstring { + // TODO: get rid of this clone + match CString::new(string.clone()) { + Ok(cstring) => { + *tcstring = TCString::CString(cstring); + } + Err(_) => { + // TODO: could recover the underlying String + return std::ptr::null(); + } + } + } + + match tcstring { + TCString::CString(cstring) => cstring.as_ptr(), + TCString::String(_) => unreachable!(), // just converted this + TCString::CStr(cstr) => cstr.as_ptr(), + } +} + +/// Free a TCString. #[no_mangle] pub extern "C" fn tc_string_free(string: *mut TCString) { debug_assert!(!string.is_null()); diff --git a/lib/src/task.rs b/lib/src/task.rs index 781b5d3d0..ba129caa4 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -15,28 +15,6 @@ impl TCTask { } } -/// Utility function to allow using `?` notation to return an error value. -fn wrap<'a, T, F>(task: *const TCTask, f: F, err_value: T) -> T -where - F: FnOnce(&Task) -> anyhow::Result, -{ - let task: &'a Task = task_ref(task); - match f(task) { - Ok(v) => v, - Err(e) => { - /* - let error = e.to_string(); - let error = match CString::new(error.as_bytes()) { - Ok(e) => e, - Err(_) => CString::new("(invalid error message)".as_bytes()).unwrap(), - }; - */ - //task.error = Some(error); - err_value - } - } -} - /// Utility function to safely convert *const TCTask into &Task fn task_ref(task: *const TCTask) -> &'static Task { debug_assert!(!task.is_null()); @@ -66,12 +44,10 @@ pub extern "C" fn tc_task_get_status<'a>(task: *const TCTask) -> TCStatus { /// Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it /// contains embedded NUL characters). #[no_mangle] -pub extern "C" fn tc_task_get_description<'a>(task: *const TCTask) -> *mut TCString { - wrap( - task, - |task| Ok(TCString::return_string(task.get_description())?), - std::ptr::null_mut(), - ) +pub extern "C" fn tc_task_get_description<'a>(task: *const TCTask) -> *mut TCString<'static> { + let task = task_ref(task); + let descr: TCString = task.get_description().into(); + descr.return_val() } /* TODO diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 5d9691d70..445916baf 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -19,6 +19,10 @@ enum TCStatus { struct TCReplica; /// TCString supports passing strings into and out of the TaskChampion API. +/// +/// Unless specified otherwise, functions in this API take ownership of a TCString when it appears +/// as a function argument, and transfer ownership to the caller when the TCString appears as a +/// return value or otput argument. struct TCString; /// A task, as publicly exposed by this library. @@ -46,7 +50,7 @@ extern const uintptr_t TC_UUID_STRING_BYTES; /// Returns NULL on error; see tc_replica_error. /// /// TCReplicas are not threadsafe. -TCReplica *tc_replica_new(const char *path); +TCReplica *tc_replica_new(TCString *path); /// Create a new task. The task must not already exist. /// @@ -67,10 +71,24 @@ const char *tc_replica_error(TCReplica *rep); /// Free a TCReplica. void tc_replica_free(TCReplica *rep); +/// Create a new TCString referencing the given C string. The C string must remain valid until +/// after the TCString is freed. It's typically easiest to ensure this by using a static string. TCString *tc_string_new(const char *cstr); -const char *tc_string_content(TCString *string); +/// Create a new TCString by cloning the content of the given C string. +TCString *tc_string_clone(const char *cstr); +/// Create a new TCString containing the given string with the given length. This allows creation +/// of strings containing embedded NUL characters. If the given string is not valid UTF-8, this +/// function will return NULL. +TCString *tc_string_clone_with_len(const char *buf, uintptr_t len); + +/// Get the content of the string as a regular C string. The given string must not be NULL. The +/// returned value may be NULL if the string contains NUL bytes. +/// This function does _not_ take ownership of the TCString. +const char *tc_string_content(TCString *tcstring); + +/// Free a TCString. void tc_string_free(TCString *string); /// Get a task's UUID. From 40f30c6d894358084bd4ce2df68ef88d169cced4 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Mon, 24 Jan 2022 04:12:58 +0000 Subject: [PATCH 08/95] remove unnecessary string clone --- binding-tests/string.cpp | 34 +++++++++++++++++++- lib/build.rs | 1 + lib/src/string.rs | 69 +++++++++++++++++++++++++++++++++------- lib/taskchampion.h | 16 ++++++++-- 4 files changed, 104 insertions(+), 16 deletions(-) diff --git a/binding-tests/string.cpp b/binding-tests/string.cpp index dd726e862..8b743b266 100644 --- a/binding-tests/string.cpp +++ b/binding-tests/string.cpp @@ -10,20 +10,52 @@ TEST_CASE("creating borrowed strings does not crash") { TEST_CASE("creating cloned strings does not crash") { char *abcdef = strdup("abcdef"); TCString *s = tc_string_clone(abcdef); + REQUIRE(s != NULL); free(abcdef); + CHECK(strcmp(tc_string_content(s), "abcdef") == 0); tc_string_free(s); } -TEST_CASE("strings echo back their content") { +TEST_CASE("borrowed strings echo back their content") { TCString *s = tc_string_new("abcdef"); + REQUIRE(s != NULL); + CHECK(strcmp(tc_string_content(s), "abcdef") == 0); + size_t len; + const char *buf = tc_string_content_with_len(s, &len); + REQUIRE(buf != NULL); + CHECK(len == 6); + CHECK(strncmp(buf, "abcdef", len) == 0); + tc_string_free(s); +} + +TEST_CASE("cloned strings echo back their content") { + char *orig = strdup("abcdef"); + TCString *s = tc_string_clone(orig); + REQUIRE(s != NULL); + free(orig); + + CHECK(strcmp(tc_string_content(s), "abcdef") == 0); + + size_t len; + const char *buf = tc_string_content_with_len(s, &len); + REQUIRE(buf != NULL); + CHECK(len == 6); + CHECK(strncmp(buf, "abcdef", len) == 0); tc_string_free(s); } TEST_CASE("tc_string_content returns NULL for strings containing embedded NULs") { TCString *s = tc_string_clone_with_len("ab\0de", 5); REQUIRE(s != NULL); + CHECK(tc_string_content(s) == NULL); + + size_t len; + const char *buf = tc_string_content_with_len(s, &len); + REQUIRE(buf != NULL); + CHECK(len == 5); + CHECK(strncmp(buf, "ab\0de", len) == 0); tc_string_free(s); } diff --git a/lib/build.rs b/lib/build.rs index 13eda1bc5..d1ebc7bf1 100644 --- a/lib/build.rs +++ b/lib/build.rs @@ -10,6 +10,7 @@ fn main() { .with_language(Language::C) .with_config(Config { cpp_compat: true, + usize_is_size_t: true, enumeration: EnumConfig { // this appears to still default to true for C enum_class: false, diff --git a/lib/src/string.rs b/lib/src/string.rs index 3f9086d0e..1816d2a7d 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -6,11 +6,21 @@ use std::path::PathBuf; /// /// Unless specified otherwise, functions in this API take ownership of a TCString when it appears /// as a function argument, and transfer ownership to the caller when the TCString appears as a -/// return value or otput argument. +/// return value or output argument. pub enum TCString<'a> { CString(CString), CStr(&'a CStr), String(String), + + /// None is the default value for TCString, but this variant is never seen by C code or by Rust + /// code outside of this module. + None, +} + +impl<'a> Default for TCString<'a> { + fn default() -> Self { + TCString::None + } } impl<'a> TCString<'a> { @@ -33,6 +43,7 @@ impl<'a> TCString<'a> { TCString::CString(cstring) => cstring.as_c_str().to_str(), TCString::CStr(cstr) => cstr.to_str(), TCString::String(string) => Ok(string.as_ref()), + TCString::None => unreachable!(), } } @@ -41,6 +52,7 @@ impl<'a> TCString<'a> { TCString::CString(cstring) => cstring.as_bytes(), TCString::CStr(cstr) => cstr.to_bytes(), TCString::String(string) => string.as_bytes(), + TCString::None => unreachable!(), } } @@ -101,33 +113,66 @@ pub extern "C" fn tc_string_clone_with_len( } /// Get the content of the string as a regular C string. The given string must not be NULL. The -/// returned value may be NULL if the string contains NUL bytes. +/// returned value is NULL if the string contains NUL bytes. The returned string is valid until +/// the TCString is freed or passed to another TC API function. +/// /// This function does _not_ take ownership of the TCString. #[no_mangle] pub extern "C" fn tc_string_content(tcstring: *mut TCString) -> *const libc::c_char { let tcstring = TCString::from_arg_ref(tcstring); // if we have a String, we need to consume it and turn it into // a CString. - if let TCString::String(string) = tcstring { - // TODO: get rid of this clone - match CString::new(string.clone()) { - Ok(cstring) => { - *tcstring = TCString::CString(cstring); - } - Err(_) => { - // TODO: could recover the underlying String - return std::ptr::null(); + if matches!(tcstring, TCString::String(_)) { + if let TCString::String(string) = std::mem::take(tcstring) { + match CString::new(string) { + Ok(cstring) => { + *tcstring = TCString::CString(cstring); + } + Err(nul_err) => { + // recover the underlying String from the NulError + let original_bytes = nul_err.into_vec(); + // SAFETY: original_bytes just came from a String, so must be valid utf8 + let string = unsafe { String::from_utf8_unchecked(original_bytes) }; + *tcstring = TCString::String(string); + + // and return NULL as advertized + return std::ptr::null(); + } } + } else { + unreachable!() } } match tcstring { TCString::CString(cstring) => cstring.as_ptr(), - TCString::String(_) => unreachable!(), // just converted this + TCString::String(_) => unreachable!(), // just converted to CString TCString::CStr(cstr) => cstr.as_ptr(), + TCString::None => unreachable!(), } } +/// Get the content of the string as a pointer and length. The given string must not be NULL. +/// This function can return any string, even one including NUL bytes. The returned string is +/// valid until the TCString is freed or passed to another TC API function. +/// +/// This function does _not_ take ownership of the TCString. +#[no_mangle] +pub extern "C" fn tc_string_content_with_len( + tcstring: *mut TCString, + len_out: *mut usize, +) -> *const libc::c_char { + let tcstring = TCString::from_arg_ref(tcstring); + let bytes = match tcstring { + TCString::CString(cstring) => cstring.as_bytes(), + TCString::String(string) => string.as_bytes(), + TCString::CStr(cstr) => cstr.to_bytes(), + TCString::None => unreachable!(), + }; + unsafe { *len_out = bytes.len() }; + bytes.as_ptr() as *const libc::c_char +} + /// Free a TCString. #[no_mangle] pub extern "C" fn tc_string_free(string: *mut TCString) { diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 445916baf..ad42de148 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -40,7 +41,7 @@ struct TCUuid { extern "C" { -extern const uintptr_t TC_UUID_STRING_BYTES; +extern const size_t TC_UUID_STRING_BYTES; /// Create a new TCReplica. /// @@ -81,13 +82,22 @@ TCString *tc_string_clone(const char *cstr); /// Create a new TCString containing the given string with the given length. This allows creation /// of strings containing embedded NUL characters. If the given string is not valid UTF-8, this /// function will return NULL. -TCString *tc_string_clone_with_len(const char *buf, uintptr_t len); +TCString *tc_string_clone_with_len(const char *buf, size_t len); /// Get the content of the string as a regular C string. The given string must not be NULL. The -/// returned value may be NULL if the string contains NUL bytes. +/// returned value is NULL if the string contains NUL bytes. The returned string is valid until +/// the TCString is freed or passed to another TC API function. +/// /// This function does _not_ take ownership of the TCString. const char *tc_string_content(TCString *tcstring); +/// Get the content of the string as a pointer and length. The given string must not be NULL. +/// This function can return any string, even one including NUL bytes. The returned string is +/// valid until the TCString is freed or passed to another TC API function. +/// +/// This function does _not_ take ownership of the TCString. +const char *tc_string_content_with_len(TCString *tcstring, size_t *len_out); + /// Free a TCString. void tc_string_free(TCString *string); From 017fb398beea0a3ae66b07e2023be05af7921b13 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Mon, 24 Jan 2022 04:14:10 +0000 Subject: [PATCH 09/95] replace a clone with a copy --- lib/src/uuid.rs | 3 +-- lib/taskchampion.h | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index 8ce407afc..5e3492bea 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -10,8 +10,7 @@ pub struct TCUuid([u8; 16]); impl From for TCUuid { fn from(uuid: Uuid) -> TCUuid { - // TODO: can we avoid clone here? - TCUuid(uuid.as_bytes().clone()) + TCUuid(*uuid.as_bytes()) } } diff --git a/lib/taskchampion.h b/lib/taskchampion.h index ad42de148..560f23eb1 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -23,7 +23,7 @@ struct TCReplica; /// /// Unless specified otherwise, functions in this API take ownership of a TCString when it appears /// as a function argument, and transfer ownership to the caller when the TCString appears as a -/// return value or otput argument. +/// return value or output argument. struct TCString; /// A task, as publicly exposed by this library. From 56a805151d9c87c00b623b82b99df5f225da518d Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Mon, 24 Jan 2022 15:44:51 +0000 Subject: [PATCH 10/95] use 2018 edition like the other crates --- lib/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index b01d1bcbd..b8545df90 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "taskchampion-lib" version = "0.1.0" -edition = "2021" +edition = "2018" build = "build.rs" [lib] From c006cbe8e5f5934fd81824df89eadc6ec409f82f Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Tue, 25 Jan 2022 01:27:24 +0000 Subject: [PATCH 11/95] test bindings in an integration-tests crate --- Cargo.lock | 9 + Cargo.toml | 3 +- binding-tests/.gitignore | 3 - binding-tests/Makefile | 21 - binding-tests/doctest.cpp | 2 - binding-tests/doctest.h | 6816 ----------------- binding-tests/replica.cpp | 24 - binding-tests/string.cpp | 61 - binding-tests/task.cpp | 25 - binding-tests/uuid.cpp | 38 - integration-tests/Cargo.toml | 14 + integration-tests/README.md | 32 + integration-tests/build.rs | 36 + integration-tests/src/bindings_tests/mod.rs | 20 + .../src/bindings_tests/replica.c | 39 + integration-tests/src/bindings_tests/string.c | 81 + integration-tests/src/bindings_tests/task.c | 34 + integration-tests/src/bindings_tests/test.c | 6 + .../src/bindings_tests/unity/LICENSE.txt | 21 + .../src/bindings_tests/unity/README.md | 3 + .../src/bindings_tests/unity/unity.c | 2119 +++++ .../src/bindings_tests/unity/unity.h | 661 ++ .../bindings_tests/unity/unity_internals.h | 1053 +++ integration-tests/src/bindings_tests/uuid.c | 44 + integration-tests/src/lib.rs | 1 + integration-tests/tests/bindings.rs | 15 + lib/build.rs | 4 +- lib/src/replica.rs | 81 +- lib/taskchampion.h | 246 +- 29 files changed, 4389 insertions(+), 7123 deletions(-) delete mode 100644 binding-tests/.gitignore delete mode 100644 binding-tests/Makefile delete mode 100644 binding-tests/doctest.cpp delete mode 100644 binding-tests/doctest.h delete mode 100644 binding-tests/replica.cpp delete mode 100644 binding-tests/string.cpp delete mode 100644 binding-tests/task.cpp delete mode 100644 binding-tests/uuid.cpp create mode 100644 integration-tests/Cargo.toml create mode 100644 integration-tests/README.md create mode 100644 integration-tests/build.rs create mode 100644 integration-tests/src/bindings_tests/mod.rs create mode 100644 integration-tests/src/bindings_tests/replica.c create mode 100644 integration-tests/src/bindings_tests/string.c create mode 100644 integration-tests/src/bindings_tests/task.c create mode 100644 integration-tests/src/bindings_tests/test.c create mode 100644 integration-tests/src/bindings_tests/unity/LICENSE.txt create mode 100644 integration-tests/src/bindings_tests/unity/README.md create mode 100644 integration-tests/src/bindings_tests/unity/unity.c create mode 100644 integration-tests/src/bindings_tests/unity/unity.h create mode 100644 integration-tests/src/bindings_tests/unity/unity_internals.h create mode 100644 integration-tests/src/bindings_tests/uuid.c create mode 100644 integration-tests/src/lib.rs create mode 100644 integration-tests/tests/bindings.rs diff --git a/Cargo.lock b/Cargo.lock index 4f577c437..7924f76e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1520,6 +1520,15 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "integration-tests" +version = "0.4.1" +dependencies = [ + "cc", + "taskchampion", + "taskchampion-lib", +] + [[package]] name = "iovec" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index a552a0dda..09c0a9677 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,5 +5,6 @@ members = [ "cli", "sync-server", "replica-server-tests", - "lib" + "lib", + "integration-tests", ] diff --git a/binding-tests/.gitignore b/binding-tests/.gitignore deleted file mode 100644 index aa82f31b4..000000000 --- a/binding-tests/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.o -doctest -test-db diff --git a/binding-tests/Makefile b/binding-tests/Makefile deleted file mode 100644 index 26f621596..000000000 --- a/binding-tests/Makefile +++ /dev/null @@ -1,21 +0,0 @@ -CXX=g++ -INC=-I ../lib -LIB=-L ../target/debug -RPATH=-Wl,-rpath,../target/debug - -TESTS = replica.cpp string.cpp uuid.cpp task.cpp - -.PHONY: all test - -all: test - -test: doctest - @rm -rf test-db - @./doctest --no-version --no-intro - @rm -rf test-db - -%.o: %.cpp ../lib/taskchampion.h - $(CXX) $(INC) -c $< -o $@ - -doctest: doctest.o $(subst .cpp,.o,$(TESTS)) - $(CXX) $(LIB) $(RPATH) $< $(subst .cpp,.o,$(TESTS)) -ltaskchampion -o $@ diff --git a/binding-tests/doctest.cpp b/binding-tests/doctest.cpp deleted file mode 100644 index a3f832e49..000000000 --- a/binding-tests/doctest.cpp +++ /dev/null @@ -1,2 +0,0 @@ -#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN -#include "doctest.h" diff --git a/binding-tests/doctest.h b/binding-tests/doctest.h deleted file mode 100644 index d25f52682..000000000 --- a/binding-tests/doctest.h +++ /dev/null @@ -1,6816 +0,0 @@ -// ====================================================================== lgtm [cpp/missing-header-guard] -// == DO NOT MODIFY THIS FILE BY HAND - IT IS AUTO GENERATED BY CMAKE! == -// ====================================================================== -// -// doctest.h - the lightest feature-rich C++ single-header testing framework for unit tests and TDD -// -// Copyright (c) 2016-2021 Viktor Kirilov -// -// Distributed under the MIT Software License -// See accompanying file LICENSE.txt or copy at -// https://opensource.org/licenses/MIT -// -// The documentation can be found at the library's page: -// https://github.com/doctest/doctest/blob/master/doc/markdown/readme.md -// -// ================================================================================================= -// ================================================================================================= -// ================================================================================================= -// -// The library is heavily influenced by Catch - https://github.com/catchorg/Catch2 -// which uses the Boost Software License - Version 1.0 -// see here - https://github.com/catchorg/Catch2/blob/master/LICENSE.txt -// -// The concept of subcases (sections in Catch) and expression decomposition are from there. -// Some parts of the code are taken directly: -// - stringification - the detection of "ostream& operator<<(ostream&, const T&)" and StringMaker<> -// - the Approx() helper class for floating point comparison -// - colors in the console -// - breaking into a debugger -// - signal / SEH handling -// - timer -// - XmlWriter class - thanks to Phil Nash for allowing the direct reuse (AKA copy/paste) -// -// The expression decomposing templates are taken from lest - https://github.com/martinmoene/lest -// which uses the Boost Software License - Version 1.0 -// see here - https://github.com/martinmoene/lest/blob/master/LICENSE.txt -// -// ================================================================================================= -// ================================================================================================= -// ================================================================================================= - -#ifndef DOCTEST_LIBRARY_INCLUDED -#define DOCTEST_LIBRARY_INCLUDED - -// ================================================================================================= -// == VERSION ====================================================================================== -// ================================================================================================= - -#define DOCTEST_VERSION_MAJOR 2 -#define DOCTEST_VERSION_MINOR 4 -#define DOCTEST_VERSION_PATCH 8 - -// util we need here -#define DOCTEST_TOSTR_IMPL(x) #x -#define DOCTEST_TOSTR(x) DOCTEST_TOSTR_IMPL(x) - -#define DOCTEST_VERSION_STR \ - DOCTEST_TOSTR(DOCTEST_VERSION_MAJOR) "." \ - DOCTEST_TOSTR(DOCTEST_VERSION_MINOR) "." \ - DOCTEST_TOSTR(DOCTEST_VERSION_PATCH) - -#define DOCTEST_VERSION \ - (DOCTEST_VERSION_MAJOR * 10000 + DOCTEST_VERSION_MINOR * 100 + DOCTEST_VERSION_PATCH) - -// ================================================================================================= -// == COMPILER VERSION ============================================================================= -// ================================================================================================= - -// ideas for the version stuff are taken from here: https://github.com/cxxstuff/cxx_detect - -#define DOCTEST_COMPILER(MAJOR, MINOR, PATCH) ((MAJOR)*10000000 + (MINOR)*100000 + (PATCH)) - -// GCC/Clang and GCC/MSVC are mutually exclusive, but Clang/MSVC are not because of clang-cl... -#if defined(_MSC_VER) && defined(_MSC_FULL_VER) -#if _MSC_VER == _MSC_FULL_VER / 10000 -#define DOCTEST_MSVC DOCTEST_COMPILER(_MSC_VER / 100, _MSC_VER % 100, _MSC_FULL_VER % 10000) -#else // MSVC -#define DOCTEST_MSVC \ - DOCTEST_COMPILER(_MSC_VER / 100, (_MSC_FULL_VER / 100000) % 100, _MSC_FULL_VER % 100000) -#endif // MSVC -#endif // MSVC -#if defined(__clang__) && defined(__clang_minor__) -#define DOCTEST_CLANG DOCTEST_COMPILER(__clang_major__, __clang_minor__, __clang_patchlevel__) -#elif defined(__GNUC__) && defined(__GNUC_MINOR__) && defined(__GNUC_PATCHLEVEL__) && \ - !defined(__INTEL_COMPILER) -#define DOCTEST_GCC DOCTEST_COMPILER(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) -#endif // GCC - -#ifndef DOCTEST_MSVC -#define DOCTEST_MSVC 0 -#endif // DOCTEST_MSVC -#ifndef DOCTEST_CLANG -#define DOCTEST_CLANG 0 -#endif // DOCTEST_CLANG -#ifndef DOCTEST_GCC -#define DOCTEST_GCC 0 -#endif // DOCTEST_GCC - -// ================================================================================================= -// == COMPILER WARNINGS HELPERS ==================================================================== -// ================================================================================================= - -#if DOCTEST_CLANG -#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x) -#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH _Pragma("clang diagnostic push") -#define DOCTEST_CLANG_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(clang diagnostic ignored w) -#define DOCTEST_CLANG_SUPPRESS_WARNING_POP _Pragma("clang diagnostic pop") -#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w) \ - DOCTEST_CLANG_SUPPRESS_WARNING_PUSH DOCTEST_CLANG_SUPPRESS_WARNING(w) -#else // DOCTEST_CLANG -#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH -#define DOCTEST_CLANG_SUPPRESS_WARNING(w) -#define DOCTEST_CLANG_SUPPRESS_WARNING_POP -#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w) -#endif // DOCTEST_CLANG - -#if DOCTEST_GCC -#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x) -#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH _Pragma("GCC diagnostic push") -#define DOCTEST_GCC_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(GCC diagnostic ignored w) -#define DOCTEST_GCC_SUPPRESS_WARNING_POP _Pragma("GCC diagnostic pop") -#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w) \ - DOCTEST_GCC_SUPPRESS_WARNING_PUSH DOCTEST_GCC_SUPPRESS_WARNING(w) -#else // DOCTEST_GCC -#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH -#define DOCTEST_GCC_SUPPRESS_WARNING(w) -#define DOCTEST_GCC_SUPPRESS_WARNING_POP -#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w) -#endif // DOCTEST_GCC - -#if DOCTEST_MSVC -#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH __pragma(warning(push)) -#define DOCTEST_MSVC_SUPPRESS_WARNING(w) __pragma(warning(disable : w)) -#define DOCTEST_MSVC_SUPPRESS_WARNING_POP __pragma(warning(pop)) -#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w) \ - DOCTEST_MSVC_SUPPRESS_WARNING_PUSH DOCTEST_MSVC_SUPPRESS_WARNING(w) -#else // DOCTEST_MSVC -#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH -#define DOCTEST_MSVC_SUPPRESS_WARNING(w) -#define DOCTEST_MSVC_SUPPRESS_WARNING_POP -#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w) -#endif // DOCTEST_MSVC - -// ================================================================================================= -// == COMPILER WARNINGS ============================================================================ -// ================================================================================================= - -// both the header and the implementation suppress all of these, -// so it only makes sense to aggregrate them like so -#define DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH \ - DOCTEST_CLANG_SUPPRESS_WARNING_PUSH \ - DOCTEST_CLANG_SUPPRESS_WARNING("-Wunknown-pragmas") \ - DOCTEST_CLANG_SUPPRESS_WARNING("-Wweak-vtables") \ - DOCTEST_CLANG_SUPPRESS_WARNING("-Wpadded") \ - DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-prototypes") \ - DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-local-typedef") \ - DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat") \ - DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat-pedantic") \ - \ - DOCTEST_GCC_SUPPRESS_WARNING_PUSH \ - DOCTEST_GCC_SUPPRESS_WARNING("-Wunknown-pragmas") \ - DOCTEST_GCC_SUPPRESS_WARNING("-Wpragmas") \ - DOCTEST_GCC_SUPPRESS_WARNING("-Weffc++") \ - DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-overflow") \ - DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-aliasing") \ - DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-declarations") \ - DOCTEST_GCC_SUPPRESS_WARNING("-Wunused-local-typedefs") \ - DOCTEST_GCC_SUPPRESS_WARNING("-Wuseless-cast") \ - DOCTEST_GCC_SUPPRESS_WARNING("-Wnoexcept") \ - \ - DOCTEST_MSVC_SUPPRESS_WARNING_PUSH \ - /* these 4 also disabled globally via cmake: */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4514) /* unreferenced inline function has been removed */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4571) /* SEH related */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4710) /* function not inlined */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4711) /* function selected for inline expansion*/ \ - /* */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4616) /* invalid compiler warning */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4619) /* invalid compiler warning */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4996) /* The compiler encountered a deprecated declaration */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4706) /* assignment within conditional expression */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4512) /* 'class' : assignment operator could not be generated */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4127) /* conditional expression is constant */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4820) /* padding */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4625) /* copy constructor was implicitly deleted */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4626) /* assignment operator was implicitly deleted */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(5027) /* move assignment operator implicitly deleted */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(5026) /* move constructor was implicitly deleted */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4640) /* construction of local static object not thread-safe */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(5045) /* Spectre mitigation for memory load */ \ - /* static analysis */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(26439) /* Function may not throw. Declare it 'noexcept' */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(26495) /* Always initialize a member variable */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(26451) /* Arithmetic overflow ... */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(26444) /* Avoid unnamed objects with custom ctor and dtor... */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(26812) /* Prefer 'enum class' over 'enum' */ - -#define DOCTEST_SUPPRESS_COMMON_WARNINGS_POP \ - DOCTEST_CLANG_SUPPRESS_WARNING_POP \ - DOCTEST_GCC_SUPPRESS_WARNING_POP \ - DOCTEST_MSVC_SUPPRESS_WARNING_POP - -DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH - -DOCTEST_CLANG_SUPPRESS_WARNING_PUSH -DOCTEST_CLANG_SUPPRESS_WARNING("-Wnon-virtual-dtor") -DOCTEST_CLANG_SUPPRESS_WARNING("-Wdeprecated") - -DOCTEST_GCC_SUPPRESS_WARNING_PUSH -DOCTEST_GCC_SUPPRESS_WARNING("-Wctor-dtor-privacy") -DOCTEST_GCC_SUPPRESS_WARNING("-Wnon-virtual-dtor") -DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-promo") - -DOCTEST_MSVC_SUPPRESS_WARNING_PUSH -DOCTEST_MSVC_SUPPRESS_WARNING(4623) // default constructor was implicitly defined as deleted - -#define DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN \ - DOCTEST_MSVC_SUPPRESS_WARNING_PUSH \ - DOCTEST_MSVC_SUPPRESS_WARNING(4548) /* before comma no effect; expected side - effect */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4265) /* virtual functions, but destructor is not virtual */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4986) /* exception specification does not match previous */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4350) /* 'member1' called instead of 'member2' */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4668) /* not defined as a preprocessor macro */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4365) /* signed/unsigned mismatch */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4774) /* format string not a string literal */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4820) /* padding */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4625) /* copy constructor was implicitly deleted */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4626) /* assignment operator was implicitly deleted */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(5027) /* move assignment operator implicitly deleted */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(5026) /* move constructor was implicitly deleted */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(4623) /* default constructor was implicitly deleted */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(5039) /* pointer to pot. throwing function passed to extern C */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(5045) /* Spectre mitigation for memory load */ \ - DOCTEST_MSVC_SUPPRESS_WARNING(5105) /* macro producing 'defined' has undefined behavior */ - -#define DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END DOCTEST_MSVC_SUPPRESS_WARNING_POP - -// ================================================================================================= -// == FEATURE DETECTION ============================================================================ -// ================================================================================================= - -// general compiler feature support table: https://en.cppreference.com/w/cpp/compiler_support -// MSVC C++11 feature support table: https://msdn.microsoft.com/en-us/library/hh567368.aspx -// GCC C++11 feature support table: https://gcc.gnu.org/projects/cxx-status.html -// MSVC version table: -// https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#Internal_version_numbering -// MSVC++ 14.3 (17) _MSC_VER == 1930 (Visual Studio 2022) -// MSVC++ 14.2 (16) _MSC_VER == 1920 (Visual Studio 2019) -// MSVC++ 14.1 (15) _MSC_VER == 1910 (Visual Studio 2017) -// MSVC++ 14.0 _MSC_VER == 1900 (Visual Studio 2015) -// MSVC++ 12.0 _MSC_VER == 1800 (Visual Studio 2013) -// MSVC++ 11.0 _MSC_VER == 1700 (Visual Studio 2012) -// MSVC++ 10.0 _MSC_VER == 1600 (Visual Studio 2010) -// MSVC++ 9.0 _MSC_VER == 1500 (Visual Studio 2008) -// MSVC++ 8.0 _MSC_VER == 1400 (Visual Studio 2005) - -// Universal Windows Platform support -#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) -#define DOCTEST_CONFIG_NO_WINDOWS_SEH -#endif // WINAPI_FAMILY -#if DOCTEST_MSVC && !defined(DOCTEST_CONFIG_WINDOWS_SEH) -#define DOCTEST_CONFIG_WINDOWS_SEH -#endif // MSVC -#if defined(DOCTEST_CONFIG_NO_WINDOWS_SEH) && defined(DOCTEST_CONFIG_WINDOWS_SEH) -#undef DOCTEST_CONFIG_WINDOWS_SEH -#endif // DOCTEST_CONFIG_NO_WINDOWS_SEH - -#if !defined(_WIN32) && !defined(__QNX__) && !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && \ - !defined(__EMSCRIPTEN__) -#define DOCTEST_CONFIG_POSIX_SIGNALS -#endif // _WIN32 -#if defined(DOCTEST_CONFIG_NO_POSIX_SIGNALS) && defined(DOCTEST_CONFIG_POSIX_SIGNALS) -#undef DOCTEST_CONFIG_POSIX_SIGNALS -#endif // DOCTEST_CONFIG_NO_POSIX_SIGNALS - -#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS -#if !defined(__cpp_exceptions) && !defined(__EXCEPTIONS) && !defined(_CPPUNWIND) -#define DOCTEST_CONFIG_NO_EXCEPTIONS -#endif // no exceptions -#endif // DOCTEST_CONFIG_NO_EXCEPTIONS - -#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS -#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS -#define DOCTEST_CONFIG_NO_EXCEPTIONS -#endif // DOCTEST_CONFIG_NO_EXCEPTIONS -#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS - -#if defined(DOCTEST_CONFIG_NO_EXCEPTIONS) && !defined(DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS) -#define DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS -#endif // DOCTEST_CONFIG_NO_EXCEPTIONS && !DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS - -#if defined(DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN) && !defined(DOCTEST_CONFIG_IMPLEMENT) -#define DOCTEST_CONFIG_IMPLEMENT -#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN - -#if defined(_WIN32) || defined(__CYGWIN__) -#if DOCTEST_MSVC -#define DOCTEST_SYMBOL_EXPORT __declspec(dllexport) -#define DOCTEST_SYMBOL_IMPORT __declspec(dllimport) -#else // MSVC -#define DOCTEST_SYMBOL_EXPORT __attribute__((dllexport)) -#define DOCTEST_SYMBOL_IMPORT __attribute__((dllimport)) -#endif // MSVC -#else // _WIN32 -#define DOCTEST_SYMBOL_EXPORT __attribute__((visibility("default"))) -#define DOCTEST_SYMBOL_IMPORT -#endif // _WIN32 - -#ifdef DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL -#ifdef DOCTEST_CONFIG_IMPLEMENT -#define DOCTEST_INTERFACE DOCTEST_SYMBOL_EXPORT -#else // DOCTEST_CONFIG_IMPLEMENT -#define DOCTEST_INTERFACE DOCTEST_SYMBOL_IMPORT -#endif // DOCTEST_CONFIG_IMPLEMENT -#else // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL -#define DOCTEST_INTERFACE -#endif // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL - -#define DOCTEST_EMPTY - -#if DOCTEST_MSVC -#define DOCTEST_NOINLINE __declspec(noinline) -#define DOCTEST_UNUSED -#define DOCTEST_ALIGNMENT(x) -#elif DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 5, 0) -#define DOCTEST_NOINLINE -#define DOCTEST_UNUSED -#define DOCTEST_ALIGNMENT(x) -#else -#define DOCTEST_NOINLINE __attribute__((noinline)) -#define DOCTEST_UNUSED __attribute__((unused)) -#define DOCTEST_ALIGNMENT(x) __attribute__((aligned(x))) -#endif - -#ifndef DOCTEST_NORETURN -#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) -#define DOCTEST_NORETURN -#else // DOCTEST_MSVC -#define DOCTEST_NORETURN [[noreturn]] -#endif // DOCTEST_MSVC -#endif // DOCTEST_NORETURN - -#ifndef DOCTEST_NOEXCEPT -#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) -#define DOCTEST_NOEXCEPT -#else // DOCTEST_MSVC -#define DOCTEST_NOEXCEPT noexcept -#endif // DOCTEST_MSVC -#endif // DOCTEST_NOEXCEPT - -#ifndef DOCTEST_CONSTEXPR -#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) -#define DOCTEST_CONSTEXPR const -#else // DOCTEST_MSVC -#define DOCTEST_CONSTEXPR constexpr -#endif // DOCTEST_MSVC -#endif // DOCTEST_CONSTEXPR - -// ================================================================================================= -// == FEATURE DETECTION END ======================================================================== -// ================================================================================================= - -// internal macros for string concatenation and anonymous variable name generation -#define DOCTEST_CAT_IMPL(s1, s2) s1##s2 -#define DOCTEST_CAT(s1, s2) DOCTEST_CAT_IMPL(s1, s2) -#ifdef __COUNTER__ // not standard and may be missing for some compilers -#define DOCTEST_ANONYMOUS(x) DOCTEST_CAT(x, __COUNTER__) -#else // __COUNTER__ -#define DOCTEST_ANONYMOUS(x) DOCTEST_CAT(x, __LINE__) -#endif // __COUNTER__ - -#ifndef DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE -#define DOCTEST_REF_WRAP(x) x& -#else // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE -#define DOCTEST_REF_WRAP(x) x -#endif // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE - -// not using __APPLE__ because... this is how Catch does it -#ifdef __MAC_OS_X_VERSION_MIN_REQUIRED -#define DOCTEST_PLATFORM_MAC -#elif defined(__IPHONE_OS_VERSION_MIN_REQUIRED) -#define DOCTEST_PLATFORM_IPHONE -#elif defined(_WIN32) -#define DOCTEST_PLATFORM_WINDOWS -#else // DOCTEST_PLATFORM -#define DOCTEST_PLATFORM_LINUX -#endif // DOCTEST_PLATFORM - -namespace doctest { namespace detail { - static DOCTEST_CONSTEXPR int consume(const int*, int) { return 0; } -}} - -#define DOCTEST_GLOBAL_NO_WARNINGS(var, ...) \ - DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wglobal-constructors") \ - static const int var = doctest::detail::consume(&var, __VA_ARGS__); \ - DOCTEST_CLANG_SUPPRESS_WARNING_POP - -#ifndef DOCTEST_BREAK_INTO_DEBUGGER -// should probably take a look at https://github.com/scottt/debugbreak -#ifdef DOCTEST_PLATFORM_LINUX -#if defined(__GNUC__) && (defined(__i386) || defined(__x86_64)) -// Break at the location of the failing check if possible -#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("int $3\n" : :) // NOLINT (hicpp-no-assembler) -#else -#include -#define DOCTEST_BREAK_INTO_DEBUGGER() raise(SIGTRAP) -#endif -#elif defined(DOCTEST_PLATFORM_MAC) -#if defined(__x86_64) || defined(__x86_64__) || defined(__amd64__) || defined(__i386) -#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("int $3\n" : :) // NOLINT (hicpp-no-assembler) -#else -#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("brk #0"); // NOLINT (hicpp-no-assembler) -#endif -#elif DOCTEST_MSVC -#define DOCTEST_BREAK_INTO_DEBUGGER() __debugbreak() -#elif defined(__MINGW32__) -DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wredundant-decls") -extern "C" __declspec(dllimport) void __stdcall DebugBreak(); -DOCTEST_GCC_SUPPRESS_WARNING_POP -#define DOCTEST_BREAK_INTO_DEBUGGER() ::DebugBreak() -#else // linux -#define DOCTEST_BREAK_INTO_DEBUGGER() (static_cast(0)) -#endif // linux -#endif // DOCTEST_BREAK_INTO_DEBUGGER - -// this is kept here for backwards compatibility since the config option was changed -#ifdef DOCTEST_CONFIG_USE_IOSFWD -#define DOCTEST_CONFIG_USE_STD_HEADERS -#endif // DOCTEST_CONFIG_USE_IOSFWD - -// for clang - always include ciso646 (which drags some std stuff) because -// we want to check if we are using libc++ with the _LIBCPP_VERSION macro in -// which case we don't want to forward declare stuff from std - for reference: -// https://github.com/doctest/doctest/issues/126 -// https://github.com/doctest/doctest/issues/356 -#if DOCTEST_CLANG -#include -#ifdef _LIBCPP_VERSION -#define DOCTEST_CONFIG_USE_STD_HEADERS -#endif // _LIBCPP_VERSION -#endif // clang - -#ifdef DOCTEST_CONFIG_USE_STD_HEADERS -#ifndef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS -#define DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS -#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS -#include -#include -#include -#else // DOCTEST_CONFIG_USE_STD_HEADERS - -// Forward declaring 'X' in namespace std is not permitted by the C++ Standard. -DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4643) - -namespace std { // NOLINT (cert-dcl58-cpp) -typedef decltype(nullptr) nullptr_t; -template -struct char_traits; -template <> -struct char_traits; -template -class basic_ostream; -typedef basic_ostream> ostream; -template -class basic_istream; -typedef basic_istream> istream; -template -class tuple; -#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) -// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 -template -class allocator; -template -class basic_string; -using string = basic_string, allocator>; -#endif // VS 2019 -} // namespace std - -DOCTEST_MSVC_SUPPRESS_WARNING_POP - -#endif // DOCTEST_CONFIG_USE_STD_HEADERS - -#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS -#include -#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS - -namespace doctest { - -DOCTEST_INTERFACE extern bool is_running_in_test; - -// A 24 byte string class (can be as small as 17 for x64 and 13 for x86) that can hold strings with length -// of up to 23 chars on the stack before going on the heap - the last byte of the buffer is used for: -// - "is small" bit - the highest bit - if "0" then it is small - otherwise its "1" (128) -// - if small - capacity left before going on the heap - using the lowest 5 bits -// - if small - 2 bits are left unused - the second and third highest ones -// - if small - acts as a null terminator if strlen() is 23 (24 including the null terminator) -// and the "is small" bit remains "0" ("as well as the capacity left") so its OK -// Idea taken from this lecture about the string implementation of facebook/folly - fbstring -// https://www.youtube.com/watch?v=kPR8h4-qZdk -// TODO: -// - optimizations - like not deleting memory unnecessarily in operator= and etc. -// - resize/reserve/clear -// - substr -// - replace -// - back/front -// - iterator stuff -// - find & friends -// - push_back/pop_back -// - assign/insert/erase -// - relational operators as free functions - taking const char* as one of the params -class DOCTEST_INTERFACE String -{ - static const unsigned len = 24; //!OCLINT avoid private static members - static const unsigned last = len - 1; //!OCLINT avoid private static members - - struct view // len should be more than sizeof(view) - because of the final byte for flags - { - char* ptr; - unsigned size; - unsigned capacity; - }; - - union - { - char buf[len]; - view data; - }; - - char* allocate(unsigned sz); - - bool isOnStack() const { return (buf[last] & 128) == 0; } - void setOnHeap(); - void setLast(unsigned in = last); - - void copy(const String& other); - -public: - String(); - ~String(); - - // cppcheck-suppress noExplicitConstructor - String(const char* in); - String(const char* in, unsigned in_size); - - String(std::istream& in, unsigned in_size); - - String(const String& other); - String& operator=(const String& other); - - String& operator+=(const String& other); - - String(String&& other); - String& operator=(String&& other); - - char operator[](unsigned i) const; - char& operator[](unsigned i); - - // the only functions I'm willing to leave in the interface - available for inlining - const char* c_str() const { return const_cast(this)->c_str(); } // NOLINT - char* c_str() { - if(isOnStack()) - return reinterpret_cast(buf); - return data.ptr; - } - - unsigned size() const; - unsigned capacity() const; - - int compare(const char* other, bool no_case = false) const; - int compare(const String& other, bool no_case = false) const; -}; - -DOCTEST_INTERFACE String operator+(const String& lhs, const String& rhs); - -DOCTEST_INTERFACE bool operator==(const String& lhs, const String& rhs); -DOCTEST_INTERFACE bool operator!=(const String& lhs, const String& rhs); -DOCTEST_INTERFACE bool operator<(const String& lhs, const String& rhs); -DOCTEST_INTERFACE bool operator>(const String& lhs, const String& rhs); -DOCTEST_INTERFACE bool operator<=(const String& lhs, const String& rhs); -DOCTEST_INTERFACE bool operator>=(const String& lhs, const String& rhs); - -DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& s, const String& in); - -namespace Color { - enum Enum - { - None = 0, - White, - Red, - Green, - Blue, - Cyan, - Yellow, - Grey, - - Bright = 0x10, - - BrightRed = Bright | Red, - BrightGreen = Bright | Green, - LightGrey = Bright | Grey, - BrightWhite = Bright | White - }; - - DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& s, Color::Enum code); -} // namespace Color - -namespace assertType { - enum Enum - { - // macro traits - - is_warn = 1, - is_check = 2 * is_warn, - is_require = 2 * is_check, - - is_normal = 2 * is_require, - is_throws = 2 * is_normal, - is_throws_as = 2 * is_throws, - is_throws_with = 2 * is_throws_as, - is_nothrow = 2 * is_throws_with, - - is_false = 2 * is_nothrow, - is_unary = 2 * is_false, // not checked anywhere - used just to distinguish the types - - is_eq = 2 * is_unary, - is_ne = 2 * is_eq, - - is_lt = 2 * is_ne, - is_gt = 2 * is_lt, - - is_ge = 2 * is_gt, - is_le = 2 * is_ge, - - // macro types - - DT_WARN = is_normal | is_warn, - DT_CHECK = is_normal | is_check, - DT_REQUIRE = is_normal | is_require, - - DT_WARN_FALSE = is_normal | is_false | is_warn, - DT_CHECK_FALSE = is_normal | is_false | is_check, - DT_REQUIRE_FALSE = is_normal | is_false | is_require, - - DT_WARN_THROWS = is_throws | is_warn, - DT_CHECK_THROWS = is_throws | is_check, - DT_REQUIRE_THROWS = is_throws | is_require, - - DT_WARN_THROWS_AS = is_throws_as | is_warn, - DT_CHECK_THROWS_AS = is_throws_as | is_check, - DT_REQUIRE_THROWS_AS = is_throws_as | is_require, - - DT_WARN_THROWS_WITH = is_throws_with | is_warn, - DT_CHECK_THROWS_WITH = is_throws_with | is_check, - DT_REQUIRE_THROWS_WITH = is_throws_with | is_require, - - DT_WARN_THROWS_WITH_AS = is_throws_with | is_throws_as | is_warn, - DT_CHECK_THROWS_WITH_AS = is_throws_with | is_throws_as | is_check, - DT_REQUIRE_THROWS_WITH_AS = is_throws_with | is_throws_as | is_require, - - DT_WARN_NOTHROW = is_nothrow | is_warn, - DT_CHECK_NOTHROW = is_nothrow | is_check, - DT_REQUIRE_NOTHROW = is_nothrow | is_require, - - DT_WARN_EQ = is_normal | is_eq | is_warn, - DT_CHECK_EQ = is_normal | is_eq | is_check, - DT_REQUIRE_EQ = is_normal | is_eq | is_require, - - DT_WARN_NE = is_normal | is_ne | is_warn, - DT_CHECK_NE = is_normal | is_ne | is_check, - DT_REQUIRE_NE = is_normal | is_ne | is_require, - - DT_WARN_GT = is_normal | is_gt | is_warn, - DT_CHECK_GT = is_normal | is_gt | is_check, - DT_REQUIRE_GT = is_normal | is_gt | is_require, - - DT_WARN_LT = is_normal | is_lt | is_warn, - DT_CHECK_LT = is_normal | is_lt | is_check, - DT_REQUIRE_LT = is_normal | is_lt | is_require, - - DT_WARN_GE = is_normal | is_ge | is_warn, - DT_CHECK_GE = is_normal | is_ge | is_check, - DT_REQUIRE_GE = is_normal | is_ge | is_require, - - DT_WARN_LE = is_normal | is_le | is_warn, - DT_CHECK_LE = is_normal | is_le | is_check, - DT_REQUIRE_LE = is_normal | is_le | is_require, - - DT_WARN_UNARY = is_normal | is_unary | is_warn, - DT_CHECK_UNARY = is_normal | is_unary | is_check, - DT_REQUIRE_UNARY = is_normal | is_unary | is_require, - - DT_WARN_UNARY_FALSE = is_normal | is_false | is_unary | is_warn, - DT_CHECK_UNARY_FALSE = is_normal | is_false | is_unary | is_check, - DT_REQUIRE_UNARY_FALSE = is_normal | is_false | is_unary | is_require, - }; -} // namespace assertType - -DOCTEST_INTERFACE const char* assertString(assertType::Enum at); -DOCTEST_INTERFACE const char* failureString(assertType::Enum at); -DOCTEST_INTERFACE const char* skipPathFromFilename(const char* file); - -struct DOCTEST_INTERFACE TestCaseData -{ - String m_file; // the file in which the test was registered (using String - see #350) - unsigned m_line; // the line where the test was registered - const char* m_name; // name of the test case - const char* m_test_suite; // the test suite in which the test was added - const char* m_description; - bool m_skip; - bool m_no_breaks; - bool m_no_output; - bool m_may_fail; - bool m_should_fail; - int m_expected_failures; - double m_timeout; -}; - -struct DOCTEST_INTERFACE AssertData -{ - // common - for all asserts - const TestCaseData* m_test_case; - assertType::Enum m_at; - const char* m_file; - int m_line; - const char* m_expr; - bool m_failed; - - // exception-related - for all asserts - bool m_threw; - String m_exception; - - // for normal asserts - String m_decomp; - - // for specific exception-related asserts - bool m_threw_as; - const char* m_exception_type; - const char* m_exception_string; -}; - -struct DOCTEST_INTERFACE MessageData -{ - String m_string; - const char* m_file; - int m_line; - assertType::Enum m_severity; -}; - -struct DOCTEST_INTERFACE SubcaseSignature -{ - String m_name; - const char* m_file; - int m_line; - - bool operator<(const SubcaseSignature& other) const; -}; - -struct DOCTEST_INTERFACE IContextScope -{ - IContextScope(); - virtual ~IContextScope(); - virtual void stringify(std::ostream*) const = 0; -}; - -namespace detail { - struct DOCTEST_INTERFACE TestCase; -} // namespace detail - -struct ContextOptions //!OCLINT too many fields -{ - std::ostream* cout = nullptr; // stdout stream - String binary_name; // the test binary name - - const detail::TestCase* currentTest = nullptr; - - // == parameters from the command line - String out; // output filename - String order_by; // how tests should be ordered - unsigned rand_seed; // the seed for rand ordering - - unsigned first; // the first (matching) test to be executed - unsigned last; // the last (matching) test to be executed - - int abort_after; // stop tests after this many failed assertions - int subcase_filter_levels; // apply the subcase filters for the first N levels - - bool success; // include successful assertions in output - bool case_sensitive; // if filtering should be case sensitive - bool exit; // if the program should be exited after the tests are ran/whatever - bool duration; // print the time duration of each test case - bool minimal; // minimal console output (only test failures) - bool quiet; // no console output - bool no_throw; // to skip exceptions-related assertion macros - bool no_exitcode; // if the framework should return 0 as the exitcode - bool no_run; // to not run the tests at all (can be done with an "*" exclude) - bool no_intro; // to not print the intro of the framework - bool no_version; // to not print the version of the framework - bool no_colors; // if output to the console should be colorized - bool force_colors; // forces the use of colors even when a tty cannot be detected - bool no_breaks; // to not break into the debugger - bool no_skip; // don't skip test cases which are marked to be skipped - bool gnu_file_line; // if line numbers should be surrounded with :x: and not (x): - bool no_path_in_filenames; // if the path to files should be removed from the output - bool no_line_numbers; // if source code line numbers should be omitted from the output - bool no_debug_output; // no output in the debug console when a debugger is attached - bool no_skipped_summary; // don't print "skipped" in the summary !!! UNDOCUMENTED !!! - bool no_time_in_output; // omit any time/timestamps from output !!! UNDOCUMENTED !!! - - bool help; // to print the help - bool version; // to print the version - bool count; // if only the count of matching tests is to be retrieved - bool list_test_cases; // to list all tests matching the filters - bool list_test_suites; // to list all suites matching the filters - bool list_reporters; // lists all registered reporters -}; - -namespace detail { - template - struct enable_if - {}; - - template - struct enable_if - { typedef TYPE type; }; - - // clang-format off - template struct remove_reference { typedef T type; }; - template struct remove_reference { typedef T type; }; - template struct remove_reference { typedef T type; }; - - template U declval(int); - - template T declval(long); - - template auto declval() DOCTEST_NOEXCEPT -> decltype(declval(0)) ; - - template struct is_lvalue_reference { const static bool value=false; }; - template struct is_lvalue_reference { const static bool value=true; }; - - template struct is_rvalue_reference { const static bool value=false; }; - template struct is_rvalue_reference { const static bool value=true; }; - - template - inline T&& forward(typename remove_reference::type& t) DOCTEST_NOEXCEPT - { - return static_cast(t); - } - - template - inline T&& forward(typename remove_reference::type&& t) DOCTEST_NOEXCEPT - { - static_assert(!is_lvalue_reference::value, - "Can not forward an rvalue as an lvalue."); - return static_cast(t); - } - - template struct remove_const { typedef T type; }; - template struct remove_const { typedef T type; }; -#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS - template struct is_enum : public std::is_enum {}; - template struct underlying_type : public std::underlying_type {}; -#else - // Use compiler intrinsics - template struct is_enum { DOCTEST_CONSTEXPR static bool value = __is_enum(T); }; - template struct underlying_type { typedef __underlying_type(T) type; }; -#endif - // clang-format on - - template - struct deferred_false - // cppcheck-suppress unusedStructMember - { static const bool value = false; }; - - namespace has_insertion_operator_impl { - std::ostream &os(); - template - DOCTEST_REF_WRAP(T) val(); - - template - struct check { - static DOCTEST_CONSTEXPR bool value = false; - }; - - template - struct check(), void())> { - static DOCTEST_CONSTEXPR bool value = true; - }; - } // namespace has_insertion_operator_impl - - template - using has_insertion_operator = has_insertion_operator_impl::check; - - DOCTEST_INTERFACE std::ostream* tlssPush(); - DOCTEST_INTERFACE String tlssPop(); - - - template - struct StringMakerBase - { - template - static String convert(const DOCTEST_REF_WRAP(T)) { - return "{?}"; - } - }; - - // Vector and various type other than pointer or array. - template - struct filldata - { - static void fill(std::ostream* stream, const T &in) { - *stream << in; - } - }; - - template - struct filldata - { - static void fill(std::ostream* stream, const T (&in)[N]) { - for (unsigned long i = 0; i < N; i++) { - *stream << in[i]; - } - } - }; - - // Specialized since we don't want the terminating null byte! - template - struct filldata - { - static void fill(std::ostream* stream, const char(&in)[N]) { - *stream << in; - } - }; - - template - void filloss(std::ostream* stream, const T& in) { - filldata::fill(stream, in); - } - - template - void filloss(std::ostream* stream, const T (&in)[N]) { - // T[N], T(&)[N], T(&&)[N] have same behaviour. - // Hence remove reference. - filldata::type>::fill(stream, in); - } - - template <> - struct StringMakerBase - { - template - static String convert(const DOCTEST_REF_WRAP(T) in) { - /* When parameter "in" is a null terminated const char* it works. - * When parameter "in" is a T arr[N] without '\0' we can fill the - * stringstream with N objects (T=char).If in is char pointer * - * without '\0' , it would cause segfault - * stepping over unaccessible memory. - */ - - std::ostream* stream = tlssPush(); - filloss(stream, in); - return tlssPop(); - } - }; - - DOCTEST_INTERFACE String rawMemoryToString(const void* object, unsigned size); - - template - String rawMemoryToString(const DOCTEST_REF_WRAP(T) object) { - return rawMemoryToString(&object, sizeof(object)); - } - - template - const char* type_to_string() { - return "<>"; - } -} // namespace detail - -template -struct StringMaker : public detail::StringMakerBase::value> -{}; - -template -struct StringMaker -{ - template - static String convert(U* p) { - if(p) - return detail::rawMemoryToString(p); - return "NULL"; - } -}; - -template -struct StringMaker -{ - static String convert(R C::*p) { - if(p) - return detail::rawMemoryToString(p); - return "NULL"; - } -}; - -template ::value, bool>::type = true> -String toString(const DOCTEST_REF_WRAP(T) value) { - return StringMaker::convert(value); -} - -#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING -DOCTEST_INTERFACE String toString(char* in); -DOCTEST_INTERFACE String toString(const char* in); -#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING -DOCTEST_INTERFACE String toString(bool in); -DOCTEST_INTERFACE String toString(float in); -DOCTEST_INTERFACE String toString(double in); -DOCTEST_INTERFACE String toString(double long in); - -DOCTEST_INTERFACE String toString(char in); -DOCTEST_INTERFACE String toString(char signed in); -DOCTEST_INTERFACE String toString(char unsigned in); -DOCTEST_INTERFACE String toString(int short in); -DOCTEST_INTERFACE String toString(int short unsigned in); -DOCTEST_INTERFACE String toString(int in); -DOCTEST_INTERFACE String toString(int unsigned in); -DOCTEST_INTERFACE String toString(int long in); -DOCTEST_INTERFACE String toString(int long unsigned in); -DOCTEST_INTERFACE String toString(int long long in); -DOCTEST_INTERFACE String toString(int long long unsigned in); -DOCTEST_INTERFACE String toString(std::nullptr_t in); - -template ::value, bool>::type = true> -String toString(const DOCTEST_REF_WRAP(T) value) { - typedef typename detail::underlying_type::type UT; - return toString(static_cast(value)); -} - -#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) -// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 -DOCTEST_INTERFACE String toString(const std::string& in); -#endif // VS 2019 - -class DOCTEST_INTERFACE Approx -{ -public: - explicit Approx(double value); - - Approx operator()(double value) const; - -#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS - template - explicit Approx(const T& value, - typename detail::enable_if::value>::type* = - static_cast(nullptr)) { - *this = Approx(static_cast(value)); - } -#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS - - Approx& epsilon(double newEpsilon); - -#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS - template - typename detail::enable_if::value, Approx&>::type epsilon( - const T& newEpsilon) { - m_epsilon = static_cast(newEpsilon); - return *this; - } -#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS - - Approx& scale(double newScale); - -#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS - template - typename detail::enable_if::value, Approx&>::type scale( - const T& newScale) { - m_scale = static_cast(newScale); - return *this; - } -#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS - - // clang-format off - DOCTEST_INTERFACE friend bool operator==(double lhs, const Approx & rhs); - DOCTEST_INTERFACE friend bool operator==(const Approx & lhs, double rhs); - DOCTEST_INTERFACE friend bool operator!=(double lhs, const Approx & rhs); - DOCTEST_INTERFACE friend bool operator!=(const Approx & lhs, double rhs); - DOCTEST_INTERFACE friend bool operator<=(double lhs, const Approx & rhs); - DOCTEST_INTERFACE friend bool operator<=(const Approx & lhs, double rhs); - DOCTEST_INTERFACE friend bool operator>=(double lhs, const Approx & rhs); - DOCTEST_INTERFACE friend bool operator>=(const Approx & lhs, double rhs); - DOCTEST_INTERFACE friend bool operator< (double lhs, const Approx & rhs); - DOCTEST_INTERFACE friend bool operator< (const Approx & lhs, double rhs); - DOCTEST_INTERFACE friend bool operator> (double lhs, const Approx & rhs); - DOCTEST_INTERFACE friend bool operator> (const Approx & lhs, double rhs); - - DOCTEST_INTERFACE friend String toString(const Approx& in); - -#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS -#define DOCTEST_APPROX_PREFIX \ - template friend typename detail::enable_if::value, bool>::type - - DOCTEST_APPROX_PREFIX operator==(const T& lhs, const Approx& rhs) { return operator==(double(lhs), rhs); } - DOCTEST_APPROX_PREFIX operator==(const Approx& lhs, const T& rhs) { return operator==(rhs, lhs); } - DOCTEST_APPROX_PREFIX operator!=(const T& lhs, const Approx& rhs) { return !operator==(lhs, rhs); } - DOCTEST_APPROX_PREFIX operator!=(const Approx& lhs, const T& rhs) { return !operator==(rhs, lhs); } - DOCTEST_APPROX_PREFIX operator<=(const T& lhs, const Approx& rhs) { return double(lhs) < rhs.m_value || lhs == rhs; } - DOCTEST_APPROX_PREFIX operator<=(const Approx& lhs, const T& rhs) { return lhs.m_value < double(rhs) || lhs == rhs; } - DOCTEST_APPROX_PREFIX operator>=(const T& lhs, const Approx& rhs) { return double(lhs) > rhs.m_value || lhs == rhs; } - DOCTEST_APPROX_PREFIX operator>=(const Approx& lhs, const T& rhs) { return lhs.m_value > double(rhs) || lhs == rhs; } - DOCTEST_APPROX_PREFIX operator< (const T& lhs, const Approx& rhs) { return double(lhs) < rhs.m_value && lhs != rhs; } - DOCTEST_APPROX_PREFIX operator< (const Approx& lhs, const T& rhs) { return lhs.m_value < double(rhs) && lhs != rhs; } - DOCTEST_APPROX_PREFIX operator> (const T& lhs, const Approx& rhs) { return double(lhs) > rhs.m_value && lhs != rhs; } - DOCTEST_APPROX_PREFIX operator> (const Approx& lhs, const T& rhs) { return lhs.m_value > double(rhs) && lhs != rhs; } -#undef DOCTEST_APPROX_PREFIX -#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS - - // clang-format on - -private: - double m_epsilon; - double m_scale; - double m_value; -}; - -DOCTEST_INTERFACE String toString(const Approx& in); - -DOCTEST_INTERFACE const ContextOptions* getContextOptions(); - -#if !defined(DOCTEST_CONFIG_DISABLE) - -namespace detail { - // clang-format off -#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING - template struct decay_array { typedef T type; }; - template struct decay_array { typedef T* type; }; - template struct decay_array { typedef T* type; }; - - template struct not_char_pointer { enum { value = 1 }; }; - template<> struct not_char_pointer { enum { value = 0 }; }; - template<> struct not_char_pointer { enum { value = 0 }; }; - - template struct can_use_op : public not_char_pointer::type> {}; -#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING - // clang-format on - - struct DOCTEST_INTERFACE TestFailureException - { - }; - - DOCTEST_INTERFACE bool checkIfShouldThrow(assertType::Enum at); - -#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS - DOCTEST_NORETURN -#endif // DOCTEST_CONFIG_NO_EXCEPTIONS - DOCTEST_INTERFACE void throwException(); - - struct DOCTEST_INTERFACE Subcase - { - SubcaseSignature m_signature; - bool m_entered = false; - - Subcase(const String& name, const char* file, int line); - ~Subcase(); - - operator bool() const; - }; - - template - String stringifyBinaryExpr(const DOCTEST_REF_WRAP(L) lhs, const char* op, - const DOCTEST_REF_WRAP(R) rhs) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) - return toString(lhs) + op + toString(rhs); - } - -#if DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 6, 0) -DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-comparison") -#endif - -// This will check if there is any way it could find a operator like member or friend and uses it. -// If not it doesn't find the operator or if the operator at global scope is defined after -// this template, the template won't be instantiated due to SFINAE. Once the template is not -// instantiated it can look for global operator using normal conversions. -#define SFINAE_OP(ret,op) decltype((void)(doctest::detail::declval() op doctest::detail::declval()),ret{}) - -#define DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(op, op_str, op_macro) \ - template \ - DOCTEST_NOINLINE SFINAE_OP(Result,op) operator op(const R&& rhs) { \ - bool res = op_macro(doctest::detail::forward(lhs), doctest::detail::forward(rhs)); \ - if(m_at & assertType::is_false) \ - res = !res; \ - if(!res || doctest::getContextOptions()->success) \ - return Result(res, stringifyBinaryExpr(lhs, op_str, rhs)); \ - return Result(res); \ - } \ - template ::value, void >::type* = nullptr> \ - DOCTEST_NOINLINE SFINAE_OP(Result,op) operator op(const R& rhs) { \ - bool res = op_macro(doctest::detail::forward(lhs), rhs); \ - if(m_at & assertType::is_false) \ - res = !res; \ - if(!res || doctest::getContextOptions()->success) \ - return Result(res, stringifyBinaryExpr(lhs, op_str, rhs)); \ - return Result(res); \ - } - - // more checks could be added - like in Catch: - // https://github.com/catchorg/Catch2/pull/1480/files - // https://github.com/catchorg/Catch2/pull/1481/files -#define DOCTEST_FORBIT_EXPRESSION(rt, op) \ - template \ - rt& operator op(const R&) { \ - static_assert(deferred_false::value, \ - "Expression Too Complex Please Rewrite As Binary Comparison!"); \ - return *this; \ - } - - struct DOCTEST_INTERFACE Result - { - bool m_passed; - String m_decomp; - - Result() = default; - Result(bool passed, const String& decomposition = String()); - - // forbidding some expressions based on this table: https://en.cppreference.com/w/cpp/language/operator_precedence - DOCTEST_FORBIT_EXPRESSION(Result, &) - DOCTEST_FORBIT_EXPRESSION(Result, ^) - DOCTEST_FORBIT_EXPRESSION(Result, |) - DOCTEST_FORBIT_EXPRESSION(Result, &&) - DOCTEST_FORBIT_EXPRESSION(Result, ||) - DOCTEST_FORBIT_EXPRESSION(Result, ==) - DOCTEST_FORBIT_EXPRESSION(Result, !=) - DOCTEST_FORBIT_EXPRESSION(Result, <) - DOCTEST_FORBIT_EXPRESSION(Result, >) - DOCTEST_FORBIT_EXPRESSION(Result, <=) - DOCTEST_FORBIT_EXPRESSION(Result, >=) - DOCTEST_FORBIT_EXPRESSION(Result, =) - DOCTEST_FORBIT_EXPRESSION(Result, +=) - DOCTEST_FORBIT_EXPRESSION(Result, -=) - DOCTEST_FORBIT_EXPRESSION(Result, *=) - DOCTEST_FORBIT_EXPRESSION(Result, /=) - DOCTEST_FORBIT_EXPRESSION(Result, %=) - DOCTEST_FORBIT_EXPRESSION(Result, <<=) - DOCTEST_FORBIT_EXPRESSION(Result, >>=) - DOCTEST_FORBIT_EXPRESSION(Result, &=) - DOCTEST_FORBIT_EXPRESSION(Result, ^=) - DOCTEST_FORBIT_EXPRESSION(Result, |=) - }; - -#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION - - DOCTEST_CLANG_SUPPRESS_WARNING_PUSH - DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion") - DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-compare") - //DOCTEST_CLANG_SUPPRESS_WARNING("-Wdouble-promotion") - //DOCTEST_CLANG_SUPPRESS_WARNING("-Wconversion") - //DOCTEST_CLANG_SUPPRESS_WARNING("-Wfloat-equal") - - DOCTEST_GCC_SUPPRESS_WARNING_PUSH - DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion") - DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-compare") - //DOCTEST_GCC_SUPPRESS_WARNING("-Wdouble-promotion") - //DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion") - //DOCTEST_GCC_SUPPRESS_WARNING("-Wfloat-equal") - - DOCTEST_MSVC_SUPPRESS_WARNING_PUSH - // https://stackoverflow.com/questions/39479163 what's the difference between 4018 and 4389 - DOCTEST_MSVC_SUPPRESS_WARNING(4388) // signed/unsigned mismatch - DOCTEST_MSVC_SUPPRESS_WARNING(4389) // 'operator' : signed/unsigned mismatch - DOCTEST_MSVC_SUPPRESS_WARNING(4018) // 'expression' : signed/unsigned mismatch - //DOCTEST_MSVC_SUPPRESS_WARNING(4805) // 'operation' : unsafe mix of type 'type' and type 'type' in operation - -#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION - - // clang-format off -#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING -#define DOCTEST_COMPARISON_RETURN_TYPE bool -#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING -#define DOCTEST_COMPARISON_RETURN_TYPE typename enable_if::value || can_use_op::value, bool>::type - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) - inline bool eq(const char* lhs, const char* rhs) { return String(lhs) == String(rhs); } - inline bool ne(const char* lhs, const char* rhs) { return String(lhs) != String(rhs); } - inline bool lt(const char* lhs, const char* rhs) { return String(lhs) < String(rhs); } - inline bool gt(const char* lhs, const char* rhs) { return String(lhs) > String(rhs); } - inline bool le(const char* lhs, const char* rhs) { return String(lhs) <= String(rhs); } - inline bool ge(const char* lhs, const char* rhs) { return String(lhs) >= String(rhs); } -#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING - // clang-format on - -#define DOCTEST_RELATIONAL_OP(name, op) \ - template \ - DOCTEST_COMPARISON_RETURN_TYPE name(const DOCTEST_REF_WRAP(L) lhs, \ - const DOCTEST_REF_WRAP(R) rhs) { \ - return lhs op rhs; \ - } - - DOCTEST_RELATIONAL_OP(eq, ==) - DOCTEST_RELATIONAL_OP(ne, !=) - DOCTEST_RELATIONAL_OP(lt, <) - DOCTEST_RELATIONAL_OP(gt, >) - DOCTEST_RELATIONAL_OP(le, <=) - DOCTEST_RELATIONAL_OP(ge, >=) - -#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING -#define DOCTEST_CMP_EQ(l, r) l == r -#define DOCTEST_CMP_NE(l, r) l != r -#define DOCTEST_CMP_GT(l, r) l > r -#define DOCTEST_CMP_LT(l, r) l < r -#define DOCTEST_CMP_GE(l, r) l >= r -#define DOCTEST_CMP_LE(l, r) l <= r -#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING -#define DOCTEST_CMP_EQ(l, r) eq(l, r) -#define DOCTEST_CMP_NE(l, r) ne(l, r) -#define DOCTEST_CMP_GT(l, r) gt(l, r) -#define DOCTEST_CMP_LT(l, r) lt(l, r) -#define DOCTEST_CMP_GE(l, r) ge(l, r) -#define DOCTEST_CMP_LE(l, r) le(l, r) -#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING - - template - // cppcheck-suppress copyCtorAndEqOperator - struct Expression_lhs - { - L lhs; - assertType::Enum m_at; - - explicit Expression_lhs(L&& in, assertType::Enum at) - : lhs(doctest::detail::forward(in)) - , m_at(at) {} - - DOCTEST_NOINLINE operator Result() { -// this is needed only for MSVC 2015 -DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4800) // 'int': forcing value to bool - bool res = static_cast(lhs); -DOCTEST_MSVC_SUPPRESS_WARNING_POP - if(m_at & assertType::is_false) //!OCLINT bitwise operator in conditional - res = !res; - - if(!res || getContextOptions()->success) - return Result(res, toString(lhs)); - return Result(res); - } - - /* This is required for user-defined conversions from Expression_lhs to L */ - operator L() const { return lhs; } - - // clang-format off - DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(==, " == ", DOCTEST_CMP_EQ) //!OCLINT bitwise operator in conditional - DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(!=, " != ", DOCTEST_CMP_NE) //!OCLINT bitwise operator in conditional - DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>, " > ", DOCTEST_CMP_GT) //!OCLINT bitwise operator in conditional - DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<, " < ", DOCTEST_CMP_LT) //!OCLINT bitwise operator in conditional - DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>=, " >= ", DOCTEST_CMP_GE) //!OCLINT bitwise operator in conditional - DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<=, " <= ", DOCTEST_CMP_LE) //!OCLINT bitwise operator in conditional - // clang-format on - - // forbidding some expressions based on this table: https://en.cppreference.com/w/cpp/language/operator_precedence - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &) - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^) - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |) - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &&) - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ||) - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, =) - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, +=) - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, -=) - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, *=) - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, /=) - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, %=) - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<=) - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>=) - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &=) - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^=) - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |=) - // these 2 are unfortunate because they should be allowed - they have higher precedence over the comparisons, but the - // ExpressionDecomposer class uses the left shift operator to capture the left operand of the binary expression... - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<) - DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>) - }; - -#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION - - DOCTEST_CLANG_SUPPRESS_WARNING_POP - DOCTEST_MSVC_SUPPRESS_WARNING_POP - DOCTEST_GCC_SUPPRESS_WARNING_POP - -#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION - -#if DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 6, 0) -DOCTEST_CLANG_SUPPRESS_WARNING_POP -#endif - - struct DOCTEST_INTERFACE ExpressionDecomposer - { - assertType::Enum m_at; - - ExpressionDecomposer(assertType::Enum at); - - // The right operator for capturing expressions is "<=" instead of "<<" (based on the operator precedence table) - // but then there will be warnings from GCC about "-Wparentheses" and since "_Pragma()" is problematic this will stay for now... - // https://github.com/catchorg/Catch2/issues/870 - // https://github.com/catchorg/Catch2/issues/565 - template - Expression_lhs operator<<(const L &&operand) { - return Expression_lhs(doctest::detail::forward(operand), m_at); - } - - template ::value,void >::type* = nullptr> - Expression_lhs operator<<(const L &operand) { - return Expression_lhs(operand, m_at); - } - }; - - struct DOCTEST_INTERFACE TestSuite - { - const char* m_test_suite = nullptr; - const char* m_description = nullptr; - bool m_skip = false; - bool m_no_breaks = false; - bool m_no_output = false; - bool m_may_fail = false; - bool m_should_fail = false; - int m_expected_failures = 0; - double m_timeout = 0; - - TestSuite& operator*(const char* in); - - template - TestSuite& operator*(const T& in) { - in.fill(*this); - return *this; - } - }; - - typedef void (*funcType)(); - - struct DOCTEST_INTERFACE TestCase : public TestCaseData - { - funcType m_test; // a function pointer to the test case - - const char* m_type; // for templated test cases - gets appended to the real name - int m_template_id; // an ID used to distinguish between the different versions of a templated test case - String m_full_name; // contains the name (only for templated test cases!) + the template type - - TestCase(funcType test, const char* file, unsigned line, const TestSuite& test_suite, - const char* type = "", int template_id = -1); - - TestCase(const TestCase& other); - - DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function - TestCase& operator=(const TestCase& other); - DOCTEST_MSVC_SUPPRESS_WARNING_POP - - TestCase& operator*(const char* in); - - template - TestCase& operator*(const T& in) { - in.fill(*this); - return *this; - } - - bool operator<(const TestCase& other) const; - }; - - // forward declarations of functions used by the macros - DOCTEST_INTERFACE int regTest(const TestCase& tc); - DOCTEST_INTERFACE int setTestSuite(const TestSuite& ts); - DOCTEST_INTERFACE bool isDebuggerActive(); - - template - int instantiationHelper(const T&) { return 0; } - - namespace binaryAssertComparison { - enum Enum - { - eq = 0, - ne, - gt, - lt, - ge, - le - }; - } // namespace binaryAssertComparison - - // clang-format off - template struct RelationalComparator { bool operator()(const DOCTEST_REF_WRAP(L), const DOCTEST_REF_WRAP(R) ) const { return false; } }; - -#define DOCTEST_BINARY_RELATIONAL_OP(n, op) \ - template struct RelationalComparator { bool operator()(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) const { return op(lhs, rhs); } }; - // clang-format on - - DOCTEST_BINARY_RELATIONAL_OP(0, doctest::detail::eq) - DOCTEST_BINARY_RELATIONAL_OP(1, doctest::detail::ne) - DOCTEST_BINARY_RELATIONAL_OP(2, doctest::detail::gt) - DOCTEST_BINARY_RELATIONAL_OP(3, doctest::detail::lt) - DOCTEST_BINARY_RELATIONAL_OP(4, doctest::detail::ge) - DOCTEST_BINARY_RELATIONAL_OP(5, doctest::detail::le) - - struct DOCTEST_INTERFACE ResultBuilder : public AssertData - { - ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr, - const char* exception_type = "", const char* exception_string = ""); - - void setResult(const Result& res); - - template - DOCTEST_NOINLINE bool binary_assert(const DOCTEST_REF_WRAP(L) lhs, - const DOCTEST_REF_WRAP(R) rhs) { - m_failed = !RelationalComparator()(lhs, rhs); - if(m_failed || getContextOptions()->success) - m_decomp = stringifyBinaryExpr(lhs, ", ", rhs); - return !m_failed; - } - - template - DOCTEST_NOINLINE bool unary_assert(const DOCTEST_REF_WRAP(L) val) { - m_failed = !val; - - if(m_at & assertType::is_false) //!OCLINT bitwise operator in conditional - m_failed = !m_failed; - - if(m_failed || getContextOptions()->success) - m_decomp = toString(val); - - return !m_failed; - } - - void translateException(); - - bool log(); - void react() const; - }; - - namespace assertAction { - enum Enum - { - nothing = 0, - dbgbreak = 1, - shouldthrow = 2 - }; - } // namespace assertAction - - DOCTEST_INTERFACE void failed_out_of_a_testing_context(const AssertData& ad); - - DOCTEST_INTERFACE bool decomp_assert(assertType::Enum at, const char* file, int line, - const char* expr, Result result); - -#define DOCTEST_ASSERT_OUT_OF_TESTS(decomp) \ - do { \ - if(!is_running_in_test) { \ - if(failed) { \ - ResultBuilder rb(at, file, line, expr); \ - rb.m_failed = failed; \ - rb.m_decomp = decomp; \ - failed_out_of_a_testing_context(rb); \ - if(isDebuggerActive() && !getContextOptions()->no_breaks) \ - DOCTEST_BREAK_INTO_DEBUGGER(); \ - if(checkIfShouldThrow(at)) \ - throwException(); \ - } \ - return !failed; \ - } \ - } while(false) - -#define DOCTEST_ASSERT_IN_TESTS(decomp) \ - ResultBuilder rb(at, file, line, expr); \ - rb.m_failed = failed; \ - if(rb.m_failed || getContextOptions()->success) \ - rb.m_decomp = decomp; \ - if(rb.log()) \ - DOCTEST_BREAK_INTO_DEBUGGER(); \ - if(rb.m_failed && checkIfShouldThrow(at)) \ - throwException() - - template - DOCTEST_NOINLINE bool binary_assert(assertType::Enum at, const char* file, int line, - const char* expr, const DOCTEST_REF_WRAP(L) lhs, - const DOCTEST_REF_WRAP(R) rhs) { - bool failed = !RelationalComparator()(lhs, rhs); - - // ################################################################################### - // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT - // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED - // ################################################################################### - DOCTEST_ASSERT_OUT_OF_TESTS(stringifyBinaryExpr(lhs, ", ", rhs)); - DOCTEST_ASSERT_IN_TESTS(stringifyBinaryExpr(lhs, ", ", rhs)); - return !failed; - } - - template - DOCTEST_NOINLINE bool unary_assert(assertType::Enum at, const char* file, int line, - const char* expr, const DOCTEST_REF_WRAP(L) val) { - bool failed = !val; - - if(at & assertType::is_false) //!OCLINT bitwise operator in conditional - failed = !failed; - - // ################################################################################### - // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT - // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED - // ################################################################################### - DOCTEST_ASSERT_OUT_OF_TESTS(toString(val)); - DOCTEST_ASSERT_IN_TESTS(toString(val)); - return !failed; - } - - struct DOCTEST_INTERFACE IExceptionTranslator - { - IExceptionTranslator(); - virtual ~IExceptionTranslator(); - virtual bool translate(String&) const = 0; - }; - - template - class ExceptionTranslator : public IExceptionTranslator //!OCLINT destructor of virtual class - { - public: - explicit ExceptionTranslator(String (*translateFunction)(T)) - : m_translateFunction(translateFunction) {} - - bool translate(String& res) const override { -#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS - try { - throw; // lgtm [cpp/rethrow-no-exception] - // cppcheck-suppress catchExceptionByValue - } catch(T ex) { // NOLINT - res = m_translateFunction(ex); //!OCLINT parameter reassignment - return true; - } catch(...) {} //!OCLINT - empty catch statement -#endif // DOCTEST_CONFIG_NO_EXCEPTIONS - static_cast(res); // to silence -Wunused-parameter - return false; - } - - private: - String (*m_translateFunction)(T); - }; - - DOCTEST_INTERFACE void registerExceptionTranslatorImpl(const IExceptionTranslator* et); - - template - struct StringStreamBase - { - template - static void convert(std::ostream* s, const T& in) { - *s << toString(in); - } - - // always treat char* as a string in this context - no matter - // if DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING is defined - static void convert(std::ostream* s, const char* in) { *s << String(in); } - }; - - template <> - struct StringStreamBase - { - template - static void convert(std::ostream* s, const T& in) { - *s << in; - } - }; - - template - struct StringStream : public StringStreamBase::value> - {}; - - template - void toStream(std::ostream* s, const T& value) { - StringStream::convert(s, value); - } - -#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING - DOCTEST_INTERFACE void toStream(std::ostream* s, char* in); - DOCTEST_INTERFACE void toStream(std::ostream* s, const char* in); -#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING - DOCTEST_INTERFACE void toStream(std::ostream* s, bool in); - DOCTEST_INTERFACE void toStream(std::ostream* s, float in); - DOCTEST_INTERFACE void toStream(std::ostream* s, double in); - DOCTEST_INTERFACE void toStream(std::ostream* s, double long in); - - DOCTEST_INTERFACE void toStream(std::ostream* s, char in); - DOCTEST_INTERFACE void toStream(std::ostream* s, char signed in); - DOCTEST_INTERFACE void toStream(std::ostream* s, char unsigned in); - DOCTEST_INTERFACE void toStream(std::ostream* s, int short in); - DOCTEST_INTERFACE void toStream(std::ostream* s, int short unsigned in); - DOCTEST_INTERFACE void toStream(std::ostream* s, int in); - DOCTEST_INTERFACE void toStream(std::ostream* s, int unsigned in); - DOCTEST_INTERFACE void toStream(std::ostream* s, int long in); - DOCTEST_INTERFACE void toStream(std::ostream* s, int long unsigned in); - DOCTEST_INTERFACE void toStream(std::ostream* s, int long long in); - DOCTEST_INTERFACE void toStream(std::ostream* s, int long long unsigned in); - - // ContextScope base class used to allow implementing methods of ContextScope - // that don't depend on the template parameter in doctest.cpp. - class DOCTEST_INTERFACE ContextScopeBase : public IContextScope { - protected: - ContextScopeBase(); - ContextScopeBase(ContextScopeBase&& other); - - void destroy(); - bool need_to_destroy{true}; - }; - - template class ContextScope : public ContextScopeBase - { - const L lambda_; - - public: - explicit ContextScope(const L &lambda) : lambda_(lambda) {} - - ContextScope(ContextScope &&other) : ContextScopeBase(static_cast(other)), lambda_(other.lambda_) {} - - void stringify(std::ostream* s) const override { lambda_(s); } - - ~ContextScope() override { - if (need_to_destroy) { - destroy(); - } - } - }; - - struct DOCTEST_INTERFACE MessageBuilder : public MessageData - { - std::ostream* m_stream; - bool logged = false; - - MessageBuilder(const char* file, int line, assertType::Enum severity); - MessageBuilder() = delete; - ~MessageBuilder(); - - // the preferred way of chaining parameters for stringification - template - MessageBuilder& operator,(const T& in) { - toStream(m_stream, in); - return *this; - } - - // kept here just for backwards-compatibility - the comma operator should be preferred now - template - MessageBuilder& operator<<(const T& in) { return this->operator,(in); } - - // the `,` operator has the lowest operator precedence - if `<<` is used by the user then - // the `,` operator will be called last which is not what we want and thus the `*` operator - // is used first (has higher operator precedence compared to `<<`) so that we guarantee that - // an operator of the MessageBuilder class is called first before the rest of the parameters - template - MessageBuilder& operator*(const T& in) { return this->operator,(in); } - - bool log(); - void react(); - }; - - template - ContextScope MakeContextScope(const L &lambda) { - return ContextScope(lambda); - } -} // namespace detail - -#define DOCTEST_DEFINE_DECORATOR(name, type, def) \ - struct name \ - { \ - type data; \ - name(type in = def) \ - : data(in) {} \ - void fill(detail::TestCase& state) const { state.DOCTEST_CAT(m_, name) = data; } \ - void fill(detail::TestSuite& state) const { state.DOCTEST_CAT(m_, name) = data; } \ - } - -DOCTEST_DEFINE_DECORATOR(test_suite, const char*, ""); -DOCTEST_DEFINE_DECORATOR(description, const char*, ""); -DOCTEST_DEFINE_DECORATOR(skip, bool, true); -DOCTEST_DEFINE_DECORATOR(no_breaks, bool, true); -DOCTEST_DEFINE_DECORATOR(no_output, bool, true); -DOCTEST_DEFINE_DECORATOR(timeout, double, 0); -DOCTEST_DEFINE_DECORATOR(may_fail, bool, true); -DOCTEST_DEFINE_DECORATOR(should_fail, bool, true); -DOCTEST_DEFINE_DECORATOR(expected_failures, int, 0); - -template -int registerExceptionTranslator(String (*translateFunction)(T)) { - DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors") - static detail::ExceptionTranslator exceptionTranslator(translateFunction); - DOCTEST_CLANG_SUPPRESS_WARNING_POP - detail::registerExceptionTranslatorImpl(&exceptionTranslator); - return 0; -} - -} // namespace doctest - -// in a separate namespace outside of doctest because the DOCTEST_TEST_SUITE macro -// introduces an anonymous namespace in which getCurrentTestSuite gets overridden -namespace doctest_detail_test_suite_ns { -DOCTEST_INTERFACE doctest::detail::TestSuite& getCurrentTestSuite(); -} // namespace doctest_detail_test_suite_ns - -namespace doctest { -#else // DOCTEST_CONFIG_DISABLE -template -int registerExceptionTranslator(String (*)(T)) { - return 0; -} -#endif // DOCTEST_CONFIG_DISABLE - -namespace detail { - typedef void (*assert_handler)(const AssertData&); - struct ContextState; -} // namespace detail - -class DOCTEST_INTERFACE Context -{ - detail::ContextState* p; - - void parseArgs(int argc, const char* const* argv, bool withDefaults = false); - -public: - explicit Context(int argc = 0, const char* const* argv = nullptr); - - ~Context(); - - void applyCommandLine(int argc, const char* const* argv); - - void addFilter(const char* filter, const char* value); - void clearFilters(); - void setOption(const char* option, bool value); - void setOption(const char* option, int value); - void setOption(const char* option, const char* value); - - bool shouldExit(); - - void setAsDefaultForAssertsOutOfTestCases(); - - void setAssertHandler(detail::assert_handler ah); - - void setCout(std::ostream* out); - - int run(); -}; - -namespace TestCaseFailureReason { - enum Enum - { - None = 0, - AssertFailure = 1, // an assertion has failed in the test case - Exception = 2, // test case threw an exception - Crash = 4, // a crash... - TooManyFailedAsserts = 8, // the abort-after option - Timeout = 16, // see the timeout decorator - ShouldHaveFailedButDidnt = 32, // see the should_fail decorator - ShouldHaveFailedAndDid = 64, // see the should_fail decorator - DidntFailExactlyNumTimes = 128, // see the expected_failures decorator - FailedExactlyNumTimes = 256, // see the expected_failures decorator - CouldHaveFailedAndDid = 512 // see the may_fail decorator - }; -} // namespace TestCaseFailureReason - -struct DOCTEST_INTERFACE CurrentTestCaseStats -{ - int numAssertsCurrentTest; - int numAssertsFailedCurrentTest; - double seconds; - int failure_flags; // use TestCaseFailureReason::Enum - bool testCaseSuccess; -}; - -struct DOCTEST_INTERFACE TestCaseException -{ - String error_string; - bool is_crash; -}; - -struct DOCTEST_INTERFACE TestRunStats -{ - unsigned numTestCases; - unsigned numTestCasesPassingFilters; - unsigned numTestSuitesPassingFilters; - unsigned numTestCasesFailed; - int numAsserts; - int numAssertsFailed; -}; - -struct QueryData -{ - const TestRunStats* run_stats = nullptr; - const TestCaseData** data = nullptr; - unsigned num_data = 0; -}; - -struct DOCTEST_INTERFACE IReporter -{ - // The constructor has to accept "const ContextOptions&" as a single argument - // which has most of the options for the run + a pointer to the stdout stream - // Reporter(const ContextOptions& in) - - // called when a query should be reported (listing test cases, printing the version, etc.) - virtual void report_query(const QueryData&) = 0; - - // called when the whole test run starts - virtual void test_run_start() = 0; - // called when the whole test run ends (caching a pointer to the input doesn't make sense here) - virtual void test_run_end(const TestRunStats&) = 0; - - // called when a test case is started (safe to cache a pointer to the input) - virtual void test_case_start(const TestCaseData&) = 0; - // called when a test case is reentered because of unfinished subcases (safe to cache a pointer to the input) - virtual void test_case_reenter(const TestCaseData&) = 0; - // called when a test case has ended - virtual void test_case_end(const CurrentTestCaseStats&) = 0; - - // called when an exception is thrown from the test case (or it crashes) - virtual void test_case_exception(const TestCaseException&) = 0; - - // called whenever a subcase is entered (don't cache pointers to the input) - virtual void subcase_start(const SubcaseSignature&) = 0; - // called whenever a subcase is exited (don't cache pointers to the input) - virtual void subcase_end() = 0; - - // called for each assert (don't cache pointers to the input) - virtual void log_assert(const AssertData&) = 0; - // called for each message (don't cache pointers to the input) - virtual void log_message(const MessageData&) = 0; - - // called when a test case is skipped either because it doesn't pass the filters, has a skip decorator - // or isn't in the execution range (between first and last) (safe to cache a pointer to the input) - virtual void test_case_skipped(const TestCaseData&) = 0; - - // doctest will not be managing the lifetimes of reporters given to it but this would still be nice to have - virtual ~IReporter(); - - // can obtain all currently active contexts and stringify them if one wishes to do so - static int get_num_active_contexts(); - static const IContextScope* const* get_active_contexts(); - - // can iterate through contexts which have been stringified automatically in their destructors when an exception has been thrown - static int get_num_stringified_contexts(); - static const String* get_stringified_contexts(); -}; - -namespace detail { - typedef IReporter* (*reporterCreatorFunc)(const ContextOptions&); - - DOCTEST_INTERFACE void registerReporterImpl(const char* name, int prio, reporterCreatorFunc c, bool isReporter); - - template - IReporter* reporterCreator(const ContextOptions& o) { - return new Reporter(o); - } -} // namespace detail - -template -int registerReporter(const char* name, int priority, bool isReporter) { - detail::registerReporterImpl(name, priority, detail::reporterCreator, isReporter); - return 0; -} -} // namespace doctest - -// if registering is not disabled -#if !defined(DOCTEST_CONFIG_DISABLE) - -// common code in asserts - for convenience -#define DOCTEST_ASSERT_LOG_REACT_RETURN(b) \ - if(b.log()) \ - DOCTEST_BREAK_INTO_DEBUGGER(); \ - b.react(); \ - return !b.m_failed - -#ifdef DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS -#define DOCTEST_WRAP_IN_TRY(x) x; -#else // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS -#define DOCTEST_WRAP_IN_TRY(x) \ - try { \ - x; \ - } catch(...) { DOCTEST_RB.translateException(); } -#endif // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS - -#ifdef DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS -#define DOCTEST_CAST_TO_VOID(...) \ - DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wuseless-cast") \ - static_cast(__VA_ARGS__); \ - DOCTEST_GCC_SUPPRESS_WARNING_POP -#else // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS -#define DOCTEST_CAST_TO_VOID(...) __VA_ARGS__; -#endif // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS - -// registers the test by initializing a dummy var with a function -#define DOCTEST_REGISTER_FUNCTION(global_prefix, f, decorators) \ - global_prefix DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), \ - doctest::detail::regTest( \ - doctest::detail::TestCase( \ - f, __FILE__, __LINE__, \ - doctest_detail_test_suite_ns::getCurrentTestSuite()) * \ - decorators)) - -#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, decorators) \ - namespace { \ - struct der : public base \ - { \ - void f(); \ - }; \ - static void func() { \ - der v; \ - v.f(); \ - } \ - DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, func, decorators) \ - } \ - inline DOCTEST_NOINLINE void der::f() - -#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, decorators) \ - static void f(); \ - DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, f, decorators) \ - static void f() - -#define DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS(f, proxy, decorators) \ - static doctest::detail::funcType proxy() { return f; } \ - DOCTEST_REGISTER_FUNCTION(inline, proxy(), decorators) \ - static void f() - -// for registering tests -#define DOCTEST_TEST_CASE(decorators) \ - DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), decorators) - -// for registering tests in classes - requires C++17 for inline variables! -#if __cplusplus >= 201703L || (DOCTEST_MSVC >= DOCTEST_COMPILER(19, 12, 0) && _MSVC_LANG >= 201703L) -#define DOCTEST_TEST_CASE_CLASS(decorators) \ - DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), \ - DOCTEST_ANONYMOUS(DOCTEST_ANON_PROXY_), \ - decorators) -#else // DOCTEST_TEST_CASE_CLASS -#define DOCTEST_TEST_CASE_CLASS(...) \ - TEST_CASES_CAN_BE_REGISTERED_IN_CLASSES_ONLY_IN_CPP17_MODE_OR_WITH_VS_2017_OR_NEWER -#endif // DOCTEST_TEST_CASE_CLASS - -// for registering tests with a fixture -#define DOCTEST_TEST_CASE_FIXTURE(c, decorators) \ - DOCTEST_IMPLEMENT_FIXTURE(DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), c, \ - DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), decorators) - -// for converting types to strings without the header and demangling -#define DOCTEST_TYPE_TO_STRING_IMPL(...) \ - template <> \ - inline const char* type_to_string<__VA_ARGS__>() { \ - return "<" #__VA_ARGS__ ">"; \ - } -#define DOCTEST_TYPE_TO_STRING(...) \ - namespace doctest { namespace detail { \ - DOCTEST_TYPE_TO_STRING_IMPL(__VA_ARGS__) \ - } \ - } \ - static_assert(true, "") - -#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, iter, func) \ - template \ - static void func(); \ - namespace { \ - template \ - struct iter; \ - template \ - struct iter> \ - { \ - iter(const char* file, unsigned line, int index) { \ - doctest::detail::regTest(doctest::detail::TestCase(func, file, line, \ - doctest_detail_test_suite_ns::getCurrentTestSuite(), \ - doctest::detail::type_to_string(), \ - int(line) * 1000 + index) \ - * dec); \ - iter>(file, line, index + 1); \ - } \ - }; \ - template <> \ - struct iter> \ - { \ - iter(const char*, unsigned, int) {} \ - }; \ - } \ - template \ - static void func() - -#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(dec, T, id) \ - DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(id, ITERATOR), \ - DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)) - -#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, anon, ...) \ - DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_CAT(anon, DUMMY), \ - doctest::detail::instantiationHelper( \ - DOCTEST_CAT(id, ITERATOR)<__VA_ARGS__>(__FILE__, __LINE__, 0))) - -#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) \ - DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), std::tuple<__VA_ARGS__>) \ - static_assert(true, "") - -#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) \ - DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), __VA_ARGS__) \ - static_assert(true, "") - -#define DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, anon, ...) \ - DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(anon, ITERATOR), anon); \ - DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(anon, anon, std::tuple<__VA_ARGS__>) \ - template \ - static void anon() - -#define DOCTEST_TEST_CASE_TEMPLATE(dec, T, ...) \ - DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), __VA_ARGS__) - -// for subcases -#define DOCTEST_SUBCASE(name) \ - if(const doctest::detail::Subcase & DOCTEST_ANONYMOUS(DOCTEST_ANON_SUBCASE_) DOCTEST_UNUSED = \ - doctest::detail::Subcase(name, __FILE__, __LINE__)) - -// for grouping tests in test suites by using code blocks -#define DOCTEST_TEST_SUITE_IMPL(decorators, ns_name) \ - namespace ns_name { namespace doctest_detail_test_suite_ns { \ - static DOCTEST_NOINLINE doctest::detail::TestSuite& getCurrentTestSuite() { \ - DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4640) \ - DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors") \ - DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wmissing-field-initializers") \ - static doctest::detail::TestSuite data{}; \ - static bool inited = false; \ - DOCTEST_MSVC_SUPPRESS_WARNING_POP \ - DOCTEST_CLANG_SUPPRESS_WARNING_POP \ - DOCTEST_GCC_SUPPRESS_WARNING_POP \ - if(!inited) { \ - data* decorators; \ - inited = true; \ - } \ - return data; \ - } \ - } \ - } \ - namespace ns_name - -#define DOCTEST_TEST_SUITE(decorators) \ - DOCTEST_TEST_SUITE_IMPL(decorators, DOCTEST_ANONYMOUS(DOCTEST_ANON_SUITE_)) - -// for starting a testsuite block -#define DOCTEST_TEST_SUITE_BEGIN(decorators) \ - DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), \ - doctest::detail::setTestSuite(doctest::detail::TestSuite() * decorators)) \ - static_assert(true, "") - -// for ending a testsuite block -#define DOCTEST_TEST_SUITE_END \ - DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), \ - doctest::detail::setTestSuite(doctest::detail::TestSuite() * "")) \ - typedef int DOCTEST_ANONYMOUS(DOCTEST_ANON_FOR_SEMICOLON_) - -// for registering exception translators -#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(translatorName, signature) \ - inline doctest::String translatorName(signature); \ - DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_), \ - doctest::registerExceptionTranslator(translatorName)) \ - doctest::String translatorName(signature) - -#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \ - DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_), \ - signature) - -// for registering reporters -#define DOCTEST_REGISTER_REPORTER(name, priority, reporter) \ - DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_REPORTER_), \ - doctest::registerReporter(name, priority, true)) \ - static_assert(true, "") - -// for registering listeners -#define DOCTEST_REGISTER_LISTENER(name, priority, reporter) \ - DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_REPORTER_), \ - doctest::registerReporter(name, priority, false)) \ - static_assert(true, "") - -// clang-format off -// for logging - disabling formatting because it's important to have these on 2 separate lines - see PR #557 -#define DOCTEST_INFO(...) \ - DOCTEST_INFO_IMPL(DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_), \ - DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_OTHER_), \ - __VA_ARGS__) -// clang-format on - -#define DOCTEST_INFO_IMPL(mb_name, s_name, ...) \ - auto DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_) = doctest::detail::MakeContextScope( \ - [&](std::ostream* s_name) { \ - doctest::detail::MessageBuilder mb_name(__FILE__, __LINE__, doctest::assertType::is_warn); \ - mb_name.m_stream = s_name; \ - mb_name * __VA_ARGS__; \ - }) - -#define DOCTEST_CAPTURE(x) DOCTEST_INFO(#x " := ", x) - -#define DOCTEST_ADD_AT_IMPL(type, file, line, mb, ...) \ - [&] { \ - doctest::detail::MessageBuilder mb(file, line, doctest::assertType::type); \ - mb * __VA_ARGS__; \ - if(mb.log()) \ - DOCTEST_BREAK_INTO_DEBUGGER(); \ - mb.react(); \ - }() - -// clang-format off -#define DOCTEST_ADD_MESSAGE_AT(file, line, ...) DOCTEST_ADD_AT_IMPL(is_warn, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) -#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, ...) DOCTEST_ADD_AT_IMPL(is_check, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) -#define DOCTEST_ADD_FAIL_AT(file, line, ...) DOCTEST_ADD_AT_IMPL(is_require, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) -// clang-format on - -#define DOCTEST_MESSAGE(...) DOCTEST_ADD_MESSAGE_AT(__FILE__, __LINE__, __VA_ARGS__) -#define DOCTEST_FAIL_CHECK(...) DOCTEST_ADD_FAIL_CHECK_AT(__FILE__, __LINE__, __VA_ARGS__) -#define DOCTEST_FAIL(...) DOCTEST_ADD_FAIL_AT(__FILE__, __LINE__, __VA_ARGS__) - -#define DOCTEST_TO_LVALUE(...) __VA_ARGS__ // Not removed to keep backwards compatibility. - -#ifndef DOCTEST_CONFIG_SUPER_FAST_ASSERTS - -#define DOCTEST_ASSERT_IMPLEMENT_2(assert_type, ...) \ - DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \ - doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ - __LINE__, #__VA_ARGS__); \ - DOCTEST_WRAP_IN_TRY(DOCTEST_RB.setResult( \ - doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) \ - << __VA_ARGS__)) \ - DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB) \ - DOCTEST_CLANG_SUPPRESS_WARNING_POP - -#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \ - [&] { \ - DOCTEST_ASSERT_IMPLEMENT_2(assert_type, __VA_ARGS__); \ - }() - -#else // DOCTEST_CONFIG_SUPER_FAST_ASSERTS - -// necessary for _MESSAGE -#define DOCTEST_ASSERT_IMPLEMENT_2 DOCTEST_ASSERT_IMPLEMENT_1 - -#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \ - DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \ - doctest::detail::decomp_assert( \ - doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, \ - doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) \ - << __VA_ARGS__) DOCTEST_CLANG_SUPPRESS_WARNING_POP - -#endif // DOCTEST_CONFIG_SUPER_FAST_ASSERTS - -#define DOCTEST_WARN(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN, __VA_ARGS__) -#define DOCTEST_CHECK(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK, __VA_ARGS__) -#define DOCTEST_REQUIRE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE, __VA_ARGS__) -#define DOCTEST_WARN_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN_FALSE, __VA_ARGS__) -#define DOCTEST_CHECK_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK_FALSE, __VA_ARGS__) -#define DOCTEST_REQUIRE_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE_FALSE, __VA_ARGS__) - -// clang-format off -#define DOCTEST_WARN_MESSAGE(cond, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN, cond); }() -#define DOCTEST_CHECK_MESSAGE(cond, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK, cond); }() -#define DOCTEST_REQUIRE_MESSAGE(cond, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE, cond); }() -#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN_FALSE, cond); }() -#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK_FALSE, cond); }() -#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE_FALSE, cond); }() -// clang-format on - -#define DOCTEST_ASSERT_THROWS_AS(expr, assert_type, message, ...) \ - [&] { \ - if(!doctest::getContextOptions()->no_throw) { \ - doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ - __LINE__, #expr, #__VA_ARGS__, message); \ - try { \ - DOCTEST_CAST_TO_VOID(expr) \ - } catch(const typename doctest::detail::remove_const< \ - typename doctest::detail::remove_reference<__VA_ARGS__>::type>::type&) { \ - DOCTEST_RB.translateException(); \ - DOCTEST_RB.m_threw_as = true; \ - } catch(...) { DOCTEST_RB.translateException(); } \ - DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ - } else { \ - return false; \ - } \ - }() - -#define DOCTEST_ASSERT_THROWS_WITH(expr, expr_str, assert_type, ...) \ - [&] { \ - if(!doctest::getContextOptions()->no_throw) { \ - doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ - __LINE__, expr_str, "", __VA_ARGS__); \ - try { \ - DOCTEST_CAST_TO_VOID(expr) \ - } catch(...) { DOCTEST_RB.translateException(); } \ - DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ - } else { \ - return false; \ - } \ - }() - -#define DOCTEST_ASSERT_NOTHROW(assert_type, ...) \ - [&] { \ - doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ - __LINE__, #__VA_ARGS__); \ - try { \ - DOCTEST_CAST_TO_VOID(__VA_ARGS__) \ - } catch(...) { DOCTEST_RB.translateException(); } \ - DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ - }() - -// clang-format off -#define DOCTEST_WARN_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_WARN_THROWS, "") -#define DOCTEST_CHECK_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_CHECK_THROWS, "") -#define DOCTEST_REQUIRE_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_REQUIRE_THROWS, "") - -#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_AS, "", __VA_ARGS__) -#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_AS, "", __VA_ARGS__) -#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_AS, "", __VA_ARGS__) - -#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_WARN_THROWS_WITH, __VA_ARGS__) -#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_CHECK_THROWS_WITH, __VA_ARGS__) -#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_REQUIRE_THROWS_WITH, __VA_ARGS__) - -#define DOCTEST_WARN_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_WITH_AS, message, __VA_ARGS__) -#define DOCTEST_CHECK_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_WITH_AS, message, __VA_ARGS__) -#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_WITH_AS, message, __VA_ARGS__) - -#define DOCTEST_WARN_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_WARN_NOTHROW, __VA_ARGS__) -#define DOCTEST_CHECK_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_CHECK_NOTHROW, __VA_ARGS__) -#define DOCTEST_REQUIRE_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_REQUIRE_NOTHROW, __VA_ARGS__) - -#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS(expr); }() -#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS(expr); }() -#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS(expr); }() -#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_AS(expr, ex); }() -#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_AS(expr, ex); }() -#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_AS(expr, ex); }() -#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_WITH(expr, with); }() -#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_WITH(expr, with); }() -#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_WITH(expr, with); }() -#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_WITH_AS(expr, with, ex); }() -#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ex); }() -#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ex); }() -#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_NOTHROW(expr); }() -#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_NOTHROW(expr); }() -#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) [&] {DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_NOTHROW(expr); }() -// clang-format on - -#ifndef DOCTEST_CONFIG_SUPER_FAST_ASSERTS - -#define DOCTEST_BINARY_ASSERT(assert_type, comp, ...) \ - [&] { \ - doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ - __LINE__, #__VA_ARGS__); \ - DOCTEST_WRAP_IN_TRY( \ - DOCTEST_RB.binary_assert( \ - __VA_ARGS__)) \ - DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ - }() - -#define DOCTEST_UNARY_ASSERT(assert_type, ...) \ - [&] { \ - doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ - __LINE__, #__VA_ARGS__); \ - DOCTEST_WRAP_IN_TRY(DOCTEST_RB.unary_assert(__VA_ARGS__)) \ - DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ - }() - -#else // DOCTEST_CONFIG_SUPER_FAST_ASSERTS - -#define DOCTEST_BINARY_ASSERT(assert_type, comparison, ...) \ - doctest::detail::binary_assert( \ - doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, __VA_ARGS__) - -#define DOCTEST_UNARY_ASSERT(assert_type, ...) \ - doctest::detail::unary_assert(doctest::assertType::assert_type, __FILE__, __LINE__, \ - #__VA_ARGS__, __VA_ARGS__) - -#endif // DOCTEST_CONFIG_SUPER_FAST_ASSERTS - -#define DOCTEST_WARN_EQ(...) DOCTEST_BINARY_ASSERT(DT_WARN_EQ, eq, __VA_ARGS__) -#define DOCTEST_CHECK_EQ(...) DOCTEST_BINARY_ASSERT(DT_CHECK_EQ, eq, __VA_ARGS__) -#define DOCTEST_REQUIRE_EQ(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_EQ, eq, __VA_ARGS__) -#define DOCTEST_WARN_NE(...) DOCTEST_BINARY_ASSERT(DT_WARN_NE, ne, __VA_ARGS__) -#define DOCTEST_CHECK_NE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_NE, ne, __VA_ARGS__) -#define DOCTEST_REQUIRE_NE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_NE, ne, __VA_ARGS__) -#define DOCTEST_WARN_GT(...) DOCTEST_BINARY_ASSERT(DT_WARN_GT, gt, __VA_ARGS__) -#define DOCTEST_CHECK_GT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GT, gt, __VA_ARGS__) -#define DOCTEST_REQUIRE_GT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GT, gt, __VA_ARGS__) -#define DOCTEST_WARN_LT(...) DOCTEST_BINARY_ASSERT(DT_WARN_LT, lt, __VA_ARGS__) -#define DOCTEST_CHECK_LT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LT, lt, __VA_ARGS__) -#define DOCTEST_REQUIRE_LT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LT, lt, __VA_ARGS__) -#define DOCTEST_WARN_GE(...) DOCTEST_BINARY_ASSERT(DT_WARN_GE, ge, __VA_ARGS__) -#define DOCTEST_CHECK_GE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GE, ge, __VA_ARGS__) -#define DOCTEST_REQUIRE_GE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GE, ge, __VA_ARGS__) -#define DOCTEST_WARN_LE(...) DOCTEST_BINARY_ASSERT(DT_WARN_LE, le, __VA_ARGS__) -#define DOCTEST_CHECK_LE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LE, le, __VA_ARGS__) -#define DOCTEST_REQUIRE_LE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LE, le, __VA_ARGS__) - -#define DOCTEST_WARN_UNARY(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY, __VA_ARGS__) -#define DOCTEST_CHECK_UNARY(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY, __VA_ARGS__) -#define DOCTEST_REQUIRE_UNARY(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY, __VA_ARGS__) -#define DOCTEST_WARN_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY_FALSE, __VA_ARGS__) -#define DOCTEST_CHECK_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY_FALSE, __VA_ARGS__) -#define DOCTEST_REQUIRE_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY_FALSE, __VA_ARGS__) - -#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS - -#undef DOCTEST_WARN_THROWS -#undef DOCTEST_CHECK_THROWS -#undef DOCTEST_REQUIRE_THROWS -#undef DOCTEST_WARN_THROWS_AS -#undef DOCTEST_CHECK_THROWS_AS -#undef DOCTEST_REQUIRE_THROWS_AS -#undef DOCTEST_WARN_THROWS_WITH -#undef DOCTEST_CHECK_THROWS_WITH -#undef DOCTEST_REQUIRE_THROWS_WITH -#undef DOCTEST_WARN_THROWS_WITH_AS -#undef DOCTEST_CHECK_THROWS_WITH_AS -#undef DOCTEST_REQUIRE_THROWS_WITH_AS -#undef DOCTEST_WARN_NOTHROW -#undef DOCTEST_CHECK_NOTHROW -#undef DOCTEST_REQUIRE_NOTHROW - -#undef DOCTEST_WARN_THROWS_MESSAGE -#undef DOCTEST_CHECK_THROWS_MESSAGE -#undef DOCTEST_REQUIRE_THROWS_MESSAGE -#undef DOCTEST_WARN_THROWS_AS_MESSAGE -#undef DOCTEST_CHECK_THROWS_AS_MESSAGE -#undef DOCTEST_REQUIRE_THROWS_AS_MESSAGE -#undef DOCTEST_WARN_THROWS_WITH_MESSAGE -#undef DOCTEST_CHECK_THROWS_WITH_MESSAGE -#undef DOCTEST_REQUIRE_THROWS_WITH_MESSAGE -#undef DOCTEST_WARN_THROWS_WITH_AS_MESSAGE -#undef DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE -#undef DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE -#undef DOCTEST_WARN_NOTHROW_MESSAGE -#undef DOCTEST_CHECK_NOTHROW_MESSAGE -#undef DOCTEST_REQUIRE_NOTHROW_MESSAGE - -#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS - -#define DOCTEST_WARN_THROWS(...) ([] { return false; }) -#define DOCTEST_CHECK_THROWS(...) ([] { return false; }) -#define DOCTEST_REQUIRE_THROWS(...) ([] { return false; }) -#define DOCTEST_WARN_THROWS_AS(expr, ...) ([] { return false; }) -#define DOCTEST_CHECK_THROWS_AS(expr, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) ([] { return false; }) -#define DOCTEST_WARN_THROWS_WITH(expr, ...) ([] { return false; }) -#define DOCTEST_CHECK_THROWS_WITH(expr, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) ([] { return false; }) -#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) ([] { return false; }) -#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) ([] { return false; }) -#define DOCTEST_WARN_NOTHROW(...) ([] { return false; }) -#define DOCTEST_CHECK_NOTHROW(...) ([] { return false; }) -#define DOCTEST_REQUIRE_NOTHROW(...) ([] { return false; }) - -#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) ([] { return false; }) -#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) ([] { return false; }) -#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) ([] { return false; }) -#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) ([] { return false; }) -#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) ([] { return false; }) -#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) ([] { return false; }) -#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) ([] { return false; }) -#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) ([] { return false; }) -#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) ([] { return false; }) -#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) ([] { return false; }) - -#else // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS - -#undef DOCTEST_REQUIRE -#undef DOCTEST_REQUIRE_FALSE -#undef DOCTEST_REQUIRE_MESSAGE -#undef DOCTEST_REQUIRE_FALSE_MESSAGE -#undef DOCTEST_REQUIRE_EQ -#undef DOCTEST_REQUIRE_NE -#undef DOCTEST_REQUIRE_GT -#undef DOCTEST_REQUIRE_LT -#undef DOCTEST_REQUIRE_GE -#undef DOCTEST_REQUIRE_LE -#undef DOCTEST_REQUIRE_UNARY -#undef DOCTEST_REQUIRE_UNARY_FALSE - -#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS - -#endif // DOCTEST_CONFIG_NO_EXCEPTIONS - -// ================================================================================================= -// == WHAT FOLLOWS IS VERSIONS OF THE MACROS THAT DO NOT DO ANY REGISTERING! == -// == THIS CAN BE ENABLED BY DEFINING DOCTEST_CONFIG_DISABLE GLOBALLY! == -// ================================================================================================= -#else // DOCTEST_CONFIG_DISABLE - -#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, name) \ - namespace { \ - template \ - struct der : public base \ - { void f(); }; \ - } \ - template \ - inline void der::f() - -#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, name) \ - template \ - static inline void f() - -// for registering tests -#define DOCTEST_TEST_CASE(name) \ - DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) - -// for registering tests in classes -#define DOCTEST_TEST_CASE_CLASS(name) \ - DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) - -// for registering tests with a fixture -#define DOCTEST_TEST_CASE_FIXTURE(x, name) \ - DOCTEST_IMPLEMENT_FIXTURE(DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), x, \ - DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) - -// for converting types to strings without the header and demangling -#define DOCTEST_TYPE_TO_STRING(...) static_assert(true, "") -#define DOCTEST_TYPE_TO_STRING_IMPL(...) - -// for typed tests -#define DOCTEST_TEST_CASE_TEMPLATE(name, type, ...) \ - template \ - inline void DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)() - -#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(name, type, id) \ - template \ - inline void DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)() - -#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) static_assert(true, "") -#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) static_assert(true, "") - -// for subcases -#define DOCTEST_SUBCASE(name) - -// for a testsuite block -#define DOCTEST_TEST_SUITE(name) namespace - -// for starting a testsuite block -#define DOCTEST_TEST_SUITE_BEGIN(name) static_assert(true, "") - -// for ending a testsuite block -#define DOCTEST_TEST_SUITE_END typedef int DOCTEST_ANONYMOUS(DOCTEST_ANON_FOR_SEMICOLON_) - -#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \ - template \ - static inline doctest::String DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_)(signature) - -#define DOCTEST_REGISTER_REPORTER(name, priority, reporter) -#define DOCTEST_REGISTER_LISTENER(name, priority, reporter) - -#define DOCTEST_INFO(...) (static_cast(0)) -#define DOCTEST_CAPTURE(x) (static_cast(0)) -#define DOCTEST_ADD_MESSAGE_AT(file, line, ...) (static_cast(0)) -#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, ...) (static_cast(0)) -#define DOCTEST_ADD_FAIL_AT(file, line, ...) (static_cast(0)) -#define DOCTEST_MESSAGE(...) (static_cast(0)) -#define DOCTEST_FAIL_CHECK(...) (static_cast(0)) -#define DOCTEST_FAIL(...) (static_cast(0)) - -#ifdef DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED - -#define DOCTEST_WARN(...) [&] { return __VA_ARGS__; }() -#define DOCTEST_CHECK(...) [&] { return __VA_ARGS__; }() -#define DOCTEST_REQUIRE(...) [&] { return __VA_ARGS__; }() -#define DOCTEST_WARN_FALSE(...) [&] { return !(__VA_ARGS__); }() -#define DOCTEST_CHECK_FALSE(...) [&] { return !(__VA_ARGS__); }() -#define DOCTEST_REQUIRE_FALSE(...) [&] { return !(__VA_ARGS__); }() - -#define DOCTEST_WARN_MESSAGE(cond, ...) [&] { return cond; }() -#define DOCTEST_CHECK_MESSAGE(cond, ...) [&] { return cond; }() -#define DOCTEST_REQUIRE_MESSAGE(cond, ...) [&] { return cond; }() -#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() -#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() -#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() - -namespace doctest { -namespace detail { -#define DOCTEST_RELATIONAL_OP(name, op) \ - template \ - bool name(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) { return lhs op rhs; } - - DOCTEST_RELATIONAL_OP(eq, ==) - DOCTEST_RELATIONAL_OP(ne, !=) - DOCTEST_RELATIONAL_OP(lt, <) - DOCTEST_RELATIONAL_OP(gt, >) - DOCTEST_RELATIONAL_OP(le, <=) - DOCTEST_RELATIONAL_OP(ge, >=) -} // namespace detail -} // namespace doctest - -#define DOCTEST_WARN_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() -#define DOCTEST_CHECK_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() -#define DOCTEST_REQUIRE_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() -#define DOCTEST_WARN_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() -#define DOCTEST_CHECK_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() -#define DOCTEST_REQUIRE_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() -#define DOCTEST_WARN_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() -#define DOCTEST_CHECK_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() -#define DOCTEST_REQUIRE_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() -#define DOCTEST_WARN_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() -#define DOCTEST_CHECK_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() -#define DOCTEST_REQUIRE_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() -#define DOCTEST_WARN_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() -#define DOCTEST_CHECK_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() -#define DOCTEST_REQUIRE_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() -#define DOCTEST_WARN_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() -#define DOCTEST_CHECK_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() -#define DOCTEST_REQUIRE_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() -#define DOCTEST_WARN_UNARY(...) [&] { return __VA_ARGS__; }() -#define DOCTEST_CHECK_UNARY(...) [&] { return __VA_ARGS__; }() -#define DOCTEST_REQUIRE_UNARY(...) [&] { return __VA_ARGS__; }() -#define DOCTEST_WARN_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() -#define DOCTEST_CHECK_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() -#define DOCTEST_REQUIRE_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() - -#else // DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED - -#define DOCTEST_WARN(...) ([] { return false; }) -#define DOCTEST_CHECK(...) ([] { return false; }) -#define DOCTEST_REQUIRE(...) ([] { return false; }) -#define DOCTEST_WARN_FALSE(...) ([] { return false; }) -#define DOCTEST_CHECK_FALSE(...) ([] { return false; }) -#define DOCTEST_REQUIRE_FALSE(...) ([] { return false; }) - -#define DOCTEST_WARN_MESSAGE(cond, ...) ([] { return false; }) -#define DOCTEST_CHECK_MESSAGE(cond, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_MESSAGE(cond, ...) ([] { return false; }) -#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) ([] { return false; }) -#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) ([] { return false; }) - -#define DOCTEST_WARN_EQ(...) ([] { return false; }) -#define DOCTEST_CHECK_EQ(...) ([] { return false; }) -#define DOCTEST_REQUIRE_EQ(...) ([] { return false; }) -#define DOCTEST_WARN_NE(...) ([] { return false; }) -#define DOCTEST_CHECK_NE(...) ([] { return false; }) -#define DOCTEST_REQUIRE_NE(...) ([] { return false; }) -#define DOCTEST_WARN_GT(...) ([] { return false; }) -#define DOCTEST_CHECK_GT(...) ([] { return false; }) -#define DOCTEST_REQUIRE_GT(...) ([] { return false; }) -#define DOCTEST_WARN_LT(...) ([] { return false; }) -#define DOCTEST_CHECK_LT(...) ([] { return false; }) -#define DOCTEST_REQUIRE_LT(...) ([] { return false; }) -#define DOCTEST_WARN_GE(...) ([] { return false; }) -#define DOCTEST_CHECK_GE(...) ([] { return false; }) -#define DOCTEST_REQUIRE_GE(...) ([] { return false; }) -#define DOCTEST_WARN_LE(...) ([] { return false; }) -#define DOCTEST_CHECK_LE(...) ([] { return false; }) -#define DOCTEST_REQUIRE_LE(...) ([] { return false; }) - -#define DOCTEST_WARN_UNARY(...) ([] { return false; }) -#define DOCTEST_CHECK_UNARY(...) ([] { return false; }) -#define DOCTEST_REQUIRE_UNARY(...) ([] { return false; }) -#define DOCTEST_WARN_UNARY_FALSE(...) ([] { return false; }) -#define DOCTEST_CHECK_UNARY_FALSE(...) ([] { return false; }) -#define DOCTEST_REQUIRE_UNARY_FALSE(...) ([] { return false; }) - -#endif // DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED - -// TODO: think about if these also need to work properly even when doctest is disabled -#define DOCTEST_WARN_THROWS(...) ([] { return false; }) -#define DOCTEST_CHECK_THROWS(...) ([] { return false; }) -#define DOCTEST_REQUIRE_THROWS(...) ([] { return false; }) -#define DOCTEST_WARN_THROWS_AS(expr, ...) ([] { return false; }) -#define DOCTEST_CHECK_THROWS_AS(expr, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) ([] { return false; }) -#define DOCTEST_WARN_THROWS_WITH(expr, ...) ([] { return false; }) -#define DOCTEST_CHECK_THROWS_WITH(expr, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) ([] { return false; }) -#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) ([] { return false; }) -#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) ([] { return false; }) -#define DOCTEST_WARN_NOTHROW(...) ([] { return false; }) -#define DOCTEST_CHECK_NOTHROW(...) ([] { return false; }) -#define DOCTEST_REQUIRE_NOTHROW(...) ([] { return false; }) - -#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) ([] { return false; }) -#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) ([] { return false; }) -#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) ([] { return false; }) -#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) ([] { return false; }) -#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) ([] { return false; }) -#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) ([] { return false; }) -#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) ([] { return false; }) -#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) ([] { return false; }) -#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) ([] { return false; }) -#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) ([] { return false; }) -#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) ([] { return false; }) - -#endif // DOCTEST_CONFIG_DISABLE - -// clang-format off -// KEPT FOR BACKWARDS COMPATIBILITY - FORWARDING TO THE RIGHT MACROS -#define DOCTEST_FAST_WARN_EQ DOCTEST_WARN_EQ -#define DOCTEST_FAST_CHECK_EQ DOCTEST_CHECK_EQ -#define DOCTEST_FAST_REQUIRE_EQ DOCTEST_REQUIRE_EQ -#define DOCTEST_FAST_WARN_NE DOCTEST_WARN_NE -#define DOCTEST_FAST_CHECK_NE DOCTEST_CHECK_NE -#define DOCTEST_FAST_REQUIRE_NE DOCTEST_REQUIRE_NE -#define DOCTEST_FAST_WARN_GT DOCTEST_WARN_GT -#define DOCTEST_FAST_CHECK_GT DOCTEST_CHECK_GT -#define DOCTEST_FAST_REQUIRE_GT DOCTEST_REQUIRE_GT -#define DOCTEST_FAST_WARN_LT DOCTEST_WARN_LT -#define DOCTEST_FAST_CHECK_LT DOCTEST_CHECK_LT -#define DOCTEST_FAST_REQUIRE_LT DOCTEST_REQUIRE_LT -#define DOCTEST_FAST_WARN_GE DOCTEST_WARN_GE -#define DOCTEST_FAST_CHECK_GE DOCTEST_CHECK_GE -#define DOCTEST_FAST_REQUIRE_GE DOCTEST_REQUIRE_GE -#define DOCTEST_FAST_WARN_LE DOCTEST_WARN_LE -#define DOCTEST_FAST_CHECK_LE DOCTEST_CHECK_LE -#define DOCTEST_FAST_REQUIRE_LE DOCTEST_REQUIRE_LE - -#define DOCTEST_FAST_WARN_UNARY DOCTEST_WARN_UNARY -#define DOCTEST_FAST_CHECK_UNARY DOCTEST_CHECK_UNARY -#define DOCTEST_FAST_REQUIRE_UNARY DOCTEST_REQUIRE_UNARY -#define DOCTEST_FAST_WARN_UNARY_FALSE DOCTEST_WARN_UNARY_FALSE -#define DOCTEST_FAST_CHECK_UNARY_FALSE DOCTEST_CHECK_UNARY_FALSE -#define DOCTEST_FAST_REQUIRE_UNARY_FALSE DOCTEST_REQUIRE_UNARY_FALSE - -#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id,__VA_ARGS__) -// clang-format on - -// BDD style macros -// clang-format off -#define DOCTEST_SCENARIO(name) DOCTEST_TEST_CASE(" Scenario: " name) -#define DOCTEST_SCENARIO_CLASS(name) DOCTEST_TEST_CASE_CLASS(" Scenario: " name) -#define DOCTEST_SCENARIO_TEMPLATE(name, T, ...) DOCTEST_TEST_CASE_TEMPLATE(" Scenario: " name, T, __VA_ARGS__) -#define DOCTEST_SCENARIO_TEMPLATE_DEFINE(name, T, id) DOCTEST_TEST_CASE_TEMPLATE_DEFINE(" Scenario: " name, T, id) - -#define DOCTEST_GIVEN(name) DOCTEST_SUBCASE(" Given: " name) -#define DOCTEST_WHEN(name) DOCTEST_SUBCASE(" When: " name) -#define DOCTEST_AND_WHEN(name) DOCTEST_SUBCASE("And when: " name) -#define DOCTEST_THEN(name) DOCTEST_SUBCASE(" Then: " name) -#define DOCTEST_AND_THEN(name) DOCTEST_SUBCASE(" And: " name) -// clang-format on - -// == SHORT VERSIONS OF THE MACROS -#if !defined(DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES) - -#define TEST_CASE(name) DOCTEST_TEST_CASE(name) -#define TEST_CASE_CLASS(name) DOCTEST_TEST_CASE_CLASS(name) -#define TEST_CASE_FIXTURE(x, name) DOCTEST_TEST_CASE_FIXTURE(x, name) -#define TYPE_TO_STRING(...) DOCTEST_TYPE_TO_STRING(__VA_ARGS__) -#define TEST_CASE_TEMPLATE(name, T, ...) DOCTEST_TEST_CASE_TEMPLATE(name, T, __VA_ARGS__) -#define TEST_CASE_TEMPLATE_DEFINE(name, T, id) DOCTEST_TEST_CASE_TEMPLATE_DEFINE(name, T, id) -#define TEST_CASE_TEMPLATE_INVOKE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, __VA_ARGS__) -#define TEST_CASE_TEMPLATE_APPLY(id, ...) DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, __VA_ARGS__) -#define SUBCASE(name) DOCTEST_SUBCASE(name) -#define TEST_SUITE(decorators) DOCTEST_TEST_SUITE(decorators) -#define TEST_SUITE_BEGIN(name) DOCTEST_TEST_SUITE_BEGIN(name) -#define TEST_SUITE_END DOCTEST_TEST_SUITE_END -#define REGISTER_EXCEPTION_TRANSLATOR(signature) DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) -#define REGISTER_REPORTER(name, priority, reporter) DOCTEST_REGISTER_REPORTER(name, priority, reporter) -#define REGISTER_LISTENER(name, priority, reporter) DOCTEST_REGISTER_LISTENER(name, priority, reporter) -#define INFO(...) DOCTEST_INFO(__VA_ARGS__) -#define CAPTURE(x) DOCTEST_CAPTURE(x) -#define ADD_MESSAGE_AT(file, line, ...) DOCTEST_ADD_MESSAGE_AT(file, line, __VA_ARGS__) -#define ADD_FAIL_CHECK_AT(file, line, ...) DOCTEST_ADD_FAIL_CHECK_AT(file, line, __VA_ARGS__) -#define ADD_FAIL_AT(file, line, ...) DOCTEST_ADD_FAIL_AT(file, line, __VA_ARGS__) -#define MESSAGE(...) DOCTEST_MESSAGE(__VA_ARGS__) -#define FAIL_CHECK(...) DOCTEST_FAIL_CHECK(__VA_ARGS__) -#define FAIL(...) DOCTEST_FAIL(__VA_ARGS__) -#define TO_LVALUE(...) DOCTEST_TO_LVALUE(__VA_ARGS__) - -#define WARN(...) DOCTEST_WARN(__VA_ARGS__) -#define WARN_FALSE(...) DOCTEST_WARN_FALSE(__VA_ARGS__) -#define WARN_THROWS(...) DOCTEST_WARN_THROWS(__VA_ARGS__) -#define WARN_THROWS_AS(expr, ...) DOCTEST_WARN_THROWS_AS(expr, __VA_ARGS__) -#define WARN_THROWS_WITH(expr, ...) DOCTEST_WARN_THROWS_WITH(expr, __VA_ARGS__) -#define WARN_THROWS_WITH_AS(expr, with, ...) DOCTEST_WARN_THROWS_WITH_AS(expr, with, __VA_ARGS__) -#define WARN_NOTHROW(...) DOCTEST_WARN_NOTHROW(__VA_ARGS__) -#define CHECK(...) DOCTEST_CHECK(__VA_ARGS__) -#define CHECK_FALSE(...) DOCTEST_CHECK_FALSE(__VA_ARGS__) -#define CHECK_THROWS(...) DOCTEST_CHECK_THROWS(__VA_ARGS__) -#define CHECK_THROWS_AS(expr, ...) DOCTEST_CHECK_THROWS_AS(expr, __VA_ARGS__) -#define CHECK_THROWS_WITH(expr, ...) DOCTEST_CHECK_THROWS_WITH(expr, __VA_ARGS__) -#define CHECK_THROWS_WITH_AS(expr, with, ...) DOCTEST_CHECK_THROWS_WITH_AS(expr, with, __VA_ARGS__) -#define CHECK_NOTHROW(...) DOCTEST_CHECK_NOTHROW(__VA_ARGS__) -#define REQUIRE(...) DOCTEST_REQUIRE(__VA_ARGS__) -#define REQUIRE_FALSE(...) DOCTEST_REQUIRE_FALSE(__VA_ARGS__) -#define REQUIRE_THROWS(...) DOCTEST_REQUIRE_THROWS(__VA_ARGS__) -#define REQUIRE_THROWS_AS(expr, ...) DOCTEST_REQUIRE_THROWS_AS(expr, __VA_ARGS__) -#define REQUIRE_THROWS_WITH(expr, ...) DOCTEST_REQUIRE_THROWS_WITH(expr, __VA_ARGS__) -#define REQUIRE_THROWS_WITH_AS(expr, with, ...) DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, __VA_ARGS__) -#define REQUIRE_NOTHROW(...) DOCTEST_REQUIRE_NOTHROW(__VA_ARGS__) - -#define WARN_MESSAGE(cond, ...) DOCTEST_WARN_MESSAGE(cond, __VA_ARGS__) -#define WARN_FALSE_MESSAGE(cond, ...) DOCTEST_WARN_FALSE_MESSAGE(cond, __VA_ARGS__) -#define WARN_THROWS_MESSAGE(expr, ...) DOCTEST_WARN_THROWS_MESSAGE(expr, __VA_ARGS__) -#define WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) -#define WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) -#define WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) -#define WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_WARN_NOTHROW_MESSAGE(expr, __VA_ARGS__) -#define CHECK_MESSAGE(cond, ...) DOCTEST_CHECK_MESSAGE(cond, __VA_ARGS__) -#define CHECK_FALSE_MESSAGE(cond, ...) DOCTEST_CHECK_FALSE_MESSAGE(cond, __VA_ARGS__) -#define CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_CHECK_THROWS_MESSAGE(expr, __VA_ARGS__) -#define CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) -#define CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) -#define CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) -#define CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_CHECK_NOTHROW_MESSAGE(expr, __VA_ARGS__) -#define REQUIRE_MESSAGE(cond, ...) DOCTEST_REQUIRE_MESSAGE(cond, __VA_ARGS__) -#define REQUIRE_FALSE_MESSAGE(cond, ...) DOCTEST_REQUIRE_FALSE_MESSAGE(cond, __VA_ARGS__) -#define REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_REQUIRE_THROWS_MESSAGE(expr, __VA_ARGS__) -#define REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) -#define REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) -#define REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) -#define REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, __VA_ARGS__) - -#define SCENARIO(name) DOCTEST_SCENARIO(name) -#define SCENARIO_CLASS(name) DOCTEST_SCENARIO_CLASS(name) -#define SCENARIO_TEMPLATE(name, T, ...) DOCTEST_SCENARIO_TEMPLATE(name, T, __VA_ARGS__) -#define SCENARIO_TEMPLATE_DEFINE(name, T, id) DOCTEST_SCENARIO_TEMPLATE_DEFINE(name, T, id) -#define GIVEN(name) DOCTEST_GIVEN(name) -#define WHEN(name) DOCTEST_WHEN(name) -#define AND_WHEN(name) DOCTEST_AND_WHEN(name) -#define THEN(name) DOCTEST_THEN(name) -#define AND_THEN(name) DOCTEST_AND_THEN(name) - -#define WARN_EQ(...) DOCTEST_WARN_EQ(__VA_ARGS__) -#define CHECK_EQ(...) DOCTEST_CHECK_EQ(__VA_ARGS__) -#define REQUIRE_EQ(...) DOCTEST_REQUIRE_EQ(__VA_ARGS__) -#define WARN_NE(...) DOCTEST_WARN_NE(__VA_ARGS__) -#define CHECK_NE(...) DOCTEST_CHECK_NE(__VA_ARGS__) -#define REQUIRE_NE(...) DOCTEST_REQUIRE_NE(__VA_ARGS__) -#define WARN_GT(...) DOCTEST_WARN_GT(__VA_ARGS__) -#define CHECK_GT(...) DOCTEST_CHECK_GT(__VA_ARGS__) -#define REQUIRE_GT(...) DOCTEST_REQUIRE_GT(__VA_ARGS__) -#define WARN_LT(...) DOCTEST_WARN_LT(__VA_ARGS__) -#define CHECK_LT(...) DOCTEST_CHECK_LT(__VA_ARGS__) -#define REQUIRE_LT(...) DOCTEST_REQUIRE_LT(__VA_ARGS__) -#define WARN_GE(...) DOCTEST_WARN_GE(__VA_ARGS__) -#define CHECK_GE(...) DOCTEST_CHECK_GE(__VA_ARGS__) -#define REQUIRE_GE(...) DOCTEST_REQUIRE_GE(__VA_ARGS__) -#define WARN_LE(...) DOCTEST_WARN_LE(__VA_ARGS__) -#define CHECK_LE(...) DOCTEST_CHECK_LE(__VA_ARGS__) -#define REQUIRE_LE(...) DOCTEST_REQUIRE_LE(__VA_ARGS__) -#define WARN_UNARY(...) DOCTEST_WARN_UNARY(__VA_ARGS__) -#define CHECK_UNARY(...) DOCTEST_CHECK_UNARY(__VA_ARGS__) -#define REQUIRE_UNARY(...) DOCTEST_REQUIRE_UNARY(__VA_ARGS__) -#define WARN_UNARY_FALSE(...) DOCTEST_WARN_UNARY_FALSE(__VA_ARGS__) -#define CHECK_UNARY_FALSE(...) DOCTEST_CHECK_UNARY_FALSE(__VA_ARGS__) -#define REQUIRE_UNARY_FALSE(...) DOCTEST_REQUIRE_UNARY_FALSE(__VA_ARGS__) - -// KEPT FOR BACKWARDS COMPATIBILITY -#define FAST_WARN_EQ(...) DOCTEST_FAST_WARN_EQ(__VA_ARGS__) -#define FAST_CHECK_EQ(...) DOCTEST_FAST_CHECK_EQ(__VA_ARGS__) -#define FAST_REQUIRE_EQ(...) DOCTEST_FAST_REQUIRE_EQ(__VA_ARGS__) -#define FAST_WARN_NE(...) DOCTEST_FAST_WARN_NE(__VA_ARGS__) -#define FAST_CHECK_NE(...) DOCTEST_FAST_CHECK_NE(__VA_ARGS__) -#define FAST_REQUIRE_NE(...) DOCTEST_FAST_REQUIRE_NE(__VA_ARGS__) -#define FAST_WARN_GT(...) DOCTEST_FAST_WARN_GT(__VA_ARGS__) -#define FAST_CHECK_GT(...) DOCTEST_FAST_CHECK_GT(__VA_ARGS__) -#define FAST_REQUIRE_GT(...) DOCTEST_FAST_REQUIRE_GT(__VA_ARGS__) -#define FAST_WARN_LT(...) DOCTEST_FAST_WARN_LT(__VA_ARGS__) -#define FAST_CHECK_LT(...) DOCTEST_FAST_CHECK_LT(__VA_ARGS__) -#define FAST_REQUIRE_LT(...) DOCTEST_FAST_REQUIRE_LT(__VA_ARGS__) -#define FAST_WARN_GE(...) DOCTEST_FAST_WARN_GE(__VA_ARGS__) -#define FAST_CHECK_GE(...) DOCTEST_FAST_CHECK_GE(__VA_ARGS__) -#define FAST_REQUIRE_GE(...) DOCTEST_FAST_REQUIRE_GE(__VA_ARGS__) -#define FAST_WARN_LE(...) DOCTEST_FAST_WARN_LE(__VA_ARGS__) -#define FAST_CHECK_LE(...) DOCTEST_FAST_CHECK_LE(__VA_ARGS__) -#define FAST_REQUIRE_LE(...) DOCTEST_FAST_REQUIRE_LE(__VA_ARGS__) - -#define FAST_WARN_UNARY(...) DOCTEST_FAST_WARN_UNARY(__VA_ARGS__) -#define FAST_CHECK_UNARY(...) DOCTEST_FAST_CHECK_UNARY(__VA_ARGS__) -#define FAST_REQUIRE_UNARY(...) DOCTEST_FAST_REQUIRE_UNARY(__VA_ARGS__) -#define FAST_WARN_UNARY_FALSE(...) DOCTEST_FAST_WARN_UNARY_FALSE(__VA_ARGS__) -#define FAST_CHECK_UNARY_FALSE(...) DOCTEST_FAST_CHECK_UNARY_FALSE(__VA_ARGS__) -#define FAST_REQUIRE_UNARY_FALSE(...) DOCTEST_FAST_REQUIRE_UNARY_FALSE(__VA_ARGS__) - -#define TEST_CASE_TEMPLATE_INSTANTIATE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE(id, __VA_ARGS__) - -#endif // DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES - -#if !defined(DOCTEST_CONFIG_DISABLE) - -// this is here to clear the 'current test suite' for the current translation unit - at the top -DOCTEST_TEST_SUITE_END(); - -// add stringification for primitive/fundamental types -namespace doctest { namespace detail { - DOCTEST_TYPE_TO_STRING_IMPL(bool) - DOCTEST_TYPE_TO_STRING_IMPL(float) - DOCTEST_TYPE_TO_STRING_IMPL(double) - DOCTEST_TYPE_TO_STRING_IMPL(long double) - DOCTEST_TYPE_TO_STRING_IMPL(char) - DOCTEST_TYPE_TO_STRING_IMPL(signed char) - DOCTEST_TYPE_TO_STRING_IMPL(unsigned char) -#if !DOCTEST_MSVC || defined(_NATIVE_WCHAR_T_DEFINED) - DOCTEST_TYPE_TO_STRING_IMPL(wchar_t) -#endif // not MSVC or wchar_t support enabled - DOCTEST_TYPE_TO_STRING_IMPL(short int) - DOCTEST_TYPE_TO_STRING_IMPL(unsigned short int) - DOCTEST_TYPE_TO_STRING_IMPL(int) - DOCTEST_TYPE_TO_STRING_IMPL(unsigned int) - DOCTEST_TYPE_TO_STRING_IMPL(long int) - DOCTEST_TYPE_TO_STRING_IMPL(unsigned long int) - DOCTEST_TYPE_TO_STRING_IMPL(long long int) - DOCTEST_TYPE_TO_STRING_IMPL(unsigned long long int) -}} // namespace doctest::detail - -#endif // DOCTEST_CONFIG_DISABLE - -DOCTEST_CLANG_SUPPRESS_WARNING_POP -DOCTEST_MSVC_SUPPRESS_WARNING_POP -DOCTEST_GCC_SUPPRESS_WARNING_POP - -DOCTEST_SUPPRESS_COMMON_WARNINGS_POP - -#endif // DOCTEST_LIBRARY_INCLUDED - -#ifndef DOCTEST_SINGLE_HEADER -#define DOCTEST_SINGLE_HEADER -#endif // DOCTEST_SINGLE_HEADER - -#if defined(DOCTEST_CONFIG_IMPLEMENT) || !defined(DOCTEST_SINGLE_HEADER) - -#ifndef DOCTEST_SINGLE_HEADER -#include "doctest_fwd.h" -#endif // DOCTEST_SINGLE_HEADER - -DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-macros") - -#ifndef DOCTEST_LIBRARY_IMPLEMENTATION -#define DOCTEST_LIBRARY_IMPLEMENTATION - -DOCTEST_CLANG_SUPPRESS_WARNING_POP - -DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH - -DOCTEST_CLANG_SUPPRESS_WARNING_PUSH -DOCTEST_CLANG_SUPPRESS_WARNING("-Wglobal-constructors") -DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") -DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion") -DOCTEST_CLANG_SUPPRESS_WARNING("-Wshorten-64-to-32") -DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-variable-declarations") -DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch") -DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch-enum") -DOCTEST_CLANG_SUPPRESS_WARNING("-Wcovered-switch-default") -DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-noreturn") -DOCTEST_CLANG_SUPPRESS_WARNING("-Wdisabled-macro-expansion") -DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-braces") -DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-field-initializers") -DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-member-function") -DOCTEST_CLANG_SUPPRESS_WARNING("-Wnonportable-system-include-path") - -DOCTEST_GCC_SUPPRESS_WARNING_PUSH -DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion") -DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion") -DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-field-initializers") -DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-braces") -DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch") -DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-enum") -DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-default") -DOCTEST_GCC_SUPPRESS_WARNING("-Wunsafe-loop-optimizations") -DOCTEST_GCC_SUPPRESS_WARNING("-Wold-style-cast") -DOCTEST_GCC_SUPPRESS_WARNING("-Wunused-function") -DOCTEST_GCC_SUPPRESS_WARNING("-Wmultiple-inheritance") -DOCTEST_GCC_SUPPRESS_WARNING("-Wsuggest-attribute") - -DOCTEST_MSVC_SUPPRESS_WARNING_PUSH -DOCTEST_MSVC_SUPPRESS_WARNING(4267) // 'var' : conversion from 'x' to 'y', possible loss of data -DOCTEST_MSVC_SUPPRESS_WARNING(4530) // C++ exception handler used, but unwind semantics not enabled -DOCTEST_MSVC_SUPPRESS_WARNING(4577) // 'noexcept' used with no exception handling mode specified -DOCTEST_MSVC_SUPPRESS_WARNING(4774) // format string expected in argument is not a string literal -DOCTEST_MSVC_SUPPRESS_WARNING(4365) // conversion from 'int' to 'unsigned', signed/unsigned mismatch -DOCTEST_MSVC_SUPPRESS_WARNING(5039) // pointer to potentially throwing function passed to extern C -DOCTEST_MSVC_SUPPRESS_WARNING(4800) // forcing value to bool 'true' or 'false' (performance warning) -DOCTEST_MSVC_SUPPRESS_WARNING(5245) // unreferenced function with internal linkage has been removed - -DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN - -// required includes - will go only in one translation unit! -#include -#include -#include -// borland (Embarcadero) compiler requires math.h and not cmath - https://github.com/doctest/doctest/pull/37 -#ifdef __BORLANDC__ -#include -#endif // __BORLANDC__ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef DOCTEST_PLATFORM_MAC -#include -#include -#include -#endif // DOCTEST_PLATFORM_MAC - -#ifdef DOCTEST_PLATFORM_WINDOWS - -// defines for a leaner windows.h -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif // WIN32_LEAN_AND_MEAN -#ifndef NOMINMAX -#define NOMINMAX -#endif // NOMINMAX - -// not sure what AfxWin.h is for - here I do what Catch does -#ifdef __AFXDLL -#include -#else -#include -#endif -#include - -#else // DOCTEST_PLATFORM_WINDOWS - -#include -#include - -#endif // DOCTEST_PLATFORM_WINDOWS - -// this is a fix for https://github.com/doctest/doctest/issues/348 -// https://mail.gnome.org/archives/xml/2012-January/msg00000.html -#if !defined(HAVE_UNISTD_H) && !defined(STDOUT_FILENO) -#define STDOUT_FILENO fileno(stdout) -#endif // HAVE_UNISTD_H - -DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END - -// counts the number of elements in a C array -#define DOCTEST_COUNTOF(x) (sizeof(x) / sizeof(x[0])) - -#ifdef DOCTEST_CONFIG_DISABLE -#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_disabled -#else // DOCTEST_CONFIG_DISABLE -#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_not_disabled -#endif // DOCTEST_CONFIG_DISABLE - -#ifndef DOCTEST_CONFIG_OPTIONS_PREFIX -#define DOCTEST_CONFIG_OPTIONS_PREFIX "dt-" -#endif - -#ifndef DOCTEST_THREAD_LOCAL -#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) -#define DOCTEST_THREAD_LOCAL -#else // DOCTEST_MSVC -#define DOCTEST_THREAD_LOCAL thread_local -#endif // DOCTEST_MSVC -#endif // DOCTEST_THREAD_LOCAL - -#ifndef DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES -#define DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES 32 -#endif - -#ifndef DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE -#define DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE 64 -#endif - -#ifdef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS -#define DOCTEST_OPTIONS_PREFIX_DISPLAY DOCTEST_CONFIG_OPTIONS_PREFIX -#else -#define DOCTEST_OPTIONS_PREFIX_DISPLAY "" -#endif - -#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) -#define DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS -#endif - -#ifndef DOCTEST_CDECL -#define DOCTEST_CDECL __cdecl -#endif - -namespace doctest { - -bool is_running_in_test = false; - -namespace { - using namespace detail; - - template - DOCTEST_NORETURN void throw_exception(Ex const& e) { -#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS - throw e; -#else // DOCTEST_CONFIG_NO_EXCEPTIONS - std::cerr << "doctest will terminate because it needed to throw an exception.\n" - << "The message was: " << e.what() << '\n'; - std::terminate(); -#endif // DOCTEST_CONFIG_NO_EXCEPTIONS - } - -#ifndef DOCTEST_INTERNAL_ERROR -#define DOCTEST_INTERNAL_ERROR(msg) \ - throw_exception(std::logic_error( \ - __FILE__ ":" DOCTEST_TOSTR(__LINE__) ": Internal doctest error: " msg)) -#endif // DOCTEST_INTERNAL_ERROR - - // case insensitive strcmp - int stricmp(const char* a, const char* b) { - for(;; a++, b++) { - const int d = tolower(*a) - tolower(*b); - if(d != 0 || !*a) - return d; - } - } - - template - String fpToString(T value, int precision) { - std::ostringstream oss; - oss << std::setprecision(precision) << std::fixed << value; - std::string d = oss.str(); - size_t i = d.find_last_not_of('0'); - if(i != std::string::npos && i != d.size() - 1) { - if(d[i] == '.') - i++; - d = d.substr(0, i + 1); - } - return d.c_str(); - } - - struct Endianness - { - enum Arch - { - Big, - Little - }; - - static Arch which() { - int x = 1; - // casting any data pointer to char* is allowed - auto ptr = reinterpret_cast(&x); - if(*ptr) - return Little; - return Big; - } - }; -} // namespace - -namespace detail { - String rawMemoryToString(const void* object, unsigned size) { - // Reverse order for little endian architectures - int i = 0, end = static_cast(size), inc = 1; - if(Endianness::which() == Endianness::Little) { - i = end - 1; - end = inc = -1; - } - - unsigned const char* bytes = static_cast(object); - std::ostream* oss = tlssPush(); - *oss << "0x" << std::setfill('0') << std::hex; - for(; i != end; i += inc) - *oss << std::setw(2) << static_cast(bytes[i]); - return tlssPop(); - } - - DOCTEST_THREAD_LOCAL class - { - std::vector stack; - std::stringstream ss; - - public: - std::ostream* push() { - stack.push_back(ss.tellp()); - return &ss; - } - - String pop() { - if (stack.empty()) - DOCTEST_INTERNAL_ERROR("TLSS was empty when trying to pop!"); - - std::streampos pos = stack.back(); - stack.pop_back(); - unsigned sz = static_cast(ss.tellp() - pos); - ss.rdbuf()->pubseekpos(pos, std::ios::in | std::ios::out); - return String(ss, sz); - } - } g_oss; - - std::ostream* tlssPush() { - return g_oss.push(); - } - - String tlssPop() { - return g_oss.pop(); - } - -#ifndef DOCTEST_CONFIG_DISABLE - -namespace timer_large_integer -{ - -#if defined(DOCTEST_PLATFORM_WINDOWS) - typedef ULONGLONG type; -#else // DOCTEST_PLATFORM_WINDOWS - typedef std::uint64_t type; -#endif // DOCTEST_PLATFORM_WINDOWS -} - -typedef timer_large_integer::type ticks_t; - -#ifdef DOCTEST_CONFIG_GETCURRENTTICKS - ticks_t getCurrentTicks() { return DOCTEST_CONFIG_GETCURRENTTICKS(); } -#elif defined(DOCTEST_PLATFORM_WINDOWS) - ticks_t getCurrentTicks() { - static LARGE_INTEGER hz = {0}, hzo = {0}; - if(!hz.QuadPart) { - QueryPerformanceFrequency(&hz); - QueryPerformanceCounter(&hzo); - } - LARGE_INTEGER t; - QueryPerformanceCounter(&t); - return ((t.QuadPart - hzo.QuadPart) * LONGLONG(1000000)) / hz.QuadPart; - } -#else // DOCTEST_PLATFORM_WINDOWS - ticks_t getCurrentTicks() { - timeval t; - gettimeofday(&t, nullptr); - return static_cast(t.tv_sec) * 1000000 + static_cast(t.tv_usec); - } -#endif // DOCTEST_PLATFORM_WINDOWS - - struct Timer - { - void start() { m_ticks = getCurrentTicks(); } - unsigned int getElapsedMicroseconds() const { - return static_cast(getCurrentTicks() - m_ticks); - } - //unsigned int getElapsedMilliseconds() const { - // return static_cast(getElapsedMicroseconds() / 1000); - //} - double getElapsedSeconds() const { return static_cast(getCurrentTicks() - m_ticks) / 1000000.0; } - - private: - ticks_t m_ticks = 0; - }; - -#ifdef DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS - template - using AtomicOrMultiLaneAtomic = std::atomic; -#else // DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS - // Provides a multilane implementation of an atomic variable that supports add, sub, load, - // store. Instead of using a single atomic variable, this splits up into multiple ones, - // each sitting on a separate cache line. The goal is to provide a speedup when most - // operations are modifying. It achieves this with two properties: - // - // * Multiple atomics are used, so chance of congestion from the same atomic is reduced. - // * Each atomic sits on a separate cache line, so false sharing is reduced. - // - // The disadvantage is that there is a small overhead due to the use of TLS, and load/store - // is slower because all atomics have to be accessed. - template - class MultiLaneAtomic - { - struct CacheLineAlignedAtomic - { - std::atomic atomic{}; - char padding[DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE - sizeof(std::atomic)]; - }; - CacheLineAlignedAtomic m_atomics[DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES]; - - static_assert(sizeof(CacheLineAlignedAtomic) == DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE, - "guarantee one atomic takes exactly one cache line"); - - public: - T operator++() DOCTEST_NOEXCEPT { return fetch_add(1) + 1; } - - T operator++(int) DOCTEST_NOEXCEPT { return fetch_add(1); } - - T fetch_add(T arg, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { - return myAtomic().fetch_add(arg, order); - } - - T fetch_sub(T arg, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { - return myAtomic().fetch_sub(arg, order); - } - - operator T() const DOCTEST_NOEXCEPT { return load(); } - - T load(std::memory_order order = std::memory_order_seq_cst) const DOCTEST_NOEXCEPT { - auto result = T(); - for(auto const& c : m_atomics) { - result += c.atomic.load(order); - } - return result; - } - - T operator=(T desired) DOCTEST_NOEXCEPT { // lgtm [cpp/assignment-does-not-return-this] - store(desired); - return desired; - } - - void store(T desired, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { - // first value becomes desired", all others become 0. - for(auto& c : m_atomics) { - c.atomic.store(desired, order); - desired = {}; - } - } - - private: - // Each thread has a different atomic that it operates on. If more than NumLanes threads - // use this, some will use the same atomic. So performance will degrade a bit, but still - // everything will work. - // - // The logic here is a bit tricky. The call should be as fast as possible, so that there - // is minimal to no overhead in determining the correct atomic for the current thread. - // - // 1. A global static counter laneCounter counts continuously up. - // 2. Each successive thread will use modulo operation of that counter so it gets an atomic - // assigned in a round-robin fashion. - // 3. This tlsLaneIdx is stored in the thread local data, so it is directly available with - // little overhead. - std::atomic& myAtomic() DOCTEST_NOEXCEPT { - static std::atomic laneCounter; - DOCTEST_THREAD_LOCAL size_t tlsLaneIdx = - laneCounter++ % DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES; - - return m_atomics[tlsLaneIdx].atomic; - } - }; - - template - using AtomicOrMultiLaneAtomic = MultiLaneAtomic; -#endif // DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS - - // this holds both parameters from the command line and runtime data for tests - struct ContextState : ContextOptions, TestRunStats, CurrentTestCaseStats - { - AtomicOrMultiLaneAtomic numAssertsCurrentTest_atomic; - AtomicOrMultiLaneAtomic numAssertsFailedCurrentTest_atomic; - - std::vector> filters = decltype(filters)(9); // 9 different filters - - std::vector reporters_currently_used; - - assert_handler ah = nullptr; - - Timer timer; - - std::vector stringifiedContexts; // logging from INFO() due to an exception - - // stuff for subcases - std::vector subcasesStack; - std::set subcasesPassed; - int subcasesCurrentMaxLevel; - bool should_reenter; - std::atomic shouldLogCurrentException; - - void resetRunData() { - numTestCases = 0; - numTestCasesPassingFilters = 0; - numTestSuitesPassingFilters = 0; - numTestCasesFailed = 0; - numAsserts = 0; - numAssertsFailed = 0; - numAssertsCurrentTest = 0; - numAssertsFailedCurrentTest = 0; - } - - void finalizeTestCaseData() { - seconds = timer.getElapsedSeconds(); - - // update the non-atomic counters - numAsserts += numAssertsCurrentTest_atomic; - numAssertsFailed += numAssertsFailedCurrentTest_atomic; - numAssertsCurrentTest = numAssertsCurrentTest_atomic; - numAssertsFailedCurrentTest = numAssertsFailedCurrentTest_atomic; - - if(numAssertsFailedCurrentTest) - failure_flags |= TestCaseFailureReason::AssertFailure; - - if(Approx(currentTest->m_timeout).epsilon(DBL_EPSILON) != 0 && - Approx(seconds).epsilon(DBL_EPSILON) > currentTest->m_timeout) - failure_flags |= TestCaseFailureReason::Timeout; - - if(currentTest->m_should_fail) { - if(failure_flags) { - failure_flags |= TestCaseFailureReason::ShouldHaveFailedAndDid; - } else { - failure_flags |= TestCaseFailureReason::ShouldHaveFailedButDidnt; - } - } else if(failure_flags && currentTest->m_may_fail) { - failure_flags |= TestCaseFailureReason::CouldHaveFailedAndDid; - } else if(currentTest->m_expected_failures > 0) { - if(numAssertsFailedCurrentTest == currentTest->m_expected_failures) { - failure_flags |= TestCaseFailureReason::FailedExactlyNumTimes; - } else { - failure_flags |= TestCaseFailureReason::DidntFailExactlyNumTimes; - } - } - - bool ok_to_fail = (TestCaseFailureReason::ShouldHaveFailedAndDid & failure_flags) || - (TestCaseFailureReason::CouldHaveFailedAndDid & failure_flags) || - (TestCaseFailureReason::FailedExactlyNumTimes & failure_flags); - - // if any subcase has failed - the whole test case has failed - testCaseSuccess = !(failure_flags && !ok_to_fail); - if(!testCaseSuccess) - numTestCasesFailed++; - } - }; - - ContextState* g_cs = nullptr; - - // used to avoid locks for the debug output - // TODO: figure out if this is indeed necessary/correct - seems like either there still - // could be a race or that there wouldn't be a race even if using the context directly - DOCTEST_THREAD_LOCAL bool g_no_colors; - -#endif // DOCTEST_CONFIG_DISABLE -} // namespace detail - -char* String::allocate(unsigned sz) { - if (sz <= last) { - buf[sz] = '\0'; - setLast(last - sz); - return buf; - } else { - setOnHeap(); - data.size = sz; - data.capacity = data.size + 1; - data.ptr = new char[data.capacity]; - data.ptr[sz] = '\0'; - return data.ptr; - } -} - -void String::setOnHeap() { *reinterpret_cast(&buf[last]) = 128; } -void String::setLast(unsigned in) { buf[last] = char(in); } - -void String::copy(const String& other) { - if(other.isOnStack()) { - memcpy(buf, other.buf, len); - } else { - memcpy(allocate(other.data.size), other.data.ptr, other.data.size); - } -} - -String::String() { - buf[0] = '\0'; - setLast(); -} - -String::~String() { - if(!isOnStack()) - delete[] data.ptr; - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) -} - -String::String(const char* in) - : String(in, strlen(in)) {} - -String::String(const char* in, unsigned in_size) { - memcpy(allocate(in_size), in, in_size); -} - -String::String(std::istream& in, unsigned in_size) { - in.read(allocate(in_size), in_size); -} - -String::String(const String& other) { copy(other); } - -String& String::operator=(const String& other) { - if(this != &other) { - if(!isOnStack()) - delete[] data.ptr; - - copy(other); - } - - return *this; -} - -String& String::operator+=(const String& other) { - const unsigned my_old_size = size(); - const unsigned other_size = other.size(); - const unsigned total_size = my_old_size + other_size; - if(isOnStack()) { - if(total_size < len) { - // append to the current stack space - memcpy(buf + my_old_size, other.c_str(), other_size + 1); - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) - setLast(last - total_size); - } else { - // alloc new chunk - char* temp = new char[total_size + 1]; - // copy current data to new location before writing in the union - memcpy(temp, buf, my_old_size); // skip the +1 ('\0') for speed - // update data in union - setOnHeap(); - data.size = total_size; - data.capacity = data.size + 1; - data.ptr = temp; - // transfer the rest of the data - memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); - } - } else { - if(data.capacity > total_size) { - // append to the current heap block - data.size = total_size; - memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); - } else { - // resize - data.capacity *= 2; - if(data.capacity <= total_size) - data.capacity = total_size + 1; - // alloc new chunk - char* temp = new char[data.capacity]; - // copy current data to new location before releasing it - memcpy(temp, data.ptr, my_old_size); // skip the +1 ('\0') for speed - // release old chunk - delete[] data.ptr; - // update the rest of the union members - data.size = total_size; - data.ptr = temp; - // transfer the rest of the data - memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); - } - } - - return *this; -} - -String::String(String&& other) { - memcpy(buf, other.buf, len); - other.buf[0] = '\0'; - other.setLast(); -} - -String& String::operator=(String&& other) { - if(this != &other) { - if(!isOnStack()) - delete[] data.ptr; - memcpy(buf, other.buf, len); - other.buf[0] = '\0'; - other.setLast(); - } - return *this; -} - -char String::operator[](unsigned i) const { - return const_cast(this)->operator[](i); // NOLINT -} - -char& String::operator[](unsigned i) { - if(isOnStack()) - return reinterpret_cast(buf)[i]; - return data.ptr[i]; -} - -DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wmaybe-uninitialized") -unsigned String::size() const { - if(isOnStack()) - return last - (unsigned(buf[last]) & 31); // using "last" would work only if "len" is 32 - return data.size; -} -DOCTEST_GCC_SUPPRESS_WARNING_POP - -unsigned String::capacity() const { - if(isOnStack()) - return len; - return data.capacity; -} - -int String::compare(const char* other, bool no_case) const { - if(no_case) - return doctest::stricmp(c_str(), other); - return std::strcmp(c_str(), other); -} - -int String::compare(const String& other, bool no_case) const { - return compare(other.c_str(), no_case); -} - -// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) -String operator+(const String& lhs, const String& rhs) { return String(lhs) += rhs; } - -// clang-format off -bool operator==(const String& lhs, const String& rhs) { return lhs.compare(rhs) == 0; } -bool operator!=(const String& lhs, const String& rhs) { return lhs.compare(rhs) != 0; } -bool operator< (const String& lhs, const String& rhs) { return lhs.compare(rhs) < 0; } -bool operator> (const String& lhs, const String& rhs) { return lhs.compare(rhs) > 0; } -bool operator<=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) < 0 : true; } -bool operator>=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) > 0 : true; } -// clang-format on - -std::ostream& operator<<(std::ostream& s, const String& in) { return s << in.c_str(); } - -namespace { - void color_to_stream(std::ostream&, Color::Enum) DOCTEST_BRANCH_ON_DISABLED({}, ;) -} // namespace - -namespace Color { - std::ostream& operator<<(std::ostream& s, Color::Enum code) { - color_to_stream(s, code); - return s; - } -} // namespace Color - -// clang-format off -const char* assertString(assertType::Enum at) { - DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4062) // enum 'x' in switch of enum 'y' is not handled - switch(at) { //!OCLINT missing default in switch statements - case assertType::DT_WARN : return "WARN"; - case assertType::DT_CHECK : return "CHECK"; - case assertType::DT_REQUIRE : return "REQUIRE"; - - case assertType::DT_WARN_FALSE : return "WARN_FALSE"; - case assertType::DT_CHECK_FALSE : return "CHECK_FALSE"; - case assertType::DT_REQUIRE_FALSE : return "REQUIRE_FALSE"; - - case assertType::DT_WARN_THROWS : return "WARN_THROWS"; - case assertType::DT_CHECK_THROWS : return "CHECK_THROWS"; - case assertType::DT_REQUIRE_THROWS : return "REQUIRE_THROWS"; - - case assertType::DT_WARN_THROWS_AS : return "WARN_THROWS_AS"; - case assertType::DT_CHECK_THROWS_AS : return "CHECK_THROWS_AS"; - case assertType::DT_REQUIRE_THROWS_AS : return "REQUIRE_THROWS_AS"; - - case assertType::DT_WARN_THROWS_WITH : return "WARN_THROWS_WITH"; - case assertType::DT_CHECK_THROWS_WITH : return "CHECK_THROWS_WITH"; - case assertType::DT_REQUIRE_THROWS_WITH : return "REQUIRE_THROWS_WITH"; - - case assertType::DT_WARN_THROWS_WITH_AS : return "WARN_THROWS_WITH_AS"; - case assertType::DT_CHECK_THROWS_WITH_AS : return "CHECK_THROWS_WITH_AS"; - case assertType::DT_REQUIRE_THROWS_WITH_AS : return "REQUIRE_THROWS_WITH_AS"; - - case assertType::DT_WARN_NOTHROW : return "WARN_NOTHROW"; - case assertType::DT_CHECK_NOTHROW : return "CHECK_NOTHROW"; - case assertType::DT_REQUIRE_NOTHROW : return "REQUIRE_NOTHROW"; - - case assertType::DT_WARN_EQ : return "WARN_EQ"; - case assertType::DT_CHECK_EQ : return "CHECK_EQ"; - case assertType::DT_REQUIRE_EQ : return "REQUIRE_EQ"; - case assertType::DT_WARN_NE : return "WARN_NE"; - case assertType::DT_CHECK_NE : return "CHECK_NE"; - case assertType::DT_REQUIRE_NE : return "REQUIRE_NE"; - case assertType::DT_WARN_GT : return "WARN_GT"; - case assertType::DT_CHECK_GT : return "CHECK_GT"; - case assertType::DT_REQUIRE_GT : return "REQUIRE_GT"; - case assertType::DT_WARN_LT : return "WARN_LT"; - case assertType::DT_CHECK_LT : return "CHECK_LT"; - case assertType::DT_REQUIRE_LT : return "REQUIRE_LT"; - case assertType::DT_WARN_GE : return "WARN_GE"; - case assertType::DT_CHECK_GE : return "CHECK_GE"; - case assertType::DT_REQUIRE_GE : return "REQUIRE_GE"; - case assertType::DT_WARN_LE : return "WARN_LE"; - case assertType::DT_CHECK_LE : return "CHECK_LE"; - case assertType::DT_REQUIRE_LE : return "REQUIRE_LE"; - - case assertType::DT_WARN_UNARY : return "WARN_UNARY"; - case assertType::DT_CHECK_UNARY : return "CHECK_UNARY"; - case assertType::DT_REQUIRE_UNARY : return "REQUIRE_UNARY"; - case assertType::DT_WARN_UNARY_FALSE : return "WARN_UNARY_FALSE"; - case assertType::DT_CHECK_UNARY_FALSE : return "CHECK_UNARY_FALSE"; - case assertType::DT_REQUIRE_UNARY_FALSE : return "REQUIRE_UNARY_FALSE"; - } - DOCTEST_MSVC_SUPPRESS_WARNING_POP - return ""; -} -// clang-format on - -const char* failureString(assertType::Enum at) { - if(at & assertType::is_warn) //!OCLINT bitwise operator in conditional - return "WARNING"; - if(at & assertType::is_check) //!OCLINT bitwise operator in conditional - return "ERROR"; - if(at & assertType::is_require) //!OCLINT bitwise operator in conditional - return "FATAL ERROR"; - return ""; -} - -DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference") -DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference") -// depending on the current options this will remove the path of filenames -const char* skipPathFromFilename(const char* file) { -#ifndef DOCTEST_CONFIG_DISABLE - if(getContextOptions()->no_path_in_filenames) { - auto back = std::strrchr(file, '\\'); - auto forward = std::strrchr(file, '/'); - if(back || forward) { - if(back > forward) - forward = back; - return forward + 1; - } - } -#endif // DOCTEST_CONFIG_DISABLE - return file; -} -DOCTEST_CLANG_SUPPRESS_WARNING_POP -DOCTEST_GCC_SUPPRESS_WARNING_POP - -bool SubcaseSignature::operator<(const SubcaseSignature& other) const { - if(m_line != other.m_line) - return m_line < other.m_line; - if(std::strcmp(m_file, other.m_file) != 0) - return std::strcmp(m_file, other.m_file) < 0; - return m_name.compare(other.m_name) < 0; -} - -IContextScope::IContextScope() = default; -IContextScope::~IContextScope() = default; - -#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING -String toString(char* in) { return toString(static_cast(in)); } -// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) -String toString(const char* in) { return String("\"") + (in ? in : "{null string}") + "\""; } -#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING -String toString(bool in) { return in ? "true" : "false"; } -String toString(float in) { return fpToString(in, 5) + "f"; } -String toString(double in) { return fpToString(in, 10); } -String toString(double long in) { return fpToString(in, 15); } - -#define DOCTEST_TO_STRING_OVERLOAD(type, fmt) \ - String toString(type in) { \ - char buf[64]; \ - std::sprintf(buf, fmt, in); \ - return buf; \ - } - -DOCTEST_TO_STRING_OVERLOAD(char, "%d") -DOCTEST_TO_STRING_OVERLOAD(char signed, "%d") -DOCTEST_TO_STRING_OVERLOAD(char unsigned, "%u") -DOCTEST_TO_STRING_OVERLOAD(int short, "%d") -DOCTEST_TO_STRING_OVERLOAD(int short unsigned, "%u") -DOCTEST_TO_STRING_OVERLOAD(int, "%d") -DOCTEST_TO_STRING_OVERLOAD(unsigned, "%u") -DOCTEST_TO_STRING_OVERLOAD(int long, "%ld") -DOCTEST_TO_STRING_OVERLOAD(int long unsigned, "%lu") -DOCTEST_TO_STRING_OVERLOAD(int long long, "%lld") -DOCTEST_TO_STRING_OVERLOAD(int long long unsigned, "%llu") - -String toString(std::nullptr_t) { return "NULL"; } - -#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) -// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 -String toString(const std::string& in) { return in.c_str(); } -#endif // VS 2019 - -Approx::Approx(double value) - : m_epsilon(static_cast(std::numeric_limits::epsilon()) * 100) - , m_scale(1.0) - , m_value(value) {} - -Approx Approx::operator()(double value) const { - Approx approx(value); - approx.epsilon(m_epsilon); - approx.scale(m_scale); - return approx; -} - -Approx& Approx::epsilon(double newEpsilon) { - m_epsilon = newEpsilon; - return *this; -} -Approx& Approx::scale(double newScale) { - m_scale = newScale; - return *this; -} - -bool operator==(double lhs, const Approx& rhs) { - // Thanks to Richard Harris for his help refining this formula - return std::fabs(lhs - rhs.m_value) < - rhs.m_epsilon * (rhs.m_scale + std::max(std::fabs(lhs), std::fabs(rhs.m_value))); -} -bool operator==(const Approx& lhs, double rhs) { return operator==(rhs, lhs); } -bool operator!=(double lhs, const Approx& rhs) { return !operator==(lhs, rhs); } -bool operator!=(const Approx& lhs, double rhs) { return !operator==(rhs, lhs); } -bool operator<=(double lhs, const Approx& rhs) { return lhs < rhs.m_value || lhs == rhs; } -bool operator<=(const Approx& lhs, double rhs) { return lhs.m_value < rhs || lhs == rhs; } -bool operator>=(double lhs, const Approx& rhs) { return lhs > rhs.m_value || lhs == rhs; } -bool operator>=(const Approx& lhs, double rhs) { return lhs.m_value > rhs || lhs == rhs; } -bool operator<(double lhs, const Approx& rhs) { return lhs < rhs.m_value && lhs != rhs; } -bool operator<(const Approx& lhs, double rhs) { return lhs.m_value < rhs && lhs != rhs; } -bool operator>(double lhs, const Approx& rhs) { return lhs > rhs.m_value && lhs != rhs; } -bool operator>(const Approx& lhs, double rhs) { return lhs.m_value > rhs && lhs != rhs; } - -String toString(const Approx& in) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) - return "Approx( " + doctest::toString(in.m_value) + " )"; -} -const ContextOptions* getContextOptions() { return DOCTEST_BRANCH_ON_DISABLED(nullptr, g_cs); } - -} // namespace doctest - -#ifdef DOCTEST_CONFIG_DISABLE -namespace doctest { -Context::Context(int, const char* const*) {} -Context::~Context() = default; -void Context::applyCommandLine(int, const char* const*) {} -void Context::addFilter(const char*, const char*) {} -void Context::clearFilters() {} -void Context::setOption(const char*, bool) {} -void Context::setOption(const char*, int) {} -void Context::setOption(const char*, const char*) {} -bool Context::shouldExit() { return false; } -void Context::setAsDefaultForAssertsOutOfTestCases() {} -void Context::setAssertHandler(detail::assert_handler) {} -void Context::setCout(std::ostream* out) {} -int Context::run() { return 0; } - -IReporter::~IReporter() = default; - -int IReporter::get_num_active_contexts() { return 0; } -const IContextScope* const* IReporter::get_active_contexts() { return nullptr; } -int IReporter::get_num_stringified_contexts() { return 0; } -const String* IReporter::get_stringified_contexts() { return nullptr; } - -int registerReporter(const char*, int, IReporter*) { return 0; } - -} // namespace doctest -#else // DOCTEST_CONFIG_DISABLE - -#if !defined(DOCTEST_CONFIG_COLORS_NONE) -#if !defined(DOCTEST_CONFIG_COLORS_WINDOWS) && !defined(DOCTEST_CONFIG_COLORS_ANSI) -#ifdef DOCTEST_PLATFORM_WINDOWS -#define DOCTEST_CONFIG_COLORS_WINDOWS -#else // linux -#define DOCTEST_CONFIG_COLORS_ANSI -#endif // platform -#endif // DOCTEST_CONFIG_COLORS_WINDOWS && DOCTEST_CONFIG_COLORS_ANSI -#endif // DOCTEST_CONFIG_COLORS_NONE - -namespace doctest_detail_test_suite_ns { -// holds the current test suite -doctest::detail::TestSuite& getCurrentTestSuite() { - static doctest::detail::TestSuite data{}; - return data; -} -} // namespace doctest_detail_test_suite_ns - -namespace doctest { -namespace { - // the int (priority) is part of the key for automatic sorting - sadly one can register a - // reporter with a duplicate name and a different priority but hopefully that won't happen often :| - typedef std::map, reporterCreatorFunc> reporterMap; - - reporterMap& getReporters() { - static reporterMap data; - return data; - } - reporterMap& getListeners() { - static reporterMap data; - return data; - } -} // namespace -namespace detail { -#define DOCTEST_ITERATE_THROUGH_REPORTERS(function, ...) \ - for(auto& curr_rep : g_cs->reporters_currently_used) \ - curr_rep->function(__VA_ARGS__) - - bool checkIfShouldThrow(assertType::Enum at) { - if(at & assertType::is_require) //!OCLINT bitwise operator in conditional - return true; - - if((at & assertType::is_check) //!OCLINT bitwise operator in conditional - && getContextOptions()->abort_after > 0 && - (g_cs->numAssertsFailed + g_cs->numAssertsFailedCurrentTest_atomic) >= - getContextOptions()->abort_after) - return true; - - return false; - } - -#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS - DOCTEST_NORETURN void throwException() { - g_cs->shouldLogCurrentException = false; - throw TestFailureException(); - } // NOLINT(cert-err60-cpp) -#else // DOCTEST_CONFIG_NO_EXCEPTIONS - void throwException() {} -#endif // DOCTEST_CONFIG_NO_EXCEPTIONS -} // namespace detail - -namespace { - using namespace detail; - // matching of a string against a wildcard mask (case sensitivity configurable) taken from - // https://www.codeproject.com/Articles/1088/Wildcard-string-compare-globbing - int wildcmp(const char* str, const char* wild, bool caseSensitive) { - const char* cp = str; - const char* mp = wild; - - while((*str) && (*wild != '*')) { - if((caseSensitive ? (*wild != *str) : (tolower(*wild) != tolower(*str))) && - (*wild != '?')) { - return 0; - } - wild++; - str++; - } - - while(*str) { - if(*wild == '*') { - if(!*++wild) { - return 1; - } - mp = wild; - cp = str + 1; - } else if((caseSensitive ? (*wild == *str) : (tolower(*wild) == tolower(*str))) || - (*wild == '?')) { - wild++; - str++; - } else { - wild = mp; //!OCLINT parameter reassignment - str = cp++; //!OCLINT parameter reassignment - } - } - - while(*wild == '*') { - wild++; - } - return !*wild; - } - - //// C string hash function (djb2) - taken from http://www.cse.yorku.ca/~oz/hash.html - //unsigned hashStr(unsigned const char* str) { - // unsigned long hash = 5381; - // char c; - // while((c = *str++)) - // hash = ((hash << 5) + hash) + c; // hash * 33 + c - // return hash; - //} - - // checks if the name matches any of the filters (and can be configured what to do when empty) - bool matchesAny(const char* name, const std::vector& filters, bool matchEmpty, - bool caseSensitive) { - if(filters.empty() && matchEmpty) - return true; - for(auto& curr : filters) - if(wildcmp(name, curr.c_str(), caseSensitive)) - return true; - return false; - } -} // namespace -namespace detail { - - Subcase::Subcase(const String& name, const char* file, int line) - : m_signature({name, file, line}) { - auto* s = g_cs; - - // check subcase filters - if(s->subcasesStack.size() < size_t(s->subcase_filter_levels)) { - if(!matchesAny(m_signature.m_name.c_str(), s->filters[6], true, s->case_sensitive)) - return; - if(matchesAny(m_signature.m_name.c_str(), s->filters[7], false, s->case_sensitive)) - return; - } - - // if a Subcase on the same level has already been entered - if(s->subcasesStack.size() < size_t(s->subcasesCurrentMaxLevel)) { - s->should_reenter = true; - return; - } - - // push the current signature to the stack so we can check if the - // current stack + the current new subcase have been traversed - s->subcasesStack.push_back(m_signature); - if(s->subcasesPassed.count(s->subcasesStack) != 0) { - // pop - revert to previous stack since we've already passed this - s->subcasesStack.pop_back(); - return; - } - - s->subcasesCurrentMaxLevel = s->subcasesStack.size(); - m_entered = true; - - DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_start, m_signature); - } - - DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4996) // std::uncaught_exception is deprecated in C++17 - DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") - DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") - - Subcase::~Subcase() { - if(m_entered) { - // only mark the subcase stack as passed if no subcases have been skipped - if(g_cs->should_reenter == false) - g_cs->subcasesPassed.insert(g_cs->subcasesStack); - g_cs->subcasesStack.pop_back(); - -#if defined(__cpp_lib_uncaught_exceptions) && __cpp_lib_uncaught_exceptions >= 201411L && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200) - if(std::uncaught_exceptions() > 0 -#else - if(std::uncaught_exception() -#endif - && g_cs->shouldLogCurrentException) { - DOCTEST_ITERATE_THROUGH_REPORTERS( - test_case_exception, {"exception thrown in subcase - will translate later " - "when the whole test case has been exited (cannot " - "translate while there is an active exception)", - false}); - g_cs->shouldLogCurrentException = false; - } - DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY); - } - } - - DOCTEST_CLANG_SUPPRESS_WARNING_POP - DOCTEST_GCC_SUPPRESS_WARNING_POP - DOCTEST_MSVC_SUPPRESS_WARNING_POP - - Subcase::operator bool() const { return m_entered; } - - Result::Result(bool passed, const String& decomposition) - : m_passed(passed) - , m_decomp(decomposition) {} - - ExpressionDecomposer::ExpressionDecomposer(assertType::Enum at) - : m_at(at) {} - - TestSuite& TestSuite::operator*(const char* in) { - m_test_suite = in; - return *this; - } - - TestCase::TestCase(funcType test, const char* file, unsigned line, const TestSuite& test_suite, - const char* type, int template_id) { - m_file = file; - m_line = line; - m_name = nullptr; // will be later overridden in operator* - m_test_suite = test_suite.m_test_suite; - m_description = test_suite.m_description; - m_skip = test_suite.m_skip; - m_no_breaks = test_suite.m_no_breaks; - m_no_output = test_suite.m_no_output; - m_may_fail = test_suite.m_may_fail; - m_should_fail = test_suite.m_should_fail; - m_expected_failures = test_suite.m_expected_failures; - m_timeout = test_suite.m_timeout; - - m_test = test; - m_type = type; - m_template_id = template_id; - } - - TestCase::TestCase(const TestCase& other) - : TestCaseData() { - *this = other; - } - - DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function - DOCTEST_MSVC_SUPPRESS_WARNING(26437) // Do not slice - TestCase& TestCase::operator=(const TestCase& other) { - static_cast(*this) = static_cast(other); - - m_test = other.m_test; - m_type = other.m_type; - m_template_id = other.m_template_id; - m_full_name = other.m_full_name; - - if(m_template_id != -1) - m_name = m_full_name.c_str(); - return *this; - } - DOCTEST_MSVC_SUPPRESS_WARNING_POP - - TestCase& TestCase::operator*(const char* in) { - m_name = in; - // make a new name with an appended type for templated test case - if(m_template_id != -1) { - m_full_name = String(m_name) + m_type; - // redirect the name to point to the newly constructed full name - m_name = m_full_name.c_str(); - } - return *this; - } - - bool TestCase::operator<(const TestCase& other) const { - // this will be used only to differentiate between test cases - not relevant for sorting - if(m_line != other.m_line) - return m_line < other.m_line; - const int name_cmp = strcmp(m_name, other.m_name); - if(name_cmp != 0) - return name_cmp < 0; - const int file_cmp = m_file.compare(other.m_file); - if(file_cmp != 0) - return file_cmp < 0; - return m_template_id < other.m_template_id; - } - - // all the registered tests - std::set& getRegisteredTests() { - static std::set data; - return data; - } -} // namespace detail -namespace { - using namespace detail; - // for sorting tests by file/line - bool fileOrderComparator(const TestCase* lhs, const TestCase* rhs) { - // this is needed because MSVC gives different case for drive letters - // for __FILE__ when evaluated in a header and a source file - const int res = lhs->m_file.compare(rhs->m_file, bool(DOCTEST_MSVC)); - if(res != 0) - return res < 0; - if(lhs->m_line != rhs->m_line) - return lhs->m_line < rhs->m_line; - return lhs->m_template_id < rhs->m_template_id; - } - - // for sorting tests by suite/file/line - bool suiteOrderComparator(const TestCase* lhs, const TestCase* rhs) { - const int res = std::strcmp(lhs->m_test_suite, rhs->m_test_suite); - if(res != 0) - return res < 0; - return fileOrderComparator(lhs, rhs); - } - - // for sorting tests by name/suite/file/line - bool nameOrderComparator(const TestCase* lhs, const TestCase* rhs) { - const int res = std::strcmp(lhs->m_name, rhs->m_name); - if(res != 0) - return res < 0; - return suiteOrderComparator(lhs, rhs); - } - - DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") - void color_to_stream(std::ostream& s, Color::Enum code) { - static_cast(s); // for DOCTEST_CONFIG_COLORS_NONE or DOCTEST_CONFIG_COLORS_WINDOWS - static_cast(code); // for DOCTEST_CONFIG_COLORS_NONE -#ifdef DOCTEST_CONFIG_COLORS_ANSI - if(g_no_colors || - (isatty(STDOUT_FILENO) == false && getContextOptions()->force_colors == false)) - return; - - auto col = ""; - // clang-format off - switch(code) { //!OCLINT missing break in switch statement / unnecessary default statement in covered switch statement - case Color::Red: col = "[0;31m"; break; - case Color::Green: col = "[0;32m"; break; - case Color::Blue: col = "[0;34m"; break; - case Color::Cyan: col = "[0;36m"; break; - case Color::Yellow: col = "[0;33m"; break; - case Color::Grey: col = "[1;30m"; break; - case Color::LightGrey: col = "[0;37m"; break; - case Color::BrightRed: col = "[1;31m"; break; - case Color::BrightGreen: col = "[1;32m"; break; - case Color::BrightWhite: col = "[1;37m"; break; - case Color::Bright: // invalid - case Color::None: - case Color::White: - default: col = "[0m"; - } - // clang-format on - s << "\033" << col; -#endif // DOCTEST_CONFIG_COLORS_ANSI - -#ifdef DOCTEST_CONFIG_COLORS_WINDOWS - if(g_no_colors || - (_isatty(_fileno(stdout)) == false && getContextOptions()->force_colors == false)) - return; - - static struct ConsoleHelper { - HANDLE stdoutHandle; - WORD origFgAttrs; - WORD origBgAttrs; - - ConsoleHelper() { - stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); - CONSOLE_SCREEN_BUFFER_INFO csbiInfo; - GetConsoleScreenBufferInfo(stdoutHandle, &csbiInfo); - origFgAttrs = csbiInfo.wAttributes & ~(BACKGROUND_GREEN | BACKGROUND_RED | - BACKGROUND_BLUE | BACKGROUND_INTENSITY); - origBgAttrs = csbiInfo.wAttributes & ~(FOREGROUND_GREEN | FOREGROUND_RED | - FOREGROUND_BLUE | FOREGROUND_INTENSITY); - } - } ch; - -#define DOCTEST_SET_ATTR(x) SetConsoleTextAttribute(ch.stdoutHandle, x | ch.origBgAttrs) - - // clang-format off - switch (code) { - case Color::White: DOCTEST_SET_ATTR(FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break; - case Color::Red: DOCTEST_SET_ATTR(FOREGROUND_RED); break; - case Color::Green: DOCTEST_SET_ATTR(FOREGROUND_GREEN); break; - case Color::Blue: DOCTEST_SET_ATTR(FOREGROUND_BLUE); break; - case Color::Cyan: DOCTEST_SET_ATTR(FOREGROUND_BLUE | FOREGROUND_GREEN); break; - case Color::Yellow: DOCTEST_SET_ATTR(FOREGROUND_RED | FOREGROUND_GREEN); break; - case Color::Grey: DOCTEST_SET_ATTR(0); break; - case Color::LightGrey: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY); break; - case Color::BrightRed: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_RED); break; - case Color::BrightGreen: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN); break; - case Color::BrightWhite: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break; - case Color::None: - case Color::Bright: // invalid - default: DOCTEST_SET_ATTR(ch.origFgAttrs); - } - // clang-format on -#endif // DOCTEST_CONFIG_COLORS_WINDOWS - } - DOCTEST_CLANG_SUPPRESS_WARNING_POP - - std::vector& getExceptionTranslators() { - static std::vector data; - return data; - } - - String translateActiveException() { -#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS - String res; - auto& translators = getExceptionTranslators(); - for(auto& curr : translators) - if(curr->translate(res)) - return res; - // clang-format off - DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wcatch-value") - try { - throw; - } catch(std::exception& ex) { - return ex.what(); - } catch(std::string& msg) { - return msg.c_str(); - } catch(const char* msg) { - return msg; - } catch(...) { - return "unknown exception"; - } - DOCTEST_GCC_SUPPRESS_WARNING_POP -// clang-format on -#else // DOCTEST_CONFIG_NO_EXCEPTIONS - return ""; -#endif // DOCTEST_CONFIG_NO_EXCEPTIONS - } -} // namespace - -namespace detail { - // used by the macros for registering tests - int regTest(const TestCase& tc) { - getRegisteredTests().insert(tc); - return 0; - } - - // sets the current test suite - int setTestSuite(const TestSuite& ts) { - doctest_detail_test_suite_ns::getCurrentTestSuite() = ts; - return 0; - } - -#ifdef DOCTEST_IS_DEBUGGER_ACTIVE - bool isDebuggerActive() { return DOCTEST_IS_DEBUGGER_ACTIVE(); } -#else // DOCTEST_IS_DEBUGGER_ACTIVE -#ifdef DOCTEST_PLATFORM_LINUX - class ErrnoGuard { - public: - ErrnoGuard() : m_oldErrno(errno) {} - ~ErrnoGuard() { errno = m_oldErrno; } - private: - int m_oldErrno; - }; - // See the comments in Catch2 for the reasoning behind this implementation: - // https://github.com/catchorg/Catch2/blob/v2.13.1/include/internal/catch_debugger.cpp#L79-L102 - bool isDebuggerActive() { - ErrnoGuard guard; - std::ifstream in("/proc/self/status"); - for(std::string line; std::getline(in, line);) { - static const int PREFIX_LEN = 11; - if(line.compare(0, PREFIX_LEN, "TracerPid:\t") == 0) { - return line.length() > PREFIX_LEN && line[PREFIX_LEN] != '0'; - } - } - return false; - } -#elif defined(DOCTEST_PLATFORM_MAC) - // The following function is taken directly from the following technical note: - // https://developer.apple.com/library/archive/qa/qa1361/_index.html - // Returns true if the current process is being debugged (either - // running under the debugger or has a debugger attached post facto). - bool isDebuggerActive() { - int mib[4]; - kinfo_proc info; - size_t size; - // Initialize the flags so that, if sysctl fails for some bizarre - // reason, we get a predictable result. - info.kp_proc.p_flag = 0; - // Initialize mib, which tells sysctl the info we want, in this case - // we're looking for information about a specific process ID. - mib[0] = CTL_KERN; - mib[1] = KERN_PROC; - mib[2] = KERN_PROC_PID; - mib[3] = getpid(); - // Call sysctl. - size = sizeof(info); - if(sysctl(mib, DOCTEST_COUNTOF(mib), &info, &size, 0, 0) != 0) { - std::cerr << "\nCall to sysctl failed - unable to determine if debugger is active **\n"; - return false; - } - // We're being debugged if the P_TRACED flag is set. - return ((info.kp_proc.p_flag & P_TRACED) != 0); - } -#elif DOCTEST_MSVC || defined(__MINGW32__) || defined(__MINGW64__) - bool isDebuggerActive() { return ::IsDebuggerPresent() != 0; } -#else - bool isDebuggerActive() { return false; } -#endif // Platform -#endif // DOCTEST_IS_DEBUGGER_ACTIVE - - void registerExceptionTranslatorImpl(const IExceptionTranslator* et) { - if(std::find(getExceptionTranslators().begin(), getExceptionTranslators().end(), et) == - getExceptionTranslators().end()) - getExceptionTranslators().push_back(et); - } - -#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING - void toStream(std::ostream* s, char* in) { *s << in; } - void toStream(std::ostream* s, const char* in) { *s << in; } -#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING - void toStream(std::ostream* s, bool in) { *s << std::boolalpha << in << std::noboolalpha; } - void toStream(std::ostream* s, float in) { *s << in; } - void toStream(std::ostream* s, double in) { *s << in; } - void toStream(std::ostream* s, double long in) { *s << in; } - - void toStream(std::ostream* s, char in) { *s << in; } - void toStream(std::ostream* s, char signed in) { *s << in; } - void toStream(std::ostream* s, char unsigned in) { *s << in; } - void toStream(std::ostream* s, int short in) { *s << in; } - void toStream(std::ostream* s, int short unsigned in) { *s << in; } - void toStream(std::ostream* s, int in) { *s << in; } - void toStream(std::ostream* s, int unsigned in) { *s << in; } - void toStream(std::ostream* s, int long in) { *s << in; } - void toStream(std::ostream* s, int long unsigned in) { *s << in; } - void toStream(std::ostream* s, int long long in) { *s << in; } - void toStream(std::ostream* s, int long long unsigned in) { *s << in; } - - DOCTEST_THREAD_LOCAL std::vector g_infoContexts; // for logging with INFO() - - ContextScopeBase::ContextScopeBase() { - g_infoContexts.push_back(this); - } - - ContextScopeBase::ContextScopeBase(ContextScopeBase&& other) { - if (other.need_to_destroy) { - other.destroy(); - } - other.need_to_destroy = false; - g_infoContexts.push_back(this); - } - - DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4996) // std::uncaught_exception is deprecated in C++17 - DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") - DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") - - // destroy cannot be inlined into the destructor because that would mean calling stringify after - // ContextScope has been destroyed (base class destructors run after derived class destructors). - // Instead, ContextScope calls this method directly from its destructor. - void ContextScopeBase::destroy() { -#if defined(__cpp_lib_uncaught_exceptions) && __cpp_lib_uncaught_exceptions >= 201411L && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200) - if(std::uncaught_exceptions() > 0) { -#else - if(std::uncaught_exception()) { -#endif - std::ostringstream s; - this->stringify(&s); - g_cs->stringifiedContexts.push_back(s.str().c_str()); - } - g_infoContexts.pop_back(); - } - - DOCTEST_CLANG_SUPPRESS_WARNING_POP - DOCTEST_GCC_SUPPRESS_WARNING_POP - DOCTEST_MSVC_SUPPRESS_WARNING_POP -} // namespace detail -namespace { - using namespace detail; - -#if !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && !defined(DOCTEST_CONFIG_WINDOWS_SEH) - struct FatalConditionHandler - { - static void reset() {} - static void allocateAltStackMem() {} - static void freeAltStackMem() {} - }; -#else // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH - - void reportFatal(const std::string&); - -#ifdef DOCTEST_PLATFORM_WINDOWS - - struct SignalDefs - { - DWORD id; - const char* name; - }; - // There is no 1-1 mapping between signals and windows exceptions. - // Windows can easily distinguish between SO and SigSegV, - // but SigInt, SigTerm, etc are handled differently. - SignalDefs signalDefs[] = { - {static_cast(EXCEPTION_ILLEGAL_INSTRUCTION), - "SIGILL - Illegal instruction signal"}, - {static_cast(EXCEPTION_STACK_OVERFLOW), "SIGSEGV - Stack overflow"}, - {static_cast(EXCEPTION_ACCESS_VIOLATION), - "SIGSEGV - Segmentation violation signal"}, - {static_cast(EXCEPTION_INT_DIVIDE_BY_ZERO), "Divide by zero error"}, - }; - - struct FatalConditionHandler - { - static LONG CALLBACK handleException(PEXCEPTION_POINTERS ExceptionInfo) { - // Multiple threads may enter this filter/handler at once. We want the error message to be printed on the - // console just once no matter how many threads have crashed. - static std::mutex mutex; - static bool execute = true; - { - std::lock_guard lock(mutex); - if(execute) { - bool reported = false; - for(size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { - if(ExceptionInfo->ExceptionRecord->ExceptionCode == signalDefs[i].id) { - reportFatal(signalDefs[i].name); - reported = true; - break; - } - } - if(reported == false) - reportFatal("Unhandled SEH exception caught"); - if(isDebuggerActive() && !g_cs->no_breaks) - DOCTEST_BREAK_INTO_DEBUGGER(); - } - execute = false; - } - std::exit(EXIT_FAILURE); - } - - static void allocateAltStackMem() {} - static void freeAltStackMem() {} - - FatalConditionHandler() { - isSet = true; - // 32k seems enough for doctest to handle stack overflow, - // but the value was found experimentally, so there is no strong guarantee - guaranteeSize = 32 * 1024; - // Register an unhandled exception filter - previousTop = SetUnhandledExceptionFilter(handleException); - // Pass in guarantee size to be filled - SetThreadStackGuarantee(&guaranteeSize); - - // On Windows uncaught exceptions from another thread, exceptions from - // destructors, or calls to std::terminate are not a SEH exception - - // The terminal handler gets called when: - // - std::terminate is called FROM THE TEST RUNNER THREAD - // - an exception is thrown from a destructor FROM THE TEST RUNNER THREAD - original_terminate_handler = std::get_terminate(); - std::set_terminate([]() DOCTEST_NOEXCEPT { - reportFatal("Terminate handler called"); - if(isDebuggerActive() && !g_cs->no_breaks) - DOCTEST_BREAK_INTO_DEBUGGER(); - std::exit(EXIT_FAILURE); // explicitly exit - otherwise the SIGABRT handler may be called as well - }); - - // SIGABRT is raised when: - // - std::terminate is called FROM A DIFFERENT THREAD - // - an exception is thrown from a destructor FROM A DIFFERENT THREAD - // - an uncaught exception is thrown FROM A DIFFERENT THREAD - prev_sigabrt_handler = std::signal(SIGABRT, [](int signal) DOCTEST_NOEXCEPT { - if(signal == SIGABRT) { - reportFatal("SIGABRT - Abort (abnormal termination) signal"); - if(isDebuggerActive() && !g_cs->no_breaks) - DOCTEST_BREAK_INTO_DEBUGGER(); - std::exit(EXIT_FAILURE); - } - }); - - // The following settings are taken from google test, and more - // specifically from UnitTest::Run() inside of gtest.cc - - // the user does not want to see pop-up dialogs about crashes - prev_error_mode_1 = SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOALIGNMENTFAULTEXCEPT | - SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX); - // This forces the abort message to go to stderr in all circumstances. - prev_error_mode_2 = _set_error_mode(_OUT_TO_STDERR); - // In the debug version, Visual Studio pops up a separate dialog - // offering a choice to debug the aborted program - we want to disable that. - prev_abort_behavior = _set_abort_behavior(0x0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); - // In debug mode, the Windows CRT can crash with an assertion over invalid - // input (e.g. passing an invalid file descriptor). The default handling - // for these assertions is to pop up a dialog and wait for user input. - // Instead ask the CRT to dump such assertions to stderr non-interactively. - prev_report_mode = _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG); - prev_report_file = _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR); - } - - static void reset() { - if(isSet) { - // Unregister handler and restore the old guarantee - SetUnhandledExceptionFilter(previousTop); - SetThreadStackGuarantee(&guaranteeSize); - std::set_terminate(original_terminate_handler); - std::signal(SIGABRT, prev_sigabrt_handler); - SetErrorMode(prev_error_mode_1); - _set_error_mode(prev_error_mode_2); - _set_abort_behavior(prev_abort_behavior, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); - static_cast(_CrtSetReportMode(_CRT_ASSERT, prev_report_mode)); - static_cast(_CrtSetReportFile(_CRT_ASSERT, prev_report_file)); - isSet = false; - } - } - - ~FatalConditionHandler() { reset(); } - - private: - static UINT prev_error_mode_1; - static int prev_error_mode_2; - static unsigned int prev_abort_behavior; - static int prev_report_mode; - static _HFILE prev_report_file; - static void (DOCTEST_CDECL *prev_sigabrt_handler)(int); - static std::terminate_handler original_terminate_handler; - static bool isSet; - static ULONG guaranteeSize; - static LPTOP_LEVEL_EXCEPTION_FILTER previousTop; - }; - - UINT FatalConditionHandler::prev_error_mode_1; - int FatalConditionHandler::prev_error_mode_2; - unsigned int FatalConditionHandler::prev_abort_behavior; - int FatalConditionHandler::prev_report_mode; - _HFILE FatalConditionHandler::prev_report_file; - void (DOCTEST_CDECL *FatalConditionHandler::prev_sigabrt_handler)(int); - std::terminate_handler FatalConditionHandler::original_terminate_handler; - bool FatalConditionHandler::isSet = false; - ULONG FatalConditionHandler::guaranteeSize = 0; - LPTOP_LEVEL_EXCEPTION_FILTER FatalConditionHandler::previousTop = nullptr; - -#else // DOCTEST_PLATFORM_WINDOWS - - struct SignalDefs - { - int id; - const char* name; - }; - SignalDefs signalDefs[] = {{SIGINT, "SIGINT - Terminal interrupt signal"}, - {SIGILL, "SIGILL - Illegal instruction signal"}, - {SIGFPE, "SIGFPE - Floating point error signal"}, - {SIGSEGV, "SIGSEGV - Segmentation violation signal"}, - {SIGTERM, "SIGTERM - Termination request signal"}, - {SIGABRT, "SIGABRT - Abort (abnormal termination) signal"}}; - - struct FatalConditionHandler - { - static bool isSet; - static struct sigaction oldSigActions[DOCTEST_COUNTOF(signalDefs)]; - static stack_t oldSigStack; - static size_t altStackSize; - static char* altStackMem; - - static void handleSignal(int sig) { - const char* name = ""; - for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { - SignalDefs& def = signalDefs[i]; - if(sig == def.id) { - name = def.name; - break; - } - } - reset(); - reportFatal(name); - raise(sig); - } - - static void allocateAltStackMem() { - altStackMem = new char[altStackSize]; - } - - static void freeAltStackMem() { - delete[] altStackMem; - } - - FatalConditionHandler() { - isSet = true; - stack_t sigStack; - sigStack.ss_sp = altStackMem; - sigStack.ss_size = altStackSize; - sigStack.ss_flags = 0; - sigaltstack(&sigStack, &oldSigStack); - struct sigaction sa = {}; - sa.sa_handler = handleSignal; // NOLINT - sa.sa_flags = SA_ONSTACK; - for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { - sigaction(signalDefs[i].id, &sa, &oldSigActions[i]); - } - } - - ~FatalConditionHandler() { reset(); } - static void reset() { - if(isSet) { - // Set signals back to previous values -- hopefully nobody overwrote them in the meantime - for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { - sigaction(signalDefs[i].id, &oldSigActions[i], nullptr); - } - // Return the old stack - sigaltstack(&oldSigStack, nullptr); - isSet = false; - } - } - }; - - bool FatalConditionHandler::isSet = false; - struct sigaction FatalConditionHandler::oldSigActions[DOCTEST_COUNTOF(signalDefs)] = {}; - stack_t FatalConditionHandler::oldSigStack = {}; - size_t FatalConditionHandler::altStackSize = 4 * SIGSTKSZ; - char* FatalConditionHandler::altStackMem = nullptr; - -#endif // DOCTEST_PLATFORM_WINDOWS -#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH - -} // namespace - -namespace { - using namespace detail; - -#ifdef DOCTEST_PLATFORM_WINDOWS -#define DOCTEST_OUTPUT_DEBUG_STRING(text) ::OutputDebugStringA(text) -#else - // TODO: integration with XCode and other IDEs -#define DOCTEST_OUTPUT_DEBUG_STRING(text) // NOLINT(clang-diagnostic-unused-macros) -#endif // Platform - - void addAssert(assertType::Enum at) { - if((at & assertType::is_warn) == 0) //!OCLINT bitwise operator in conditional - g_cs->numAssertsCurrentTest_atomic++; - } - - void addFailedAssert(assertType::Enum at) { - if((at & assertType::is_warn) == 0) //!OCLINT bitwise operator in conditional - g_cs->numAssertsFailedCurrentTest_atomic++; - } - -#if defined(DOCTEST_CONFIG_POSIX_SIGNALS) || defined(DOCTEST_CONFIG_WINDOWS_SEH) - void reportFatal(const std::string& message) { - g_cs->failure_flags |= TestCaseFailureReason::Crash; - - DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception, {message.c_str(), true}); - - while(g_cs->subcasesStack.size()) { - g_cs->subcasesStack.pop_back(); - DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY); - } - - g_cs->finalizeTestCaseData(); - - DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs); - - DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs); - } -#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH -} // namespace -namespace detail { - - ResultBuilder::ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr, - const char* exception_type, const char* exception_string) { - m_test_case = g_cs->currentTest; - m_at = at; - m_file = file; - m_line = line; - m_expr = expr; - m_failed = true; - m_threw = false; - m_threw_as = false; - m_exception_type = exception_type; - m_exception_string = exception_string; -#if DOCTEST_MSVC - if(m_expr[0] == ' ') // this happens when variadic macros are disabled under MSVC - ++m_expr; -#endif // MSVC - } - - void ResultBuilder::setResult(const Result& res) { - m_decomp = res.m_decomp; - m_failed = !res.m_passed; - } - - void ResultBuilder::translateException() { - m_threw = true; - m_exception = translateActiveException(); - } - - bool ResultBuilder::log() { - if(m_at & assertType::is_throws) { //!OCLINT bitwise operator in conditional - m_failed = !m_threw; - } else if((m_at & assertType::is_throws_as) && (m_at & assertType::is_throws_with)) { //!OCLINT - m_failed = !m_threw_as || (m_exception != m_exception_string); - } else if(m_at & assertType::is_throws_as) { //!OCLINT bitwise operator in conditional - m_failed = !m_threw_as; - } else if(m_at & assertType::is_throws_with) { //!OCLINT bitwise operator in conditional - m_failed = m_exception != m_exception_string; - } else if(m_at & assertType::is_nothrow) { //!OCLINT bitwise operator in conditional - m_failed = m_threw; - } - - if(m_exception.size()) - m_exception = "\"" + m_exception + "\""; - - if(is_running_in_test) { - addAssert(m_at); - DOCTEST_ITERATE_THROUGH_REPORTERS(log_assert, *this); - - if(m_failed) - addFailedAssert(m_at); - } else if(m_failed) { - failed_out_of_a_testing_context(*this); - } - - return m_failed && isDebuggerActive() && !getContextOptions()->no_breaks && - (g_cs->currentTest == nullptr || !g_cs->currentTest->m_no_breaks); // break into debugger - } - - void ResultBuilder::react() const { - if(m_failed && checkIfShouldThrow(m_at)) - throwException(); - } - - void failed_out_of_a_testing_context(const AssertData& ad) { - if(g_cs->ah) - g_cs->ah(ad); - else - std::abort(); - } - - bool decomp_assert(assertType::Enum at, const char* file, int line, const char* expr, - Result result) { - bool failed = !result.m_passed; - - // ################################################################################### - // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT - // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED - // ################################################################################### - DOCTEST_ASSERT_OUT_OF_TESTS(result.m_decomp); - DOCTEST_ASSERT_IN_TESTS(result.m_decomp); - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) - return !failed; - } - - MessageBuilder::MessageBuilder(const char* file, int line, assertType::Enum severity) { - m_stream = tlssPush(); - m_file = file; - m_line = line; - m_severity = severity; - } - - MessageBuilder::~MessageBuilder() { - if (!logged) - tlssPop(); - } - - IExceptionTranslator::IExceptionTranslator() = default; - IExceptionTranslator::~IExceptionTranslator() = default; - - bool MessageBuilder::log() { - if (!logged) { - m_string = tlssPop(); - logged = true; - } - - DOCTEST_ITERATE_THROUGH_REPORTERS(log_message, *this); - - const bool isWarn = m_severity & assertType::is_warn; - - // warn is just a message in this context so we don't treat it as an assert - if(!isWarn) { - addAssert(m_severity); - addFailedAssert(m_severity); - } - - return isDebuggerActive() && !getContextOptions()->no_breaks && !isWarn && - (g_cs->currentTest == nullptr || !g_cs->currentTest->m_no_breaks); // break into debugger - } - - void MessageBuilder::react() { - if(m_severity & assertType::is_require) //!OCLINT bitwise operator in conditional - throwException(); - } -} // namespace detail -namespace { - using namespace detail; - - // clang-format off - -// ================================================================================================= -// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.h/cpp -// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched. -// ================================================================================================= - - class XmlEncode { - public: - enum ForWhat { ForTextNodes, ForAttributes }; - - XmlEncode( std::string const& str, ForWhat forWhat = ForTextNodes ); - - void encodeTo( std::ostream& os ) const; - - friend std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ); - - private: - std::string m_str; - ForWhat m_forWhat; - }; - - class XmlWriter { - public: - - class ScopedElement { - public: - ScopedElement( XmlWriter* writer ); - - ScopedElement( ScopedElement&& other ) DOCTEST_NOEXCEPT; - ScopedElement& operator=( ScopedElement&& other ) DOCTEST_NOEXCEPT; - - ~ScopedElement(); - - ScopedElement& writeText( std::string const& text, bool indent = true ); - - template - ScopedElement& writeAttribute( std::string const& name, T const& attribute ) { - m_writer->writeAttribute( name, attribute ); - return *this; - } - - private: - mutable XmlWriter* m_writer = nullptr; - }; - - XmlWriter( std::ostream& os = std::cout ); - ~XmlWriter(); - - XmlWriter( XmlWriter const& ) = delete; - XmlWriter& operator=( XmlWriter const& ) = delete; - - XmlWriter& startElement( std::string const& name ); - - ScopedElement scopedElement( std::string const& name ); - - XmlWriter& endElement(); - - XmlWriter& writeAttribute( std::string const& name, std::string const& attribute ); - - XmlWriter& writeAttribute( std::string const& name, const char* attribute ); - - XmlWriter& writeAttribute( std::string const& name, bool attribute ); - - template - XmlWriter& writeAttribute( std::string const& name, T const& attribute ) { - std::stringstream rss; - rss << attribute; - return writeAttribute( name, rss.str() ); - } - - XmlWriter& writeText( std::string const& text, bool indent = true ); - - //XmlWriter& writeComment( std::string const& text ); - - //void writeStylesheetRef( std::string const& url ); - - //XmlWriter& writeBlankLine(); - - void ensureTagClosed(); - - private: - - void writeDeclaration(); - - void newlineIfNecessary(); - - bool m_tagIsOpen = false; - bool m_needsNewline = false; - std::vector m_tags; - std::string m_indent; - std::ostream& m_os; - }; - -// ================================================================================================= -// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.h/cpp -// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched. -// ================================================================================================= - -using uchar = unsigned char; - -namespace { - - size_t trailingBytes(unsigned char c) { - if ((c & 0xE0) == 0xC0) { - return 2; - } - if ((c & 0xF0) == 0xE0) { - return 3; - } - if ((c & 0xF8) == 0xF0) { - return 4; - } - DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered"); - } - - uint32_t headerValue(unsigned char c) { - if ((c & 0xE0) == 0xC0) { - return c & 0x1F; - } - if ((c & 0xF0) == 0xE0) { - return c & 0x0F; - } - if ((c & 0xF8) == 0xF0) { - return c & 0x07; - } - DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered"); - } - - void hexEscapeChar(std::ostream& os, unsigned char c) { - std::ios_base::fmtflags f(os.flags()); - os << "\\x" - << std::uppercase << std::hex << std::setfill('0') << std::setw(2) - << static_cast(c); - os.flags(f); - } - -} // anonymous namespace - - XmlEncode::XmlEncode( std::string const& str, ForWhat forWhat ) - : m_str( str ), - m_forWhat( forWhat ) - {} - - void XmlEncode::encodeTo( std::ostream& os ) const { - // Apostrophe escaping not necessary if we always use " to write attributes - // (see: https://www.w3.org/TR/xml/#syntax) - - for( std::size_t idx = 0; idx < m_str.size(); ++ idx ) { - uchar c = m_str[idx]; - switch (c) { - case '<': os << "<"; break; - case '&': os << "&"; break; - - case '>': - // See: https://www.w3.org/TR/xml/#syntax - if (idx > 2 && m_str[idx - 1] == ']' && m_str[idx - 2] == ']') - os << ">"; - else - os << c; - break; - - case '\"': - if (m_forWhat == ForAttributes) - os << """; - else - os << c; - break; - - default: - // Check for control characters and invalid utf-8 - - // Escape control characters in standard ascii - // see https://stackoverflow.com/questions/404107/why-are-control-characters-illegal-in-xml-1-0 - if (c < 0x09 || (c > 0x0D && c < 0x20) || c == 0x7F) { - hexEscapeChar(os, c); - break; - } - - // Plain ASCII: Write it to stream - if (c < 0x7F) { - os << c; - break; - } - - // UTF-8 territory - // Check if the encoding is valid and if it is not, hex escape bytes. - // Important: We do not check the exact decoded values for validity, only the encoding format - // First check that this bytes is a valid lead byte: - // This means that it is not encoded as 1111 1XXX - // Or as 10XX XXXX - if (c < 0xC0 || - c >= 0xF8) { - hexEscapeChar(os, c); - break; - } - - auto encBytes = trailingBytes(c); - // Are there enough bytes left to avoid accessing out-of-bounds memory? - if (idx + encBytes - 1 >= m_str.size()) { - hexEscapeChar(os, c); - break; - } - // The header is valid, check data - // The next encBytes bytes must together be a valid utf-8 - // This means: bitpattern 10XX XXXX and the extracted value is sane (ish) - bool valid = true; - uint32_t value = headerValue(c); - for (std::size_t n = 1; n < encBytes; ++n) { - uchar nc = m_str[idx + n]; - valid &= ((nc & 0xC0) == 0x80); - value = (value << 6) | (nc & 0x3F); - } - - if ( - // Wrong bit pattern of following bytes - (!valid) || - // Overlong encodings - (value < 0x80) || - ( value < 0x800 && encBytes > 2) || // removed "0x80 <= value &&" because redundant - (0x800 < value && value < 0x10000 && encBytes > 3) || - // Encoded value out of range - (value >= 0x110000) - ) { - hexEscapeChar(os, c); - break; - } - - // If we got here, this is in fact a valid(ish) utf-8 sequence - for (std::size_t n = 0; n < encBytes; ++n) { - os << m_str[idx + n]; - } - idx += encBytes - 1; - break; - } - } - } - - std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ) { - xmlEncode.encodeTo( os ); - return os; - } - - XmlWriter::ScopedElement::ScopedElement( XmlWriter* writer ) - : m_writer( writer ) - {} - - XmlWriter::ScopedElement::ScopedElement( ScopedElement&& other ) DOCTEST_NOEXCEPT - : m_writer( other.m_writer ){ - other.m_writer = nullptr; - } - XmlWriter::ScopedElement& XmlWriter::ScopedElement::operator=( ScopedElement&& other ) DOCTEST_NOEXCEPT { - if ( m_writer ) { - m_writer->endElement(); - } - m_writer = other.m_writer; - other.m_writer = nullptr; - return *this; - } - - - XmlWriter::ScopedElement::~ScopedElement() { - if( m_writer ) - m_writer->endElement(); - } - - XmlWriter::ScopedElement& XmlWriter::ScopedElement::writeText( std::string const& text, bool indent ) { - m_writer->writeText( text, indent ); - return *this; - } - - XmlWriter::XmlWriter( std::ostream& os ) : m_os( os ) - { - writeDeclaration(); - } - - XmlWriter::~XmlWriter() { - while( !m_tags.empty() ) - endElement(); - } - - XmlWriter& XmlWriter::startElement( std::string const& name ) { - ensureTagClosed(); - newlineIfNecessary(); - m_os << m_indent << '<' << name; - m_tags.push_back( name ); - m_indent += " "; - m_tagIsOpen = true; - return *this; - } - - XmlWriter::ScopedElement XmlWriter::scopedElement( std::string const& name ) { - ScopedElement scoped( this ); - startElement( name ); - return scoped; - } - - XmlWriter& XmlWriter::endElement() { - newlineIfNecessary(); - m_indent = m_indent.substr( 0, m_indent.size()-2 ); - if( m_tagIsOpen ) { - m_os << "/>"; - m_tagIsOpen = false; - } - else { - m_os << m_indent << ""; - } - m_os << std::endl; - m_tags.pop_back(); - return *this; - } - - XmlWriter& XmlWriter::writeAttribute( std::string const& name, std::string const& attribute ) { - if( !name.empty() && !attribute.empty() ) - m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"'; - return *this; - } - - XmlWriter& XmlWriter::writeAttribute( std::string const& name, const char* attribute ) { - if( !name.empty() && attribute && attribute[0] != '\0' ) - m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"'; - return *this; - } - - XmlWriter& XmlWriter::writeAttribute( std::string const& name, bool attribute ) { - m_os << ' ' << name << "=\"" << ( attribute ? "true" : "false" ) << '"'; - return *this; - } - - XmlWriter& XmlWriter::writeText( std::string const& text, bool indent ) { - if( !text.empty() ){ - bool tagWasOpen = m_tagIsOpen; - ensureTagClosed(); - if( tagWasOpen && indent ) - m_os << m_indent; - m_os << XmlEncode( text ); - m_needsNewline = true; - } - return *this; - } - - //XmlWriter& XmlWriter::writeComment( std::string const& text ) { - // ensureTagClosed(); - // m_os << m_indent << ""; - // m_needsNewline = true; - // return *this; - //} - - //void XmlWriter::writeStylesheetRef( std::string const& url ) { - // m_os << "\n"; - //} - - //XmlWriter& XmlWriter::writeBlankLine() { - // ensureTagClosed(); - // m_os << '\n'; - // return *this; - //} - - void XmlWriter::ensureTagClosed() { - if( m_tagIsOpen ) { - m_os << ">" << std::endl; - m_tagIsOpen = false; - } - } - - void XmlWriter::writeDeclaration() { - m_os << "\n"; - } - - void XmlWriter::newlineIfNecessary() { - if( m_needsNewline ) { - m_os << std::endl; - m_needsNewline = false; - } - } - -// ================================================================================================= -// End of copy-pasted code from Catch -// ================================================================================================= - - // clang-format on - - struct XmlReporter : public IReporter - { - XmlWriter xml; - std::mutex mutex; - - // caching pointers/references to objects of these types - safe to do - const ContextOptions& opt; - const TestCaseData* tc = nullptr; - - XmlReporter(const ContextOptions& co) - : xml(*co.cout) - , opt(co) {} - - void log_contexts() { - int num_contexts = get_num_active_contexts(); - if(num_contexts) { - auto contexts = get_active_contexts(); - std::stringstream ss; - for(int i = 0; i < num_contexts; ++i) { - contexts[i]->stringify(&ss); - xml.scopedElement("Info").writeText(ss.str()); - ss.str(""); - } - } - } - - unsigned line(unsigned l) const { return opt.no_line_numbers ? 0 : l; } - - void test_case_start_impl(const TestCaseData& in) { - bool open_ts_tag = false; - if(tc != nullptr) { // we have already opened a test suite - if(std::strcmp(tc->m_test_suite, in.m_test_suite) != 0) { - xml.endElement(); - open_ts_tag = true; - } - } - else { - open_ts_tag = true; // first test case ==> first test suite - } - - if(open_ts_tag) { - xml.startElement("TestSuite"); - xml.writeAttribute("name", in.m_test_suite); - } - - tc = ∈ - xml.startElement("TestCase") - .writeAttribute("name", in.m_name) - .writeAttribute("filename", skipPathFromFilename(in.m_file.c_str())) - .writeAttribute("line", line(in.m_line)) - .writeAttribute("description", in.m_description); - - if(Approx(in.m_timeout) != 0) - xml.writeAttribute("timeout", in.m_timeout); - if(in.m_may_fail) - xml.writeAttribute("may_fail", true); - if(in.m_should_fail) - xml.writeAttribute("should_fail", true); - } - - // ========================================================================================= - // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE - // ========================================================================================= - - void report_query(const QueryData& in) override { - test_run_start(); - if(opt.list_reporters) { - for(auto& curr : getListeners()) - xml.scopedElement("Listener") - .writeAttribute("priority", curr.first.first) - .writeAttribute("name", curr.first.second); - for(auto& curr : getReporters()) - xml.scopedElement("Reporter") - .writeAttribute("priority", curr.first.first) - .writeAttribute("name", curr.first.second); - } else if(opt.count || opt.list_test_cases) { - for(unsigned i = 0; i < in.num_data; ++i) { - xml.scopedElement("TestCase").writeAttribute("name", in.data[i]->m_name) - .writeAttribute("testsuite", in.data[i]->m_test_suite) - .writeAttribute("filename", skipPathFromFilename(in.data[i]->m_file.c_str())) - .writeAttribute("line", line(in.data[i]->m_line)) - .writeAttribute("skipped", in.data[i]->m_skip); - } - xml.scopedElement("OverallResultsTestCases") - .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters); - } else if(opt.list_test_suites) { - for(unsigned i = 0; i < in.num_data; ++i) - xml.scopedElement("TestSuite").writeAttribute("name", in.data[i]->m_test_suite); - xml.scopedElement("OverallResultsTestCases") - .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters); - xml.scopedElement("OverallResultsTestSuites") - .writeAttribute("unskipped", in.run_stats->numTestSuitesPassingFilters); - } - xml.endElement(); - } - - void test_run_start() override { - // remove .exe extension - mainly to have the same output on UNIX and Windows - std::string binary_name = skipPathFromFilename(opt.binary_name.c_str()); -#ifdef DOCTEST_PLATFORM_WINDOWS - if(binary_name.rfind(".exe") != std::string::npos) - binary_name = binary_name.substr(0, binary_name.length() - 4); -#endif // DOCTEST_PLATFORM_WINDOWS - - xml.startElement("doctest").writeAttribute("binary", binary_name); - if(opt.no_version == false) - xml.writeAttribute("version", DOCTEST_VERSION_STR); - - // only the consequential ones (TODO: filters) - xml.scopedElement("Options") - .writeAttribute("order_by", opt.order_by.c_str()) - .writeAttribute("rand_seed", opt.rand_seed) - .writeAttribute("first", opt.first) - .writeAttribute("last", opt.last) - .writeAttribute("abort_after", opt.abort_after) - .writeAttribute("subcase_filter_levels", opt.subcase_filter_levels) - .writeAttribute("case_sensitive", opt.case_sensitive) - .writeAttribute("no_throw", opt.no_throw) - .writeAttribute("no_skip", opt.no_skip); - } - - void test_run_end(const TestRunStats& p) override { - if(tc) // the TestSuite tag - only if there has been at least 1 test case - xml.endElement(); - - xml.scopedElement("OverallResultsAsserts") - .writeAttribute("successes", p.numAsserts - p.numAssertsFailed) - .writeAttribute("failures", p.numAssertsFailed); - - xml.startElement("OverallResultsTestCases") - .writeAttribute("successes", - p.numTestCasesPassingFilters - p.numTestCasesFailed) - .writeAttribute("failures", p.numTestCasesFailed); - if(opt.no_skipped_summary == false) - xml.writeAttribute("skipped", p.numTestCases - p.numTestCasesPassingFilters); - xml.endElement(); - - xml.endElement(); - } - - void test_case_start(const TestCaseData& in) override { - test_case_start_impl(in); - xml.ensureTagClosed(); - } - - void test_case_reenter(const TestCaseData&) override {} - - void test_case_end(const CurrentTestCaseStats& st) override { - xml.startElement("OverallResultsAsserts") - .writeAttribute("successes", - st.numAssertsCurrentTest - st.numAssertsFailedCurrentTest) - .writeAttribute("failures", st.numAssertsFailedCurrentTest) - .writeAttribute("test_case_success", st.testCaseSuccess); - if(opt.duration) - xml.writeAttribute("duration", st.seconds); - if(tc->m_expected_failures) - xml.writeAttribute("expected_failures", tc->m_expected_failures); - xml.endElement(); - - xml.endElement(); - } - - void test_case_exception(const TestCaseException& e) override { - std::lock_guard lock(mutex); - - xml.scopedElement("Exception") - .writeAttribute("crash", e.is_crash) - .writeText(e.error_string.c_str()); - } - - void subcase_start(const SubcaseSignature& in) override { - xml.startElement("SubCase") - .writeAttribute("name", in.m_name) - .writeAttribute("filename", skipPathFromFilename(in.m_file)) - .writeAttribute("line", line(in.m_line)); - xml.ensureTagClosed(); - } - - void subcase_end() override { xml.endElement(); } - - void log_assert(const AssertData& rb) override { - if(!rb.m_failed && !opt.success) - return; - - std::lock_guard lock(mutex); - - xml.startElement("Expression") - .writeAttribute("success", !rb.m_failed) - .writeAttribute("type", assertString(rb.m_at)) - .writeAttribute("filename", skipPathFromFilename(rb.m_file)) - .writeAttribute("line", line(rb.m_line)); - - xml.scopedElement("Original").writeText(rb.m_expr); - - if(rb.m_threw) - xml.scopedElement("Exception").writeText(rb.m_exception.c_str()); - - if(rb.m_at & assertType::is_throws_as) - xml.scopedElement("ExpectedException").writeText(rb.m_exception_type); - if(rb.m_at & assertType::is_throws_with) - xml.scopedElement("ExpectedExceptionString").writeText(rb.m_exception_string); - if((rb.m_at & assertType::is_normal) && !rb.m_threw) - xml.scopedElement("Expanded").writeText(rb.m_decomp.c_str()); - - log_contexts(); - - xml.endElement(); - } - - void log_message(const MessageData& mb) override { - std::lock_guard lock(mutex); - - xml.startElement("Message") - .writeAttribute("type", failureString(mb.m_severity)) - .writeAttribute("filename", skipPathFromFilename(mb.m_file)) - .writeAttribute("line", line(mb.m_line)); - - xml.scopedElement("Text").writeText(mb.m_string.c_str()); - - log_contexts(); - - xml.endElement(); - } - - void test_case_skipped(const TestCaseData& in) override { - if(opt.no_skipped_summary == false) { - test_case_start_impl(in); - xml.writeAttribute("skipped", "true"); - xml.endElement(); - } - } - }; - - DOCTEST_REGISTER_REPORTER("xml", 0, XmlReporter); - - void fulltext_log_assert_to_stream(std::ostream& s, const AssertData& rb) { - if((rb.m_at & (assertType::is_throws_as | assertType::is_throws_with)) == - 0) //!OCLINT bitwise operator in conditional - s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << " ) " - << Color::None; - - if(rb.m_at & assertType::is_throws) { //!OCLINT bitwise operator in conditional - s << (rb.m_threw ? "threw as expected!" : "did NOT throw at all!") << "\n"; - } else if((rb.m_at & assertType::is_throws_as) && - (rb.m_at & assertType::is_throws_with)) { //!OCLINT - s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \"" - << rb.m_exception_string << "\", " << rb.m_exception_type << " ) " << Color::None; - if(rb.m_threw) { - if(!rb.m_failed) { - s << "threw as expected!\n"; - } else { - s << "threw a DIFFERENT exception! (contents: " << rb.m_exception << ")\n"; - } - } else { - s << "did NOT throw at all!\n"; - } - } else if(rb.m_at & - assertType::is_throws_as) { //!OCLINT bitwise operator in conditional - s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", " - << rb.m_exception_type << " ) " << Color::None - << (rb.m_threw ? (rb.m_threw_as ? "threw as expected!" : - "threw a DIFFERENT exception: ") : - "did NOT throw at all!") - << Color::Cyan << rb.m_exception << "\n"; - } else if(rb.m_at & - assertType::is_throws_with) { //!OCLINT bitwise operator in conditional - s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \"" - << rb.m_exception_string << "\" ) " << Color::None - << (rb.m_threw ? (!rb.m_failed ? "threw as expected!" : - "threw a DIFFERENT exception: ") : - "did NOT throw at all!") - << Color::Cyan << rb.m_exception << "\n"; - } else if(rb.m_at & assertType::is_nothrow) { //!OCLINT bitwise operator in conditional - s << (rb.m_threw ? "THREW exception: " : "didn't throw!") << Color::Cyan - << rb.m_exception << "\n"; - } else { - s << (rb.m_threw ? "THREW exception: " : - (!rb.m_failed ? "is correct!\n" : "is NOT correct!\n")); - if(rb.m_threw) - s << rb.m_exception << "\n"; - else - s << " values: " << assertString(rb.m_at) << "( " << rb.m_decomp << " )\n"; - } - } - - // TODO: - // - log_message() - // - respond to queries - // - honor remaining options - // - more attributes in tags - struct JUnitReporter : public IReporter - { - XmlWriter xml; - std::mutex mutex; - Timer timer; - std::vector deepestSubcaseStackNames; - - struct JUnitTestCaseData - { - static std::string getCurrentTimestamp() { - // Beware, this is not reentrant because of backward compatibility issues - // Also, UTC only, again because of backward compatibility (%z is C++11) - time_t rawtime; - std::time(&rawtime); - auto const timeStampSize = sizeof("2017-01-16T17:06:45Z"); - - std::tm timeInfo; -#ifdef DOCTEST_PLATFORM_WINDOWS - gmtime_s(&timeInfo, &rawtime); -#else // DOCTEST_PLATFORM_WINDOWS - gmtime_r(&rawtime, &timeInfo); -#endif // DOCTEST_PLATFORM_WINDOWS - - char timeStamp[timeStampSize]; - const char* const fmt = "%Y-%m-%dT%H:%M:%SZ"; - - std::strftime(timeStamp, timeStampSize, fmt, &timeInfo); - return std::string(timeStamp); - } - - struct JUnitTestMessage - { - JUnitTestMessage(const std::string& _message, const std::string& _type, const std::string& _details) - : message(_message), type(_type), details(_details) {} - - JUnitTestMessage(const std::string& _message, const std::string& _details) - : message(_message), type(), details(_details) {} - - std::string message, type, details; - }; - - struct JUnitTestCase - { - JUnitTestCase(const std::string& _classname, const std::string& _name) - : classname(_classname), name(_name), time(0), failures() {} - - std::string classname, name; - double time; - std::vector failures, errors; - }; - - void add(const std::string& classname, const std::string& name) { - testcases.emplace_back(classname, name); - } - - void appendSubcaseNamesToLastTestcase(std::vector nameStack) { - for(auto& curr: nameStack) - if(curr.size()) - testcases.back().name += std::string("/") + curr.c_str(); - } - - void addTime(double time) { - if(time < 1e-4) - time = 0; - testcases.back().time = time; - totalSeconds += time; - } - - void addFailure(const std::string& message, const std::string& type, const std::string& details) { - testcases.back().failures.emplace_back(message, type, details); - ++totalFailures; - } - - void addError(const std::string& message, const std::string& details) { - testcases.back().errors.emplace_back(message, details); - ++totalErrors; - } - - std::vector testcases; - double totalSeconds = 0; - int totalErrors = 0, totalFailures = 0; - }; - - JUnitTestCaseData testCaseData; - - // caching pointers/references to objects of these types - safe to do - const ContextOptions& opt; - const TestCaseData* tc = nullptr; - - JUnitReporter(const ContextOptions& co) - : xml(*co.cout) - , opt(co) {} - - unsigned line(unsigned l) const { return opt.no_line_numbers ? 0 : l; } - - // ========================================================================================= - // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE - // ========================================================================================= - - void report_query(const QueryData&) override {} - - void test_run_start() override {} - - void test_run_end(const TestRunStats& p) override { - // remove .exe extension - mainly to have the same output on UNIX and Windows - std::string binary_name = skipPathFromFilename(opt.binary_name.c_str()); -#ifdef DOCTEST_PLATFORM_WINDOWS - if(binary_name.rfind(".exe") != std::string::npos) - binary_name = binary_name.substr(0, binary_name.length() - 4); -#endif // DOCTEST_PLATFORM_WINDOWS - xml.startElement("testsuites"); - xml.startElement("testsuite").writeAttribute("name", binary_name) - .writeAttribute("errors", testCaseData.totalErrors) - .writeAttribute("failures", testCaseData.totalFailures) - .writeAttribute("tests", p.numAsserts); - if(opt.no_time_in_output == false) { - xml.writeAttribute("time", testCaseData.totalSeconds); - xml.writeAttribute("timestamp", JUnitTestCaseData::getCurrentTimestamp()); - } - if(opt.no_version == false) - xml.writeAttribute("doctest_version", DOCTEST_VERSION_STR); - - for(const auto& testCase : testCaseData.testcases) { - xml.startElement("testcase") - .writeAttribute("classname", testCase.classname) - .writeAttribute("name", testCase.name); - if(opt.no_time_in_output == false) - xml.writeAttribute("time", testCase.time); - // This is not ideal, but it should be enough to mimic gtest's junit output. - xml.writeAttribute("status", "run"); - - for(const auto& failure : testCase.failures) { - xml.scopedElement("failure") - .writeAttribute("message", failure.message) - .writeAttribute("type", failure.type) - .writeText(failure.details, false); - } - - for(const auto& error : testCase.errors) { - xml.scopedElement("error") - .writeAttribute("message", error.message) - .writeText(error.details); - } - - xml.endElement(); - } - xml.endElement(); - xml.endElement(); - } - - void test_case_start(const TestCaseData& in) override { - testCaseData.add(skipPathFromFilename(in.m_file.c_str()), in.m_name); - timer.start(); - } - - void test_case_reenter(const TestCaseData& in) override { - testCaseData.addTime(timer.getElapsedSeconds()); - testCaseData.appendSubcaseNamesToLastTestcase(deepestSubcaseStackNames); - deepestSubcaseStackNames.clear(); - - timer.start(); - testCaseData.add(skipPathFromFilename(in.m_file.c_str()), in.m_name); - } - - void test_case_end(const CurrentTestCaseStats&) override { - testCaseData.addTime(timer.getElapsedSeconds()); - testCaseData.appendSubcaseNamesToLastTestcase(deepestSubcaseStackNames); - deepestSubcaseStackNames.clear(); - } - - void test_case_exception(const TestCaseException& e) override { - std::lock_guard lock(mutex); - testCaseData.addError("exception", e.error_string.c_str()); - } - - void subcase_start(const SubcaseSignature& in) override { - deepestSubcaseStackNames.push_back(in.m_name); - } - - void subcase_end() override {} - - void log_assert(const AssertData& rb) override { - if(!rb.m_failed) // report only failures & ignore the `success` option - return; - - std::lock_guard lock(mutex); - - std::ostringstream os; - os << skipPathFromFilename(rb.m_file) << (opt.gnu_file_line ? ":" : "(") - << line(rb.m_line) << (opt.gnu_file_line ? ":" : "):") << std::endl; - - fulltext_log_assert_to_stream(os, rb); - log_contexts(os); - testCaseData.addFailure(rb.m_decomp.c_str(), assertString(rb.m_at), os.str()); - } - - void log_message(const MessageData&) override {} - - void test_case_skipped(const TestCaseData&) override {} - - void log_contexts(std::ostringstream& s) { - int num_contexts = get_num_active_contexts(); - if(num_contexts) { - auto contexts = get_active_contexts(); - - s << " logged: "; - for(int i = 0; i < num_contexts; ++i) { - s << (i == 0 ? "" : " "); - contexts[i]->stringify(&s); - s << std::endl; - } - } - } - }; - - DOCTEST_REGISTER_REPORTER("junit", 0, JUnitReporter); - - struct Whitespace - { - int nrSpaces; - explicit Whitespace(int nr) - : nrSpaces(nr) {} - }; - - std::ostream& operator<<(std::ostream& out, const Whitespace& ws) { - if(ws.nrSpaces != 0) - out << std::setw(ws.nrSpaces) << ' '; - return out; - } - - struct ConsoleReporter : public IReporter - { - std::ostream& s; - bool hasLoggedCurrentTestStart; - std::vector subcasesStack; - size_t currentSubcaseLevel; - std::mutex mutex; - - // caching pointers/references to objects of these types - safe to do - const ContextOptions& opt; - const TestCaseData* tc; - - ConsoleReporter(const ContextOptions& co) - : s(*co.cout) - , opt(co) {} - - ConsoleReporter(const ContextOptions& co, std::ostream& ostr) - : s(ostr) - , opt(co) {} - - // ========================================================================================= - // WHAT FOLLOWS ARE HELPERS USED BY THE OVERRIDES OF THE VIRTUAL METHODS OF THE INTERFACE - // ========================================================================================= - - void separator_to_stream() { - s << Color::Yellow - << "===============================================================================" - "\n"; - } - - const char* getSuccessOrFailString(bool success, assertType::Enum at, - const char* success_str) { - if(success) - return success_str; - return failureString(at); - } - - Color::Enum getSuccessOrFailColor(bool success, assertType::Enum at) { - return success ? Color::BrightGreen : - (at & assertType::is_warn) ? Color::Yellow : Color::Red; - } - - void successOrFailColoredStringToStream(bool success, assertType::Enum at, - const char* success_str = "SUCCESS") { - s << getSuccessOrFailColor(success, at) - << getSuccessOrFailString(success, at, success_str) << ": "; - } - - void log_contexts() { - int num_contexts = get_num_active_contexts(); - if(num_contexts) { - auto contexts = get_active_contexts(); - - s << Color::None << " logged: "; - for(int i = 0; i < num_contexts; ++i) { - s << (i == 0 ? "" : " "); - contexts[i]->stringify(&s); - s << "\n"; - } - } - - s << "\n"; - } - - // this was requested to be made virtual so users could override it - virtual void file_line_to_stream(const char* file, int line, - const char* tail = "") { - s << Color::LightGrey << skipPathFromFilename(file) << (opt.gnu_file_line ? ":" : "(") - << (opt.no_line_numbers ? 0 : line) // 0 or the real num depending on the option - << (opt.gnu_file_line ? ":" : "):") << tail; - } - - void logTestStart() { - if(hasLoggedCurrentTestStart) - return; - - separator_to_stream(); - file_line_to_stream(tc->m_file.c_str(), tc->m_line, "\n"); - if(tc->m_description) - s << Color::Yellow << "DESCRIPTION: " << Color::None << tc->m_description << "\n"; - if(tc->m_test_suite && tc->m_test_suite[0] != '\0') - s << Color::Yellow << "TEST SUITE: " << Color::None << tc->m_test_suite << "\n"; - if(strncmp(tc->m_name, " Scenario:", 11) != 0) - s << Color::Yellow << "TEST CASE: "; - s << Color::None << tc->m_name << "\n"; - - for(size_t i = 0; i < currentSubcaseLevel; ++i) { - if(subcasesStack[i].m_name[0] != '\0') - s << " " << subcasesStack[i].m_name << "\n"; - } - - if(currentSubcaseLevel != subcasesStack.size()) { - s << Color::Yellow << "\nDEEPEST SUBCASE STACK REACHED (DIFFERENT FROM THE CURRENT ONE):\n" << Color::None; - for(size_t i = 0; i < subcasesStack.size(); ++i) { - if(subcasesStack[i].m_name[0] != '\0') - s << " " << subcasesStack[i].m_name << "\n"; - } - } - - s << "\n"; - - hasLoggedCurrentTestStart = true; - } - - void printVersion() { - if(opt.no_version == false) - s << Color::Cyan << "[doctest] " << Color::None << "doctest version is \"" - << DOCTEST_VERSION_STR << "\"\n"; - } - - void printIntro() { - if(opt.no_intro == false) { - printVersion(); - s << Color::Cyan << "[doctest] " << Color::None - << "run with \"--" DOCTEST_OPTIONS_PREFIX_DISPLAY "help\" for options\n"; - } - } - - void printHelp() { - int sizePrefixDisplay = static_cast(strlen(DOCTEST_OPTIONS_PREFIX_DISPLAY)); - printVersion(); - // clang-format off - s << Color::Cyan << "[doctest]\n" << Color::None; - s << Color::Cyan << "[doctest] " << Color::None; - s << "boolean values: \"1/on/yes/true\" or \"0/off/no/false\"\n"; - s << Color::Cyan << "[doctest] " << Color::None; - s << "filter values: \"str1,str2,str3\" (comma separated strings)\n"; - s << Color::Cyan << "[doctest]\n" << Color::None; - s << Color::Cyan << "[doctest] " << Color::None; - s << "filters use wildcards for matching strings\n"; - s << Color::Cyan << "[doctest] " << Color::None; - s << "something passes a filter if any of the strings in a filter matches\n"; -#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS - s << Color::Cyan << "[doctest]\n" << Color::None; - s << Color::Cyan << "[doctest] " << Color::None; - s << "ALL FLAGS, OPTIONS AND FILTERS ALSO AVAILABLE WITH A \"" DOCTEST_CONFIG_OPTIONS_PREFIX "\" PREFIX!!!\n"; -#endif - s << Color::Cyan << "[doctest]\n" << Color::None; - s << Color::Cyan << "[doctest] " << Color::None; - s << "Query flags - the program quits after them. Available:\n\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "?, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "help, -" DOCTEST_OPTIONS_PREFIX_DISPLAY "h " - << Whitespace(sizePrefixDisplay*0) << "prints this message\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "v, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "version " - << Whitespace(sizePrefixDisplay*1) << "prints the version\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "c, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "count " - << Whitespace(sizePrefixDisplay*1) << "prints the number of matching tests\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ltc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-cases " - << Whitespace(sizePrefixDisplay*1) << "lists all matching tests by name\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-suites " - << Whitespace(sizePrefixDisplay*1) << "lists all matching test suites\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-reporters " - << Whitespace(sizePrefixDisplay*1) << "lists all registered reporters\n\n"; - // ================================================================================== << 79 - s << Color::Cyan << "[doctest] " << Color::None; - s << "The available / options/filters are:\n\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case= " - << Whitespace(sizePrefixDisplay*1) << "filters tests by their name\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case-exclude= " - << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their name\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file= " - << Whitespace(sizePrefixDisplay*1) << "filters tests by their file\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sfe, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file-exclude= " - << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their file\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite= " - << Whitespace(sizePrefixDisplay*1) << "filters tests by their test suite\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tse, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite-exclude= " - << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their test suite\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase= " - << Whitespace(sizePrefixDisplay*1) << "filters subcases by their name\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-exclude= " - << Whitespace(sizePrefixDisplay*1) << "filters OUT subcases by their name\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "r, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "reporters= " - << Whitespace(sizePrefixDisplay*1) << "reporters to use (console is default)\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "o, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "out= " - << Whitespace(sizePrefixDisplay*1) << "output filename\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ob, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "order-by= " - << Whitespace(sizePrefixDisplay*1) << "how the tests should be ordered\n"; - s << Whitespace(sizePrefixDisplay*3) << " - [file/suite/name/rand/none]\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "rs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "rand-seed= " - << Whitespace(sizePrefixDisplay*1) << "seed for random ordering\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "f, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "first= " - << Whitespace(sizePrefixDisplay*1) << "the first test passing the filters to\n"; - s << Whitespace(sizePrefixDisplay*3) << " execute - for range-based execution\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "l, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "last= " - << Whitespace(sizePrefixDisplay*1) << "the last test passing the filters to\n"; - s << Whitespace(sizePrefixDisplay*3) << " execute - for range-based execution\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "aa, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "abort-after= " - << Whitespace(sizePrefixDisplay*1) << "stop after failed assertions\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "scfl,--" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-filter-levels= " - << Whitespace(sizePrefixDisplay*1) << "apply filters for the first levels\n"; - s << Color::Cyan << "\n[doctest] " << Color::None; - s << "Bool options - can be used like flags and true is assumed. Available:\n\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "s, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "success= " - << Whitespace(sizePrefixDisplay*1) << "include successful assertions in output\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "cs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "case-sensitive= " - << Whitespace(sizePrefixDisplay*1) << "filters being treated as case sensitive\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "e, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "exit= " - << Whitespace(sizePrefixDisplay*1) << "exits after the tests finish\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "d, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "duration= " - << Whitespace(sizePrefixDisplay*1) << "prints the time duration of each test\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "m, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "minimal= " - << Whitespace(sizePrefixDisplay*1) << "minimal console output (only failures)\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "q, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "quiet= " - << Whitespace(sizePrefixDisplay*1) << "no console output\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nt, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-throw= " - << Whitespace(sizePrefixDisplay*1) << "skips exceptions-related assert checks\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ne, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-exitcode= " - << Whitespace(sizePrefixDisplay*1) << "returns (or exits) always with success\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-run= " - << Whitespace(sizePrefixDisplay*1) << "skips all runtime doctest operations\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ni, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-intro= " - << Whitespace(sizePrefixDisplay*1) << "omit the framework intro in the output\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nv, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-version= " - << Whitespace(sizePrefixDisplay*1) << "omit the framework version in the output\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-colors= " - << Whitespace(sizePrefixDisplay*1) << "disables colors in output\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "fc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "force-colors= " - << Whitespace(sizePrefixDisplay*1) << "use colors even when not in a tty\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nb, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-breaks= " - << Whitespace(sizePrefixDisplay*1) << "disables breakpoints in debuggers\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ns, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-skip= " - << Whitespace(sizePrefixDisplay*1) << "don't skip test cases marked as skip\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "gfl, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "gnu-file-line= " - << Whitespace(sizePrefixDisplay*1) << ":n: vs (n): for line numbers in output\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "npf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-path-filenames= " - << Whitespace(sizePrefixDisplay*1) << "only filenames and no paths in output\n"; - s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nln, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-line-numbers= " - << Whitespace(sizePrefixDisplay*1) << "0 instead of real line numbers in output\n"; - // ================================================================================== << 79 - // clang-format on - - s << Color::Cyan << "\n[doctest] " << Color::None; - s << "for more information visit the project documentation\n\n"; - } - - void printRegisteredReporters() { - printVersion(); - auto printReporters = [this] (const reporterMap& reporters, const char* type) { - if(reporters.size()) { - s << Color::Cyan << "[doctest] " << Color::None << "listing all registered " << type << "\n"; - for(auto& curr : reporters) - s << "priority: " << std::setw(5) << curr.first.first - << " name: " << curr.first.second << "\n"; - } - }; - printReporters(getListeners(), "listeners"); - printReporters(getReporters(), "reporters"); - } - - // ========================================================================================= - // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE - // ========================================================================================= - - void report_query(const QueryData& in) override { - if(opt.version) { - printVersion(); - } else if(opt.help) { - printHelp(); - } else if(opt.list_reporters) { - printRegisteredReporters(); - } else if(opt.count || opt.list_test_cases) { - if(opt.list_test_cases) { - s << Color::Cyan << "[doctest] " << Color::None - << "listing all test case names\n"; - separator_to_stream(); - } - - for(unsigned i = 0; i < in.num_data; ++i) - s << Color::None << in.data[i]->m_name << "\n"; - - separator_to_stream(); - - s << Color::Cyan << "[doctest] " << Color::None - << "unskipped test cases passing the current filters: " - << g_cs->numTestCasesPassingFilters << "\n"; - - } else if(opt.list_test_suites) { - s << Color::Cyan << "[doctest] " << Color::None << "listing all test suites\n"; - separator_to_stream(); - - for(unsigned i = 0; i < in.num_data; ++i) - s << Color::None << in.data[i]->m_test_suite << "\n"; - - separator_to_stream(); - - s << Color::Cyan << "[doctest] " << Color::None - << "unskipped test cases passing the current filters: " - << g_cs->numTestCasesPassingFilters << "\n"; - s << Color::Cyan << "[doctest] " << Color::None - << "test suites with unskipped test cases passing the current filters: " - << g_cs->numTestSuitesPassingFilters << "\n"; - } - } - - void test_run_start() override { - if(!opt.minimal) - printIntro(); - } - - void test_run_end(const TestRunStats& p) override { - if(opt.minimal && p.numTestCasesFailed == 0) - return; - - separator_to_stream(); - s << std::dec; - - auto totwidth = int(std::ceil(log10((std::max(p.numTestCasesPassingFilters, static_cast(p.numAsserts))) + 1))); - auto passwidth = int(std::ceil(log10((std::max(p.numTestCasesPassingFilters - p.numTestCasesFailed, static_cast(p.numAsserts - p.numAssertsFailed))) + 1))); - auto failwidth = int(std::ceil(log10((std::max(p.numTestCasesFailed, static_cast(p.numAssertsFailed))) + 1))); - const bool anythingFailed = p.numTestCasesFailed > 0 || p.numAssertsFailed > 0; - s << Color::Cyan << "[doctest] " << Color::None << "test cases: " << std::setw(totwidth) - << p.numTestCasesPassingFilters << " | " - << ((p.numTestCasesPassingFilters == 0 || anythingFailed) ? Color::None : - Color::Green) - << std::setw(passwidth) << p.numTestCasesPassingFilters - p.numTestCasesFailed << " passed" - << Color::None << " | " << (p.numTestCasesFailed > 0 ? Color::Red : Color::None) - << std::setw(failwidth) << p.numTestCasesFailed << " failed" << Color::None << " |"; - if(opt.no_skipped_summary == false) { - const int numSkipped = p.numTestCases - p.numTestCasesPassingFilters; - s << " " << (numSkipped == 0 ? Color::None : Color::Yellow) << numSkipped - << " skipped" << Color::None; - } - s << "\n"; - s << Color::Cyan << "[doctest] " << Color::None << "assertions: " << std::setw(totwidth) - << p.numAsserts << " | " - << ((p.numAsserts == 0 || anythingFailed) ? Color::None : Color::Green) - << std::setw(passwidth) << (p.numAsserts - p.numAssertsFailed) << " passed" << Color::None - << " | " << (p.numAssertsFailed > 0 ? Color::Red : Color::None) << std::setw(failwidth) - << p.numAssertsFailed << " failed" << Color::None << " |\n"; - s << Color::Cyan << "[doctest] " << Color::None - << "Status: " << (p.numTestCasesFailed > 0 ? Color::Red : Color::Green) - << ((p.numTestCasesFailed > 0) ? "FAILURE!" : "SUCCESS!") << Color::None << std::endl; - } - - void test_case_start(const TestCaseData& in) override { - hasLoggedCurrentTestStart = false; - tc = ∈ - subcasesStack.clear(); - currentSubcaseLevel = 0; - } - - void test_case_reenter(const TestCaseData&) override { - subcasesStack.clear(); - } - - void test_case_end(const CurrentTestCaseStats& st) override { - if(tc->m_no_output) - return; - - // log the preamble of the test case only if there is something - // else to print - something other than that an assert has failed - if(opt.duration || - (st.failure_flags && st.failure_flags != TestCaseFailureReason::AssertFailure)) - logTestStart(); - - if(opt.duration) - s << Color::None << std::setprecision(6) << std::fixed << st.seconds - << " s: " << tc->m_name << "\n"; - - if(st.failure_flags & TestCaseFailureReason::Timeout) - s << Color::Red << "Test case exceeded time limit of " << std::setprecision(6) - << std::fixed << tc->m_timeout << "!\n"; - - if(st.failure_flags & TestCaseFailureReason::ShouldHaveFailedButDidnt) { - s << Color::Red << "Should have failed but didn't! Marking it as failed!\n"; - } else if(st.failure_flags & TestCaseFailureReason::ShouldHaveFailedAndDid) { - s << Color::Yellow << "Failed as expected so marking it as not failed\n"; - } else if(st.failure_flags & TestCaseFailureReason::CouldHaveFailedAndDid) { - s << Color::Yellow << "Allowed to fail so marking it as not failed\n"; - } else if(st.failure_flags & TestCaseFailureReason::DidntFailExactlyNumTimes) { - s << Color::Red << "Didn't fail exactly " << tc->m_expected_failures - << " times so marking it as failed!\n"; - } else if(st.failure_flags & TestCaseFailureReason::FailedExactlyNumTimes) { - s << Color::Yellow << "Failed exactly " << tc->m_expected_failures - << " times as expected so marking it as not failed!\n"; - } - if(st.failure_flags & TestCaseFailureReason::TooManyFailedAsserts) { - s << Color::Red << "Aborting - too many failed asserts!\n"; - } - s << Color::None; // lgtm [cpp/useless-expression] - } - - void test_case_exception(const TestCaseException& e) override { - std::lock_guard lock(mutex); - if(tc->m_no_output) - return; - - logTestStart(); - - file_line_to_stream(tc->m_file.c_str(), tc->m_line, " "); - successOrFailColoredStringToStream(false, e.is_crash ? assertType::is_require : - assertType::is_check); - s << Color::Red << (e.is_crash ? "test case CRASHED: " : "test case THREW exception: ") - << Color::Cyan << e.error_string << "\n"; - - int num_stringified_contexts = get_num_stringified_contexts(); - if(num_stringified_contexts) { - auto stringified_contexts = get_stringified_contexts(); - s << Color::None << " logged: "; - for(int i = num_stringified_contexts; i > 0; --i) { - s << (i == num_stringified_contexts ? "" : " ") - << stringified_contexts[i - 1] << "\n"; - } - } - s << "\n" << Color::None; - } - - void subcase_start(const SubcaseSignature& subc) override { - subcasesStack.push_back(subc); - ++currentSubcaseLevel; - hasLoggedCurrentTestStart = false; - } - - void subcase_end() override { - --currentSubcaseLevel; - hasLoggedCurrentTestStart = false; - } - - void log_assert(const AssertData& rb) override { - if((!rb.m_failed && !opt.success) || tc->m_no_output) - return; - - std::lock_guard lock(mutex); - - logTestStart(); - - file_line_to_stream(rb.m_file, rb.m_line, " "); - successOrFailColoredStringToStream(!rb.m_failed, rb.m_at); - - fulltext_log_assert_to_stream(s, rb); - - log_contexts(); - } - - void log_message(const MessageData& mb) override { - if(tc->m_no_output) - return; - - std::lock_guard lock(mutex); - - logTestStart(); - - file_line_to_stream(mb.m_file, mb.m_line, " "); - s << getSuccessOrFailColor(false, mb.m_severity) - << getSuccessOrFailString(mb.m_severity & assertType::is_warn, mb.m_severity, - "MESSAGE") << ": "; - s << Color::None << mb.m_string << "\n"; - log_contexts(); - } - - void test_case_skipped(const TestCaseData&) override {} - }; - - DOCTEST_REGISTER_REPORTER("console", 0, ConsoleReporter); - -#ifdef DOCTEST_PLATFORM_WINDOWS - struct DebugOutputWindowReporter : public ConsoleReporter - { - DOCTEST_THREAD_LOCAL static std::ostringstream oss; - - DebugOutputWindowReporter(const ContextOptions& co) - : ConsoleReporter(co, oss) {} - -#define DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(func, type, arg) \ - void func(type arg) override { \ - bool with_col = g_no_colors; \ - g_no_colors = false; \ - ConsoleReporter::func(arg); \ - if(oss.tellp() != std::streampos{}) { \ - DOCTEST_OUTPUT_DEBUG_STRING(oss.str().c_str()); \ - oss.str(""); \ - } \ - g_no_colors = with_col; \ - } - - DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_start, DOCTEST_EMPTY, DOCTEST_EMPTY) - DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_end, const TestRunStats&, in) - DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_start, const TestCaseData&, in) - DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_reenter, const TestCaseData&, in) - DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_end, const CurrentTestCaseStats&, in) - DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_exception, const TestCaseException&, in) - DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_start, const SubcaseSignature&, in) - DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_end, DOCTEST_EMPTY, DOCTEST_EMPTY) - DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_assert, const AssertData&, in) - DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_message, const MessageData&, in) - DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_skipped, const TestCaseData&, in) - }; - - DOCTEST_THREAD_LOCAL std::ostringstream DebugOutputWindowReporter::oss; -#endif // DOCTEST_PLATFORM_WINDOWS - - // the implementation of parseOption() - bool parseOptionImpl(int argc, const char* const* argv, const char* pattern, String* value) { - // going from the end to the beginning and stopping on the first occurrence from the end - for(int i = argc; i > 0; --i) { - auto index = i - 1; - auto temp = std::strstr(argv[index], pattern); - if(temp && (value || strlen(temp) == strlen(pattern))) { //!OCLINT prefer early exits and continue - // eliminate matches in which the chars before the option are not '-' - bool noBadCharsFound = true; - auto curr = argv[index]; - while(curr != temp) { - if(*curr++ != '-') { - noBadCharsFound = false; - break; - } - } - if(noBadCharsFound && argv[index][0] == '-') { - if(value) { - // parsing the value of an option - temp += strlen(pattern); - const unsigned len = strlen(temp); - if(len) { - *value = temp; - return true; - } - } else { - // just a flag - no value - return true; - } - } - } - } - return false; - } - - // parses an option and returns the string after the '=' character - bool parseOption(int argc, const char* const* argv, const char* pattern, String* value = nullptr, - const String& defaultVal = String()) { - if(value) - *value = defaultVal; -#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS - // offset (normally 3 for "dt-") to skip prefix - if(parseOptionImpl(argc, argv, pattern + strlen(DOCTEST_CONFIG_OPTIONS_PREFIX), value)) - return true; -#endif // DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS - return parseOptionImpl(argc, argv, pattern, value); - } - - // locates a flag on the command line - bool parseFlag(int argc, const char* const* argv, const char* pattern) { - return parseOption(argc, argv, pattern); - } - - // parses a comma separated list of words after a pattern in one of the arguments in argv - bool parseCommaSepArgs(int argc, const char* const* argv, const char* pattern, - std::vector& res) { - String filtersString; - if(parseOption(argc, argv, pattern, &filtersString)) { - // tokenize with "," as a separator, unless escaped with backslash - std::ostringstream s; - auto flush = [&s, &res]() { - auto string = s.str(); - if(string.size() > 0) { - res.push_back(string.c_str()); - } - s.str(""); - }; - - bool seenBackslash = false; - const char* current = filtersString.c_str(); - const char* end = current + strlen(current); - while(current != end) { - char character = *current++; - if(seenBackslash) { - seenBackslash = false; - if(character == ',') { - s.put(','); - continue; - } - s.put('\\'); - } - if(character == '\\') { - seenBackslash = true; - } else if(character == ',') { - flush(); - } else { - s.put(character); - } - } - - if(seenBackslash) { - s.put('\\'); - } - flush(); - return true; - } - return false; - } - - enum optionType - { - option_bool, - option_int - }; - - // parses an int/bool option from the command line - bool parseIntOption(int argc, const char* const* argv, const char* pattern, optionType type, - int& res) { - String parsedValue; - if(!parseOption(argc, argv, pattern, &parsedValue)) - return false; - - if(type == 0) { - // boolean - const char positive[][5] = {"1", "true", "on", "yes"}; // 5 - strlen("true") + 1 - const char negative[][6] = {"0", "false", "off", "no"}; // 6 - strlen("false") + 1 - - // if the value matches any of the positive/negative possibilities - for(unsigned i = 0; i < 4; i++) { - if(parsedValue.compare(positive[i], true) == 0) { - res = 1; //!OCLINT parameter reassignment - return true; - } - if(parsedValue.compare(negative[i], true) == 0) { - res = 0; //!OCLINT parameter reassignment - return true; - } - } - } else { - // integer - // TODO: change this to use std::stoi or something else! currently it uses undefined behavior - assumes '0' on failed parse... - int theInt = std::atoi(parsedValue.c_str()); // NOLINT - if(theInt != 0) { - res = theInt; //!OCLINT parameter reassignment - return true; - } - } - return false; - } -} // namespace - -Context::Context(int argc, const char* const* argv) - : p(new detail::ContextState) { - parseArgs(argc, argv, true); - if(argc) - p->binary_name = argv[0]; -} - -Context::~Context() { - if(g_cs == p) - g_cs = nullptr; - delete p; -} - -void Context::applyCommandLine(int argc, const char* const* argv) { - parseArgs(argc, argv); - if(argc) - p->binary_name = argv[0]; -} - -// parses args -void Context::parseArgs(int argc, const char* const* argv, bool withDefaults) { - using namespace detail; - - // clang-format off - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file=", p->filters[0]); - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sf=", p->filters[0]); - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file-exclude=",p->filters[1]); - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sfe=", p->filters[1]); - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite=", p->filters[2]); - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ts=", p->filters[2]); - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite-exclude=", p->filters[3]); - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tse=", p->filters[3]); - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case=", p->filters[4]); - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tc=", p->filters[4]); - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case-exclude=", p->filters[5]); - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tce=", p->filters[5]); - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase=", p->filters[6]); - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sc=", p->filters[6]); - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase-exclude=", p->filters[7]); - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sce=", p->filters[7]); - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "reporters=", p->filters[8]); - parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "r=", p->filters[8]); - // clang-format on - - int intRes = 0; - String strRes; - -#define DOCTEST_PARSE_AS_BOOL_OR_FLAG(name, sname, var, default) \ - if(parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_bool, intRes) || \ - parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_bool, intRes)) \ - p->var = static_cast(intRes); \ - else if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name) || \ - parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname)) \ - p->var = true; \ - else if(withDefaults) \ - p->var = default - -#define DOCTEST_PARSE_INT_OPTION(name, sname, var, default) \ - if(parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_int, intRes) || \ - parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_int, intRes)) \ - p->var = intRes; \ - else if(withDefaults) \ - p->var = default - -#define DOCTEST_PARSE_STR_OPTION(name, sname, var, default) \ - if(parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", &strRes, default) || \ - parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", &strRes, default) || \ - withDefaults) \ - p->var = strRes - - // clang-format off - DOCTEST_PARSE_STR_OPTION("out", "o", out, ""); - DOCTEST_PARSE_STR_OPTION("order-by", "ob", order_by, "file"); - DOCTEST_PARSE_INT_OPTION("rand-seed", "rs", rand_seed, 0); - - DOCTEST_PARSE_INT_OPTION("first", "f", first, 0); - DOCTEST_PARSE_INT_OPTION("last", "l", last, UINT_MAX); - - DOCTEST_PARSE_INT_OPTION("abort-after", "aa", abort_after, 0); - DOCTEST_PARSE_INT_OPTION("subcase-filter-levels", "scfl", subcase_filter_levels, INT_MAX); - - DOCTEST_PARSE_AS_BOOL_OR_FLAG("success", "s", success, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("case-sensitive", "cs", case_sensitive, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("exit", "e", exit, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("duration", "d", duration, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("minimal", "m", minimal, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("quiet", "q", quiet, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-throw", "nt", no_throw, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-exitcode", "ne", no_exitcode, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-run", "nr", no_run, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-intro", "ni", no_intro, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-version", "nv", no_version, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-colors", "nc", no_colors, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("force-colors", "fc", force_colors, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-breaks", "nb", no_breaks, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skip", "ns", no_skip, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("gnu-file-line", "gfl", gnu_file_line, !bool(DOCTEST_MSVC)); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-path-filenames", "npf", no_path_in_filenames, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-line-numbers", "nln", no_line_numbers, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-debug-output", "ndo", no_debug_output, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skipped-summary", "nss", no_skipped_summary, false); - DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-time-in-output", "ntio", no_time_in_output, false); - // clang-format on - - if(withDefaults) { - p->help = false; - p->version = false; - p->count = false; - p->list_test_cases = false; - p->list_test_suites = false; - p->list_reporters = false; - } - if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "help") || - parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "h") || - parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "?")) { - p->help = true; - p->exit = true; - } - if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "version") || - parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "v")) { - p->version = true; - p->exit = true; - } - if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "count") || - parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "c")) { - p->count = true; - p->exit = true; - } - if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-cases") || - parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ltc")) { - p->list_test_cases = true; - p->exit = true; - } - if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-suites") || - parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lts")) { - p->list_test_suites = true; - p->exit = true; - } - if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-reporters") || - parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lr")) { - p->list_reporters = true; - p->exit = true; - } -} - -// allows the user to add procedurally to the filters from the command line -void Context::addFilter(const char* filter, const char* value) { setOption(filter, value); } - -// allows the user to clear all filters from the command line -void Context::clearFilters() { - for(auto& curr : p->filters) - curr.clear(); -} - -// allows the user to override procedurally the bool options from the command line -void Context::setOption(const char* option, bool value) { - setOption(option, value ? "true" : "false"); -} - -// allows the user to override procedurally the int options from the command line -void Context::setOption(const char* option, int value) { - setOption(option, toString(value).c_str()); - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) -} - -// allows the user to override procedurally the string options from the command line -void Context::setOption(const char* option, const char* value) { - auto argv = String("-") + option + "=" + value; - auto lvalue = argv.c_str(); - parseArgs(1, &lvalue); -} - -// users should query this in their main() and exit the program if true -bool Context::shouldExit() { return p->exit; } - -void Context::setAsDefaultForAssertsOutOfTestCases() { g_cs = p; } - -void Context::setAssertHandler(detail::assert_handler ah) { p->ah = ah; } - -void Context::setCout(std::ostream* out) { p->cout = out; } - -static class DiscardOStream : public std::ostream -{ -private: - class : public std::streambuf - { - private: - // allowing some buffering decreases the amount of calls to overflow - char buf[1024]; - - protected: - std::streamsize xsputn(const char_type*, std::streamsize count) override { return count; } - - int_type overflow(int_type ch) override { - setp(std::begin(buf), std::end(buf)); - return traits_type::not_eof(ch); - } - } discardBuf; - -public: - DiscardOStream() - : std::ostream(&discardBuf) {} -} discardOut; - -// the main function that does all the filtering and test running -int Context::run() { - using namespace detail; - - // save the old context state in case such was setup - for using asserts out of a testing context - auto old_cs = g_cs; - // this is the current contest - g_cs = p; - is_running_in_test = true; - - g_no_colors = p->no_colors; - p->resetRunData(); - - std::fstream fstr; - if(p->cout == nullptr) { - if(p->quiet) { - p->cout = &discardOut; - } else if(p->out.size()) { - // to a file if specified - fstr.open(p->out.c_str(), std::fstream::out); - p->cout = &fstr; - } else { - // stdout by default - p->cout = &std::cout; - } - } - - FatalConditionHandler::allocateAltStackMem(); - - auto cleanup_and_return = [&]() { - FatalConditionHandler::freeAltStackMem(); - - if(fstr.is_open()) - fstr.close(); - - // restore context - g_cs = old_cs; - is_running_in_test = false; - - // we have to free the reporters which were allocated when the run started - for(auto& curr : p->reporters_currently_used) - delete curr; - p->reporters_currently_used.clear(); - - if(p->numTestCasesFailed && !p->no_exitcode) - return EXIT_FAILURE; - return EXIT_SUCCESS; - }; - - // setup default reporter if none is given through the command line - if(p->filters[8].empty()) - p->filters[8].push_back("console"); - - // check to see if any of the registered reporters has been selected - for(auto& curr : getReporters()) { - if(matchesAny(curr.first.second.c_str(), p->filters[8], false, p->case_sensitive)) - p->reporters_currently_used.push_back(curr.second(*g_cs)); - } - - // TODO: check if there is nothing in reporters_currently_used - - // prepend all listeners - for(auto& curr : getListeners()) - p->reporters_currently_used.insert(p->reporters_currently_used.begin(), curr.second(*g_cs)); - -#ifdef DOCTEST_PLATFORM_WINDOWS - if(isDebuggerActive() && p->no_debug_output == false) - p->reporters_currently_used.push_back(new DebugOutputWindowReporter(*g_cs)); -#endif // DOCTEST_PLATFORM_WINDOWS - - // handle version, help and no_run - if(p->no_run || p->version || p->help || p->list_reporters) { - DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, QueryData()); - - return cleanup_and_return(); - } - - std::vector testArray; - for(auto& curr : getRegisteredTests()) - testArray.push_back(&curr); - p->numTestCases = testArray.size(); - - // sort the collected records - if(!testArray.empty()) { - if(p->order_by.compare("file", true) == 0) { - std::sort(testArray.begin(), testArray.end(), fileOrderComparator); - } else if(p->order_by.compare("suite", true) == 0) { - std::sort(testArray.begin(), testArray.end(), suiteOrderComparator); - } else if(p->order_by.compare("name", true) == 0) { - std::sort(testArray.begin(), testArray.end(), nameOrderComparator); - } else if(p->order_by.compare("rand", true) == 0) { - std::srand(p->rand_seed); - - // random_shuffle implementation - const auto first = &testArray[0]; - for(size_t i = testArray.size() - 1; i > 0; --i) { - int idxToSwap = std::rand() % (i + 1); // NOLINT - - const auto temp = first[i]; - - first[i] = first[idxToSwap]; - first[idxToSwap] = temp; - } - } else if(p->order_by.compare("none", true) == 0) { - // means no sorting - beneficial for death tests which call into the executable - // with a specific test case in mind - we don't want to slow down the startup times - } - } - - std::set testSuitesPassingFilt; - - bool query_mode = p->count || p->list_test_cases || p->list_test_suites; - std::vector queryResults; - - if(!query_mode) - DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_start, DOCTEST_EMPTY); - - // invoke the registered functions if they match the filter criteria (or just count them) - for(auto& curr : testArray) { - const auto& tc = *curr; - - bool skip_me = false; - if(tc.m_skip && !p->no_skip) - skip_me = true; - - if(!matchesAny(tc.m_file.c_str(), p->filters[0], true, p->case_sensitive)) - skip_me = true; - if(matchesAny(tc.m_file.c_str(), p->filters[1], false, p->case_sensitive)) - skip_me = true; - if(!matchesAny(tc.m_test_suite, p->filters[2], true, p->case_sensitive)) - skip_me = true; - if(matchesAny(tc.m_test_suite, p->filters[3], false, p->case_sensitive)) - skip_me = true; - if(!matchesAny(tc.m_name, p->filters[4], true, p->case_sensitive)) - skip_me = true; - if(matchesAny(tc.m_name, p->filters[5], false, p->case_sensitive)) - skip_me = true; - - if(!skip_me) - p->numTestCasesPassingFilters++; - - // skip the test if it is not in the execution range - if((p->last < p->numTestCasesPassingFilters && p->first <= p->last) || - (p->first > p->numTestCasesPassingFilters)) - skip_me = true; - - if(skip_me) { - if(!query_mode) - DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_skipped, tc); - continue; - } - - // do not execute the test if we are to only count the number of filter passing tests - if(p->count) - continue; - - // print the name of the test and don't execute it - if(p->list_test_cases) { - queryResults.push_back(&tc); - continue; - } - - // print the name of the test suite if not done already and don't execute it - if(p->list_test_suites) { - if((testSuitesPassingFilt.count(tc.m_test_suite) == 0) && tc.m_test_suite[0] != '\0') { - queryResults.push_back(&tc); - testSuitesPassingFilt.insert(tc.m_test_suite); - p->numTestSuitesPassingFilters++; - } - continue; - } - - // execute the test if it passes all the filtering - { - p->currentTest = &tc; - - p->failure_flags = TestCaseFailureReason::None; - p->seconds = 0; - - // reset atomic counters - p->numAssertsFailedCurrentTest_atomic = 0; - p->numAssertsCurrentTest_atomic = 0; - - p->subcasesPassed.clear(); - - DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_start, tc); - - p->timer.start(); - - bool run_test = true; - - do { - // reset some of the fields for subcases (except for the set of fully passed ones) - p->should_reenter = false; - p->subcasesCurrentMaxLevel = 0; - p->subcasesStack.clear(); - - p->shouldLogCurrentException = true; - - // reset stuff for logging with INFO() - p->stringifiedContexts.clear(); - -#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS - try { -#endif // DOCTEST_CONFIG_NO_EXCEPTIONS -// MSVC 2015 diagnoses fatalConditionHandler as unused (because reset() is a static method) -DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4101) // unreferenced local variable - FatalConditionHandler fatalConditionHandler; // Handle signals - // execute the test - tc.m_test(); - fatalConditionHandler.reset(); -DOCTEST_MSVC_SUPPRESS_WARNING_POP -#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS - } catch(const TestFailureException&) { - p->failure_flags |= TestCaseFailureReason::AssertFailure; - } catch(...) { - DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception, - {translateActiveException(), false}); - p->failure_flags |= TestCaseFailureReason::Exception; - } -#endif // DOCTEST_CONFIG_NO_EXCEPTIONS - - // exit this loop if enough assertions have failed - even if there are more subcases - if(p->abort_after > 0 && - p->numAssertsFailed + p->numAssertsFailedCurrentTest_atomic >= p->abort_after) { - run_test = false; - p->failure_flags |= TestCaseFailureReason::TooManyFailedAsserts; - } - - if(p->should_reenter && run_test) - DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_reenter, tc); - if(!p->should_reenter) - run_test = false; - } while(run_test); - - p->finalizeTestCaseData(); - - DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs); - - p->currentTest = nullptr; - - // stop executing tests if enough assertions have failed - if(p->abort_after > 0 && p->numAssertsFailed >= p->abort_after) - break; - } - } - - if(!query_mode) { - DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs); - } else { - QueryData qdata; - qdata.run_stats = g_cs; - qdata.data = queryResults.data(); - qdata.num_data = unsigned(queryResults.size()); - DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, qdata); - } - - return cleanup_and_return(); -} - -IReporter::~IReporter() = default; - -int IReporter::get_num_active_contexts() { return detail::g_infoContexts.size(); } -const IContextScope* const* IReporter::get_active_contexts() { - return get_num_active_contexts() ? &detail::g_infoContexts[0] : nullptr; -} - -int IReporter::get_num_stringified_contexts() { return detail::g_cs->stringifiedContexts.size(); } -const String* IReporter::get_stringified_contexts() { - return get_num_stringified_contexts() ? &detail::g_cs->stringifiedContexts[0] : nullptr; -} - -namespace detail { - void registerReporterImpl(const char* name, int priority, reporterCreatorFunc c, bool isReporter) { - if(isReporter) - getReporters().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c)); - else - getListeners().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c)); - } -} // namespace detail - -} // namespace doctest - -#endif // DOCTEST_CONFIG_DISABLE - -#ifdef DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN -DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4007) // 'function' : must be 'attribute' - see issue #182 -int main(int argc, char** argv) { return doctest::Context(argc, argv).run(); } -DOCTEST_MSVC_SUPPRESS_WARNING_POP -#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN - -DOCTEST_CLANG_SUPPRESS_WARNING_POP -DOCTEST_MSVC_SUPPRESS_WARNING_POP -DOCTEST_GCC_SUPPRESS_WARNING_POP - -DOCTEST_SUPPRESS_COMMON_WARNINGS_POP - -#endif // DOCTEST_LIBRARY_IMPLEMENTATION -#endif // DOCTEST_CONFIG_IMPLEMENT diff --git a/binding-tests/replica.cpp b/binding-tests/replica.cpp deleted file mode 100644 index 18364a653..000000000 --- a/binding-tests/replica.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include -#include "doctest.h" -#include "taskchampion.h" - -TEST_CASE("creating an in-memory TCReplica does not crash") { - TCReplica *rep = tc_replica_new(NULL); - CHECK(tc_replica_error(rep) == NULL); - tc_replica_free(rep); -} - -TEST_CASE("creating an on-disk TCReplica does not crash") { - TCReplica *rep = tc_replica_new(tc_string_new("test-db")); - CHECK(tc_replica_error(rep) == NULL); - tc_replica_free(rep); -} - -TEST_CASE("undo on an empty in-memory TCReplica does nothing") { - TCReplica *rep = tc_replica_new(NULL); - CHECK(tc_replica_error(rep) == NULL); - int rv = tc_replica_undo(rep); - CHECK(rv == 0); - CHECK(tc_replica_error(rep) == NULL); - tc_replica_free(rep); -} diff --git a/binding-tests/string.cpp b/binding-tests/string.cpp deleted file mode 100644 index 8b743b266..000000000 --- a/binding-tests/string.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include -#include "doctest.h" -#include "taskchampion.h" - -TEST_CASE("creating borrowed strings does not crash") { - TCString *s = tc_string_new("abcdef"); - tc_string_free(s); -} - -TEST_CASE("creating cloned strings does not crash") { - char *abcdef = strdup("abcdef"); - TCString *s = tc_string_clone(abcdef); - REQUIRE(s != NULL); - free(abcdef); - - CHECK(strcmp(tc_string_content(s), "abcdef") == 0); - tc_string_free(s); -} - -TEST_CASE("borrowed strings echo back their content") { - TCString *s = tc_string_new("abcdef"); - REQUIRE(s != NULL); - - CHECK(strcmp(tc_string_content(s), "abcdef") == 0); - size_t len; - const char *buf = tc_string_content_with_len(s, &len); - REQUIRE(buf != NULL); - CHECK(len == 6); - CHECK(strncmp(buf, "abcdef", len) == 0); - tc_string_free(s); -} - -TEST_CASE("cloned strings echo back their content") { - char *orig = strdup("abcdef"); - TCString *s = tc_string_clone(orig); - REQUIRE(s != NULL); - free(orig); - - CHECK(strcmp(tc_string_content(s), "abcdef") == 0); - - size_t len; - const char *buf = tc_string_content_with_len(s, &len); - REQUIRE(buf != NULL); - CHECK(len == 6); - CHECK(strncmp(buf, "abcdef", len) == 0); - tc_string_free(s); -} - -TEST_CASE("tc_string_content returns NULL for strings containing embedded NULs") { - TCString *s = tc_string_clone_with_len("ab\0de", 5); - REQUIRE(s != NULL); - - CHECK(tc_string_content(s) == NULL); - - size_t len; - const char *buf = tc_string_content_with_len(s, &len); - REQUIRE(buf != NULL); - CHECK(len == 5); - CHECK(strncmp(buf, "ab\0de", len) == 0); - tc_string_free(s); -} diff --git a/binding-tests/task.cpp b/binding-tests/task.cpp deleted file mode 100644 index 9d82767ca..000000000 --- a/binding-tests/task.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include -#include "doctest.h" -#include "taskchampion.h" - -TEST_CASE("creating a Task does not crash") { - TCReplica *rep = tc_replica_new(NULL); - CHECK(tc_replica_error(rep) == NULL); - - TCTask *task = tc_replica_new_task( - rep, - TC_STATUS_PENDING, - tc_string_new("my task")); - REQUIRE(task != NULL); - - CHECK(tc_task_get_status(task) == TC_STATUS_PENDING); - - TCString *desc = tc_task_get_description(task); - REQUIRE(desc != NULL); - CHECK(strcmp(tc_string_content(desc), "my task") == 0); - tc_string_free(desc); - - tc_task_free(task); - - tc_replica_free(rep); -} diff --git a/binding-tests/uuid.cpp b/binding-tests/uuid.cpp deleted file mode 100644 index aba07d867..000000000 --- a/binding-tests/uuid.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include -#include "doctest.h" -#include "taskchampion.h" - -TEST_CASE("creating UUIDs does not crash") { - TCUuid u1 = tc_uuid_new_v4(); - TCUuid u2 = tc_uuid_nil(); -} - -TEST_CASE("converting UUIDs to string works") { - TCUuid u2 = tc_uuid_nil(); - REQUIRE(TC_UUID_STRING_BYTES == 36); - - char u2str[TC_UUID_STRING_BYTES]; - tc_uuid_to_str(u2, u2str); - CHECK(strncmp(u2str, "00000000-0000-0000-0000-000000000000", TC_UUID_STRING_BYTES) == 0); -} - -TEST_CASE("converting UUIDs from string works") { - TCUuid u; - char ustr[TC_UUID_STRING_BYTES] = "fdc314b7-f938-4845-b8d1-95716e4eb762"; - CHECK(tc_uuid_from_str(ustr, &u)); - CHECK(u.bytes[0] == 0xfd); - // .. if these two bytes are correct, then it probably worked :) - CHECK(u.bytes[15] == 0x62); -} - -TEST_CASE("converting invalid UUIDs from string fails as expected") { - TCUuid u; - char ustr[TC_UUID_STRING_BYTES] = "not-a-valid-uuid"; - CHECK(!tc_uuid_from_str(ustr, &u)); -} - -TEST_CASE("converting invalid UTF-8 UUIDs from string fails as expected") { - TCUuid u; - char ustr[TC_UUID_STRING_BYTES] = "\xf0\x28\x8c\xbc"; - CHECK(!tc_uuid_from_str(ustr, &u)); -} diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml new file mode 100644 index 000000000..f351e17d3 --- /dev/null +++ b/integration-tests/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "integration-tests" +version = "0.4.1" +authors = ["Dustin J. Mitchell "] +edition = "2018" +publish = false +build = "build.rs" + +[dependencies] +taskchampion = { path = "../taskchampion" } + +[build-dependencies] +cc = "1.0" +taskchampion-lib = { path = "../lib" } diff --git a/integration-tests/README.md b/integration-tests/README.md new file mode 100644 index 000000000..21ca29c3b --- /dev/null +++ b/integration-tests/README.md @@ -0,0 +1,32 @@ +# Integration Tests for Taskchampion + +## "Regular" Tests + +Some of the tests in `tests/` are just regular integration tests. +Nothing exciting to see. + +## Bindings Tests + +The bindings tests are a bit more interesting, since they are written in C. +They are composed of a collection of "suites", each in one C file in `integration-tests/src/bindings_tests/`. +Each suite contains a number of tests (using [Unity](http://www.throwtheswitch.org/unity)) and an exported function named after the suite that returns an exit status (1 = failure). + +The build script (`integration-tests/build.rs`) builds these files into a library that is linked with the `integration_tests` library crate. +This crate contains a `bindings_tests` module with a pub function for each suite. + +Finally, the `integration-tests/tests/bindings.rs` test file calls each of those functions in a separate test case. + +### Adding Tests + +To add a test, select a suite and add a new test-case function. +Add a `RUN_TEST` invocation for your new function to the `.._tests` function at the bottom. +Keep the `RUN_TEST`s in the same order as the functions they call. + +### Adding Suites + +To add a suite, + +1. Add a new C file in `integration-tests/src/bindings_tests/`. +1. Add a new `.file(..)` to build that file in `integration-tests/build.rs`. +1. Add a `suite!(..)` to `integration-tests/src/bindings_tests/mod.rs`. +1. Add a `suite!(..)` to `integration-tests/tests/bindings.rs`. diff --git a/integration-tests/build.rs b/integration-tests/build.rs new file mode 100644 index 000000000..78c531024 --- /dev/null +++ b/integration-tests/build.rs @@ -0,0 +1,36 @@ +fn main() { + // This crate has taskchampion-lib in its build-dependencies, so + // libtaskchampion.so should be built already. Hopefully it's in target/$PROFILE, and hopefully + // it's named libtaskchampion.so and not something else + let mut libtaskchampion = std::env::current_dir().unwrap(); + libtaskchampion.pop(); + libtaskchampion.push("target"); + libtaskchampion.push(std::env::var("PROFILE").unwrap()); + libtaskchampion.push("deps"); + libtaskchampion.push("libtaskchampion.so"); + + println!("cargo:rerun-if-changed=build.rs"); + + let mut build = cc::Build::new(); + build.object(libtaskchampion); + build.include("../lib"); + build.include("src/bindings_tests/unity"); + build.file("src/bindings_tests/unity/unity.c"); + + let files = &[ + "src/bindings_tests/test.c", + // keep this list in sync with integration-tests/src/bindings_tests/mod.rs and + // integration-tests/tests/bindings.rs + "src/bindings_tests/uuid.c", + "src/bindings_tests/string.c", + "src/bindings_tests/task.c", + "src/bindings_tests/replica.c", + ]; + + for file in files { + build.file(file); + println!("cargo:rerun-if-changed={}", file); + } + + build.compile("bindings-tests"); +} diff --git a/integration-tests/src/bindings_tests/mod.rs b/integration-tests/src/bindings_tests/mod.rs new file mode 100644 index 000000000..a9cf8119e --- /dev/null +++ b/integration-tests/src/bindings_tests/mod.rs @@ -0,0 +1,20 @@ +// Each suite is represented by a _tests C function in .c. +// All of these C files are built into a library that is linked to the crate -- but not to test +// crates. So, this macro produces a "glue function" that calls the C function, and that can be +// called from test crates. +macro_rules! suite( + { $s:ident } => { + pub fn $s() -> i32 { + extern "C" { + fn $s() -> i32; + } + unsafe { $s() } + } + }; +); + +// keep this list in sync with integration-tests/build.rs and integration-tests/tests/bindings.rs. +suite!(uuid_tests); +suite!(string_tests); +suite!(task_tests); +suite!(replica_tests); diff --git a/integration-tests/src/bindings_tests/replica.c b/integration-tests/src/bindings_tests/replica.c new file mode 100644 index 000000000..71eef0efe --- /dev/null +++ b/integration-tests/src/bindings_tests/replica.c @@ -0,0 +1,39 @@ +#include +#include +#include "unity.h" +#include "taskchampion.h" + +// creating an in-memory replica does not crash +static void test_replica_creation(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NOT_NULL(rep); + TEST_ASSERT_NULL(tc_replica_error(rep)); + tc_replica_free(rep); +} + +// creating an on-disk replica does not crash +static void test_replica_creation_disk(void) { + TCReplica *rep = tc_replica_new_on_disk(tc_string_new("test-db"), NULL); + TEST_ASSERT_NOT_NULL(rep); + TEST_ASSERT_NULL(tc_replica_error(rep)); + tc_replica_free(rep); +} + +// undo on an empty in-memory TCReplica does nothing +static void test_replica_undo_empty(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + int rv = tc_replica_undo(rep); + TEST_ASSERT_EQUAL(0, rv); + TEST_ASSERT_NULL(tc_replica_error(rep)); + tc_replica_free(rep); +} + +int replica_tests(void) { + UNITY_BEGIN(); + // each test case above should be named here, in order. + RUN_TEST(test_replica_creation); + RUN_TEST(test_replica_creation_disk); + RUN_TEST(test_replica_undo_empty); + return UNITY_END(); +} diff --git a/integration-tests/src/bindings_tests/string.c b/integration-tests/src/bindings_tests/string.c new file mode 100644 index 000000000..ad6c62559 --- /dev/null +++ b/integration-tests/src/bindings_tests/string.c @@ -0,0 +1,81 @@ +#include +#include +#include "unity.h" +#include "taskchampion.h" + +// creating strings does not crash +static void test_string_creation(void) { + TCString *s = tc_string_new("abcdef"); + tc_string_free(s); +} + +// creating cloned strings does not crash +static void test_string_cloning(void) { + char *abcdef = strdup("abcdef"); + TCString *s = tc_string_clone(abcdef); + TEST_ASSERT_NOT_NULL(s); + free(abcdef); + + TEST_ASSERT_EQUAL_STRING("abcdef", tc_string_content(s)); + tc_string_free(s); +} + +// borrowed strings echo back their content +static void test_string_borrowed_strings_echo(void) { + TCString *s = tc_string_new("abcdef"); + TEST_ASSERT_NOT_NULL(s); + + TEST_ASSERT_EQUAL_STRING("abcdef", tc_string_content(s)); + + size_t len; + const char *buf = tc_string_content_with_len(s, &len); + TEST_ASSERT_NOT_NULL(buf); + TEST_ASSERT_EQUAL(6, len); + TEST_ASSERT_EQUAL_MEMORY("abcdef", buf, len); + + tc_string_free(s); +} + +// cloned strings echo back their content +static void test_string_cloned_strings_echo(void) { + char *orig = strdup("abcdef"); + TCString *s = tc_string_clone(orig); + TEST_ASSERT_NOT_NULL(s); + free(orig); + + TEST_ASSERT_EQUAL_STRING("abcdef", tc_string_content(s)); + + size_t len; + const char *buf = tc_string_content_with_len(s, &len); + TEST_ASSERT_NOT_NULL(buf); + TEST_ASSERT_EQUAL(6, len); + TEST_ASSERT_EQUAL_MEMORY("abcdef", buf, len); + + tc_string_free(s); +} + +// tc_string_content returns NULL for strings containing embedded NULs +static void test_string_content_null_for_embedded_nuls(void) { + TCString *s = tc_string_clone_with_len("ab\0de", 5); + TEST_ASSERT_NOT_NULL(s); + + TEST_ASSERT_NULL(tc_string_content(s)); + + size_t len; + const char *buf = tc_string_content_with_len(s, &len); + TEST_ASSERT_NOT_NULL(buf); + TEST_ASSERT_EQUAL(5, len); + TEST_ASSERT_EQUAL_MEMORY("ab\0de", buf, len); + tc_string_free(s); +} + +int string_tests(void) { + UNITY_BEGIN(); + // each test case above should be named here, in order. + RUN_TEST(test_string_creation); + RUN_TEST(test_string_cloning); + RUN_TEST(test_string_borrowed_strings_echo); + RUN_TEST(test_string_cloned_strings_echo); + RUN_TEST(test_string_content_null_for_embedded_nuls); + return UNITY_END(); +} diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c new file mode 100644 index 000000000..9f77b8170 --- /dev/null +++ b/integration-tests/src/bindings_tests/task.c @@ -0,0 +1,34 @@ +#include +#include +#include "unity.h" +#include "taskchampion.h" + +// creating a task succeeds and the resulting task looks good +static void test_task_creation(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_new("my task")); + TEST_ASSERT_NOT_NULL(task); + + TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task)); + + TCString *desc = tc_task_get_description(task); + TEST_ASSERT_NOT_NULL(desc); + TEST_ASSERT_EQUAL_STRING("my task", tc_string_content(desc)); + tc_string_free(desc); + + tc_task_free(task); + + tc_replica_free(rep); +} + +int task_tests(void) { + UNITY_BEGIN(); + // each test case above should be named here, in order. + RUN_TEST(test_task_creation); + return UNITY_END(); +} diff --git a/integration-tests/src/bindings_tests/test.c b/integration-tests/src/bindings_tests/test.c new file mode 100644 index 000000000..609943fc4 --- /dev/null +++ b/integration-tests/src/bindings_tests/test.c @@ -0,0 +1,6 @@ +#include "unity.h" + +// these functions are shared between all test "suites" +// and cannot be customized per-suite. +void setUp(void) { } +void tearDown(void) { } diff --git a/integration-tests/src/bindings_tests/unity/LICENSE.txt b/integration-tests/src/bindings_tests/unity/LICENSE.txt new file mode 100644 index 000000000..b9a329dde --- /dev/null +++ b/integration-tests/src/bindings_tests/unity/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2007-21 Mike Karlesky, Mark VanderVoord, Greg Williams + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/integration-tests/src/bindings_tests/unity/README.md b/integration-tests/src/bindings_tests/unity/README.md new file mode 100644 index 000000000..6f755ced0 --- /dev/null +++ b/integration-tests/src/bindings_tests/unity/README.md @@ -0,0 +1,3 @@ +# Unity + +This directory contains the src from https://github.com/ThrowTheSwitch/Unity, revision 8ba01386008196a92ef4fdbdb0b00f2434c79563. diff --git a/integration-tests/src/bindings_tests/unity/unity.c b/integration-tests/src/bindings_tests/unity/unity.c new file mode 100644 index 000000000..b88024875 --- /dev/null +++ b/integration-tests/src/bindings_tests/unity/unity.c @@ -0,0 +1,2119 @@ +/* ========================================================================= + Unity Project - A Test Framework for C + Copyright (c) 2007-21 Mike Karlesky, Mark VanderVoord, Greg Williams + [Released under MIT License. Please refer to license.txt for details] +============================================================================ */ + +#include "unity.h" +#include + +#ifdef AVR +#include +#else +#define PROGMEM +#endif + +/* If omitted from header, declare overrideable prototypes here so they're ready for use */ +#ifdef UNITY_OMIT_OUTPUT_CHAR_HEADER_DECLARATION +void UNITY_OUTPUT_CHAR(int); +#endif + +/* Helpful macros for us to use here in Assert functions */ +#define UNITY_FAIL_AND_BAIL do { Unity.CurrentTestFailed = 1; UNITY_OUTPUT_FLUSH(); TEST_ABORT(); } while (0) +#define UNITY_IGNORE_AND_BAIL do { Unity.CurrentTestIgnored = 1; UNITY_OUTPUT_FLUSH(); TEST_ABORT(); } while (0) +#define RETURN_IF_FAIL_OR_IGNORE do { if (Unity.CurrentTestFailed || Unity.CurrentTestIgnored) { TEST_ABORT(); } } while (0) + +struct UNITY_STORAGE_T Unity; + +#ifdef UNITY_OUTPUT_COLOR +const char PROGMEM UnityStrOk[] = "\033[42mOK\033[00m"; +const char PROGMEM UnityStrPass[] = "\033[42mPASS\033[00m"; +const char PROGMEM UnityStrFail[] = "\033[41mFAIL\033[00m"; +const char PROGMEM UnityStrIgnore[] = "\033[43mIGNORE\033[00m"; +#else +const char PROGMEM UnityStrOk[] = "OK"; +const char PROGMEM UnityStrPass[] = "PASS"; +const char PROGMEM UnityStrFail[] = "FAIL"; +const char PROGMEM UnityStrIgnore[] = "IGNORE"; +#endif +static const char PROGMEM UnityStrNull[] = "NULL"; +static const char PROGMEM UnityStrSpacer[] = ". "; +static const char PROGMEM UnityStrExpected[] = " Expected "; +static const char PROGMEM UnityStrWas[] = " Was "; +static const char PROGMEM UnityStrGt[] = " to be greater than "; +static const char PROGMEM UnityStrLt[] = " to be less than "; +static const char PROGMEM UnityStrOrEqual[] = "or equal to "; +static const char PROGMEM UnityStrNotEqual[] = " to be not equal to "; +static const char PROGMEM UnityStrElement[] = " Element "; +static const char PROGMEM UnityStrByte[] = " Byte "; +static const char PROGMEM UnityStrMemory[] = " Memory Mismatch."; +static const char PROGMEM UnityStrDelta[] = " Values Not Within Delta "; +static const char PROGMEM UnityStrPointless[] = " You Asked Me To Compare Nothing, Which Was Pointless."; +static const char PROGMEM UnityStrNullPointerForExpected[] = " Expected pointer to be NULL"; +static const char PROGMEM UnityStrNullPointerForActual[] = " Actual pointer was NULL"; +#ifndef UNITY_EXCLUDE_FLOAT +static const char PROGMEM UnityStrNot[] = "Not "; +static const char PROGMEM UnityStrInf[] = "Infinity"; +static const char PROGMEM UnityStrNegInf[] = "Negative Infinity"; +static const char PROGMEM UnityStrNaN[] = "NaN"; +static const char PROGMEM UnityStrDet[] = "Determinate"; +static const char PROGMEM UnityStrInvalidFloatTrait[] = "Invalid Float Trait"; +#endif +const char PROGMEM UnityStrErrShorthand[] = "Unity Shorthand Support Disabled"; +const char PROGMEM UnityStrErrFloat[] = "Unity Floating Point Disabled"; +const char PROGMEM UnityStrErrDouble[] = "Unity Double Precision Disabled"; +const char PROGMEM UnityStrErr64[] = "Unity 64-bit Support Disabled"; +static const char PROGMEM UnityStrBreaker[] = "-----------------------"; +static const char PROGMEM UnityStrResultsTests[] = " Tests "; +static const char PROGMEM UnityStrResultsFailures[] = " Failures "; +static const char PROGMEM UnityStrResultsIgnored[] = " Ignored "; +#ifndef UNITY_EXCLUDE_DETAILS +static const char PROGMEM UnityStrDetail1Name[] = UNITY_DETAIL1_NAME " "; +static const char PROGMEM UnityStrDetail2Name[] = " " UNITY_DETAIL2_NAME " "; +#endif +/*----------------------------------------------- + * Pretty Printers & Test Result Output Handlers + *-----------------------------------------------*/ + +/*-----------------------------------------------*/ +/* Local helper function to print characters. */ +static void UnityPrintChar(const char* pch) +{ + /* printable characters plus CR & LF are printed */ + if ((*pch <= 126) && (*pch >= 32)) + { + UNITY_OUTPUT_CHAR(*pch); + } + /* write escaped carriage returns */ + else if (*pch == 13) + { + UNITY_OUTPUT_CHAR('\\'); + UNITY_OUTPUT_CHAR('r'); + } + /* write escaped line feeds */ + else if (*pch == 10) + { + UNITY_OUTPUT_CHAR('\\'); + UNITY_OUTPUT_CHAR('n'); + } + /* unprintable characters are shown as codes */ + else + { + UNITY_OUTPUT_CHAR('\\'); + UNITY_OUTPUT_CHAR('x'); + UnityPrintNumberHex((UNITY_UINT)*pch, 2); + } +} + +/*-----------------------------------------------*/ +/* Local helper function to print ANSI escape strings e.g. "\033[42m". */ +#ifdef UNITY_OUTPUT_COLOR +static UNITY_UINT UnityPrintAnsiEscapeString(const char* string) +{ + const char* pch = string; + UNITY_UINT count = 0; + + while (*pch && (*pch != 'm')) + { + UNITY_OUTPUT_CHAR(*pch); + pch++; + count++; + } + UNITY_OUTPUT_CHAR('m'); + count++; + + return count; +} +#endif + +/*-----------------------------------------------*/ +void UnityPrint(const char* string) +{ + const char* pch = string; + + if (pch != NULL) + { + while (*pch) + { +#ifdef UNITY_OUTPUT_COLOR + /* print ANSI escape code */ + if ((*pch == 27) && (*(pch + 1) == '[')) + { + pch += UnityPrintAnsiEscapeString(pch); + continue; + } +#endif + UnityPrintChar(pch); + pch++; + } + } +} +/*-----------------------------------------------*/ +void UnityPrintLen(const char* string, const UNITY_UINT32 length) +{ + const char* pch = string; + + if (pch != NULL) + { + while (*pch && ((UNITY_UINT32)(pch - string) < length)) + { + /* printable characters plus CR & LF are printed */ + if ((*pch <= 126) && (*pch >= 32)) + { + UNITY_OUTPUT_CHAR(*pch); + } + /* write escaped carriage returns */ + else if (*pch == 13) + { + UNITY_OUTPUT_CHAR('\\'); + UNITY_OUTPUT_CHAR('r'); + } + /* write escaped line feeds */ + else if (*pch == 10) + { + UNITY_OUTPUT_CHAR('\\'); + UNITY_OUTPUT_CHAR('n'); + } + /* unprintable characters are shown as codes */ + else + { + UNITY_OUTPUT_CHAR('\\'); + UNITY_OUTPUT_CHAR('x'); + UnityPrintNumberHex((UNITY_UINT)*pch, 2); + } + pch++; + } + } +} + +/*-----------------------------------------------*/ +void UnityPrintNumberByStyle(const UNITY_INT number, const UNITY_DISPLAY_STYLE_T style) +{ + if ((style & UNITY_DISPLAY_RANGE_INT) == UNITY_DISPLAY_RANGE_INT) + { + if (style == UNITY_DISPLAY_STYLE_CHAR) + { + /* printable characters plus CR & LF are printed */ + UNITY_OUTPUT_CHAR('\''); + if ((number <= 126) && (number >= 32)) + { + UNITY_OUTPUT_CHAR((int)number); + } + /* write escaped carriage returns */ + else if (number == 13) + { + UNITY_OUTPUT_CHAR('\\'); + UNITY_OUTPUT_CHAR('r'); + } + /* write escaped line feeds */ + else if (number == 10) + { + UNITY_OUTPUT_CHAR('\\'); + UNITY_OUTPUT_CHAR('n'); + } + /* unprintable characters are shown as codes */ + else + { + UNITY_OUTPUT_CHAR('\\'); + UNITY_OUTPUT_CHAR('x'); + UnityPrintNumberHex((UNITY_UINT)number, 2); + } + UNITY_OUTPUT_CHAR('\''); + } + else + { + UnityPrintNumber(number); + } + } + else if ((style & UNITY_DISPLAY_RANGE_UINT) == UNITY_DISPLAY_RANGE_UINT) + { + UnityPrintNumberUnsigned((UNITY_UINT)number); + } + else + { + UNITY_OUTPUT_CHAR('0'); + UNITY_OUTPUT_CHAR('x'); + UnityPrintNumberHex((UNITY_UINT)number, (char)((style & 0xF) * 2)); + } +} + +/*-----------------------------------------------*/ +void UnityPrintNumber(const UNITY_INT number_to_print) +{ + UNITY_UINT number = (UNITY_UINT)number_to_print; + + if (number_to_print < 0) + { + /* A negative number, including MIN negative */ + UNITY_OUTPUT_CHAR('-'); + number = (~number) + 1; + } + UnityPrintNumberUnsigned(number); +} + +/*----------------------------------------------- + * basically do an itoa using as little ram as possible */ +void UnityPrintNumberUnsigned(const UNITY_UINT number) +{ + UNITY_UINT divisor = 1; + + /* figure out initial divisor */ + while (number / divisor > 9) + { + divisor *= 10; + } + + /* now mod and print, then divide divisor */ + do + { + UNITY_OUTPUT_CHAR((char)('0' + (number / divisor % 10))); + divisor /= 10; + } while (divisor > 0); +} + +/*-----------------------------------------------*/ +void UnityPrintNumberHex(const UNITY_UINT number, const char nibbles_to_print) +{ + int nibble; + char nibbles = nibbles_to_print; + + if ((unsigned)nibbles > UNITY_MAX_NIBBLES) + { + nibbles = UNITY_MAX_NIBBLES; + } + + while (nibbles > 0) + { + nibbles--; + nibble = (int)(number >> (nibbles * 4)) & 0x0F; + if (nibble <= 9) + { + UNITY_OUTPUT_CHAR((char)('0' + nibble)); + } + else + { + UNITY_OUTPUT_CHAR((char)('A' - 10 + nibble)); + } + } +} + +/*-----------------------------------------------*/ +void UnityPrintMask(const UNITY_UINT mask, const UNITY_UINT number) +{ + UNITY_UINT current_bit = (UNITY_UINT)1 << (UNITY_INT_WIDTH - 1); + UNITY_INT32 i; + + for (i = 0; i < UNITY_INT_WIDTH; i++) + { + if (current_bit & mask) + { + if (current_bit & number) + { + UNITY_OUTPUT_CHAR('1'); + } + else + { + UNITY_OUTPUT_CHAR('0'); + } + } + else + { + UNITY_OUTPUT_CHAR('X'); + } + current_bit = current_bit >> 1; + } +} + +/*-----------------------------------------------*/ +#ifndef UNITY_EXCLUDE_FLOAT_PRINT +/* + * This function prints a floating-point value in a format similar to + * printf("%.7g") on a single-precision machine or printf("%.9g") on a + * double-precision machine. The 7th digit won't always be totally correct + * in single-precision operation (for that level of accuracy, a more + * complicated algorithm would be needed). + */ +void UnityPrintFloat(const UNITY_DOUBLE input_number) +{ +#ifdef UNITY_INCLUDE_DOUBLE + static const int sig_digits = 9; + static const UNITY_INT32 min_scaled = 100000000; + static const UNITY_INT32 max_scaled = 1000000000; +#else + static const int sig_digits = 7; + static const UNITY_INT32 min_scaled = 1000000; + static const UNITY_INT32 max_scaled = 10000000; +#endif + + UNITY_DOUBLE number = input_number; + + /* print minus sign (does not handle negative zero) */ + if (number < 0.0f) + { + UNITY_OUTPUT_CHAR('-'); + number = -number; + } + + /* handle zero, NaN, and +/- infinity */ + if (number == 0.0f) + { + UnityPrint("0"); + } + else if (isnan(number)) + { + UnityPrint("nan"); + } + else if (isinf(number)) + { + UnityPrint("inf"); + } + else + { + UNITY_INT32 n_int = 0; + UNITY_INT32 n; + int exponent = 0; + int decimals; + int digits; + char buf[16] = {0}; + + /* + * Scale up or down by powers of 10. To minimize rounding error, + * start with a factor/divisor of 10^10, which is the largest + * power of 10 that can be represented exactly. Finally, compute + * (exactly) the remaining power of 10 and perform one more + * multiplication or division. + */ + if (number < 1.0f) + { + UNITY_DOUBLE factor = 1.0f; + + while (number < (UNITY_DOUBLE)max_scaled / 1e10f) { number *= 1e10f; exponent -= 10; } + while (number * factor < (UNITY_DOUBLE)min_scaled) { factor *= 10.0f; exponent--; } + + number *= factor; + } + else if (number > (UNITY_DOUBLE)max_scaled) + { + UNITY_DOUBLE divisor = 1.0f; + + while (number > (UNITY_DOUBLE)min_scaled * 1e10f) { number /= 1e10f; exponent += 10; } + while (number / divisor > (UNITY_DOUBLE)max_scaled) { divisor *= 10.0f; exponent++; } + + number /= divisor; + } + else + { + /* + * In this range, we can split off the integer part before + * doing any multiplications. This reduces rounding error by + * freeing up significant bits in the fractional part. + */ + UNITY_DOUBLE factor = 1.0f; + n_int = (UNITY_INT32)number; + number -= (UNITY_DOUBLE)n_int; + + while (n_int < min_scaled) { n_int *= 10; factor *= 10.0f; exponent--; } + + number *= factor; + } + + /* round to nearest integer */ + n = ((UNITY_INT32)(number + number) + 1) / 2; + +#ifndef UNITY_ROUND_TIES_AWAY_FROM_ZERO + /* round to even if exactly between two integers */ + if ((n & 1) && (((UNITY_DOUBLE)n - number) == 0.5f)) + n--; +#endif + + n += n_int; + + if (n >= max_scaled) + { + n = min_scaled; + exponent++; + } + + /* determine where to place decimal point */ + decimals = ((exponent <= 0) && (exponent >= -(sig_digits + 3))) ? (-exponent) : (sig_digits - 1); + exponent += decimals; + + /* truncate trailing zeroes after decimal point */ + while ((decimals > 0) && ((n % 10) == 0)) + { + n /= 10; + decimals--; + } + + /* build up buffer in reverse order */ + digits = 0; + while ((n != 0) || (digits <= decimals)) + { + buf[digits++] = (char)('0' + n % 10); + n /= 10; + } + + /* print out buffer (backwards) */ + while (digits > 0) + { + if (digits == decimals) + { + UNITY_OUTPUT_CHAR('.'); + } + UNITY_OUTPUT_CHAR(buf[--digits]); + } + + /* print exponent if needed */ + if (exponent != 0) + { + UNITY_OUTPUT_CHAR('e'); + + if (exponent < 0) + { + UNITY_OUTPUT_CHAR('-'); + exponent = -exponent; + } + else + { + UNITY_OUTPUT_CHAR('+'); + } + + digits = 0; + while ((exponent != 0) || (digits < 2)) + { + buf[digits++] = (char)('0' + exponent % 10); + exponent /= 10; + } + while (digits > 0) + { + UNITY_OUTPUT_CHAR(buf[--digits]); + } + } + } +} +#endif /* ! UNITY_EXCLUDE_FLOAT_PRINT */ + +/*-----------------------------------------------*/ +static void UnityTestResultsBegin(const char* file, const UNITY_LINE_TYPE line) +{ +#ifdef UNITY_OUTPUT_FOR_ECLIPSE + UNITY_OUTPUT_CHAR('('); + UnityPrint(file); + UNITY_OUTPUT_CHAR(':'); + UnityPrintNumber((UNITY_INT)line); + UNITY_OUTPUT_CHAR(')'); + UNITY_OUTPUT_CHAR(' '); + UnityPrint(Unity.CurrentTestName); + UNITY_OUTPUT_CHAR(':'); +#else +#ifdef UNITY_OUTPUT_FOR_IAR_WORKBENCH + UnityPrint("'); + UnityPrint(Unity.CurrentTestName); + UnityPrint(" "); +#else +#ifdef UNITY_OUTPUT_FOR_QT_CREATOR + UnityPrint("file://"); + UnityPrint(file); + UNITY_OUTPUT_CHAR(':'); + UnityPrintNumber((UNITY_INT)line); + UNITY_OUTPUT_CHAR(' '); + UnityPrint(Unity.CurrentTestName); + UNITY_OUTPUT_CHAR(':'); +#else + UnityPrint(file); + UNITY_OUTPUT_CHAR(':'); + UnityPrintNumber((UNITY_INT)line); + UNITY_OUTPUT_CHAR(':'); + UnityPrint(Unity.CurrentTestName); + UNITY_OUTPUT_CHAR(':'); +#endif +#endif +#endif +} + +/*-----------------------------------------------*/ +static void UnityTestResultsFailBegin(const UNITY_LINE_TYPE line) +{ + UnityTestResultsBegin(Unity.TestFile, line); + UnityPrint(UnityStrFail); + UNITY_OUTPUT_CHAR(':'); +} + +/*-----------------------------------------------*/ +void UnityConcludeTest(void) +{ + if (Unity.CurrentTestIgnored) + { + Unity.TestIgnores++; + } + else if (!Unity.CurrentTestFailed) + { + UnityTestResultsBegin(Unity.TestFile, Unity.CurrentTestLineNumber); + UnityPrint(UnityStrPass); + } + else + { + Unity.TestFailures++; + } + + Unity.CurrentTestFailed = 0; + Unity.CurrentTestIgnored = 0; + UNITY_PRINT_EXEC_TIME(); + UNITY_PRINT_EOL(); + UNITY_FLUSH_CALL(); +} + +/*-----------------------------------------------*/ +static void UnityAddMsgIfSpecified(const char* msg) +{ + if (msg) + { + UnityPrint(UnityStrSpacer); + +#ifdef UNITY_PRINT_TEST_CONTEXT + UNITY_PRINT_TEST_CONTEXT(); +#endif +#ifndef UNITY_EXCLUDE_DETAILS + if (Unity.CurrentDetail1) + { + UnityPrint(UnityStrDetail1Name); + UnityPrint(Unity.CurrentDetail1); + if (Unity.CurrentDetail2) + { + UnityPrint(UnityStrDetail2Name); + UnityPrint(Unity.CurrentDetail2); + } + UnityPrint(UnityStrSpacer); + } +#endif + UnityPrint(msg); + } +} + +/*-----------------------------------------------*/ +static void UnityPrintExpectedAndActualStrings(const char* expected, const char* actual) +{ + UnityPrint(UnityStrExpected); + if (expected != NULL) + { + UNITY_OUTPUT_CHAR('\''); + UnityPrint(expected); + UNITY_OUTPUT_CHAR('\''); + } + else + { + UnityPrint(UnityStrNull); + } + UnityPrint(UnityStrWas); + if (actual != NULL) + { + UNITY_OUTPUT_CHAR('\''); + UnityPrint(actual); + UNITY_OUTPUT_CHAR('\''); + } + else + { + UnityPrint(UnityStrNull); + } +} + +/*-----------------------------------------------*/ +static void UnityPrintExpectedAndActualStringsLen(const char* expected, + const char* actual, + const UNITY_UINT32 length) +{ + UnityPrint(UnityStrExpected); + if (expected != NULL) + { + UNITY_OUTPUT_CHAR('\''); + UnityPrintLen(expected, length); + UNITY_OUTPUT_CHAR('\''); + } + else + { + UnityPrint(UnityStrNull); + } + UnityPrint(UnityStrWas); + if (actual != NULL) + { + UNITY_OUTPUT_CHAR('\''); + UnityPrintLen(actual, length); + UNITY_OUTPUT_CHAR('\''); + } + else + { + UnityPrint(UnityStrNull); + } +} + +/*----------------------------------------------- + * Assertion & Control Helpers + *-----------------------------------------------*/ + +/*-----------------------------------------------*/ +static int UnityIsOneArrayNull(UNITY_INTERNAL_PTR expected, + UNITY_INTERNAL_PTR actual, + const UNITY_LINE_TYPE lineNumber, + const char* msg) +{ + /* Both are NULL or same pointer */ + if (expected == actual) { return 0; } + + /* print and return true if just expected is NULL */ + if (expected == NULL) + { + UnityTestResultsFailBegin(lineNumber); + UnityPrint(UnityStrNullPointerForExpected); + UnityAddMsgIfSpecified(msg); + return 1; + } + + /* print and return true if just actual is NULL */ + if (actual == NULL) + { + UnityTestResultsFailBegin(lineNumber); + UnityPrint(UnityStrNullPointerForActual); + UnityAddMsgIfSpecified(msg); + return 1; + } + + return 0; /* return false if neither is NULL */ +} + +/*----------------------------------------------- + * Assertion Functions + *-----------------------------------------------*/ + +/*-----------------------------------------------*/ +void UnityAssertBits(const UNITY_INT mask, + const UNITY_INT expected, + const UNITY_INT actual, + const char* msg, + const UNITY_LINE_TYPE lineNumber) +{ + RETURN_IF_FAIL_OR_IGNORE; + + if ((mask & expected) != (mask & actual)) + { + UnityTestResultsFailBegin(lineNumber); + UnityPrint(UnityStrExpected); + UnityPrintMask((UNITY_UINT)mask, (UNITY_UINT)expected); + UnityPrint(UnityStrWas); + UnityPrintMask((UNITY_UINT)mask, (UNITY_UINT)actual); + UnityAddMsgIfSpecified(msg); + UNITY_FAIL_AND_BAIL; + } +} + +/*-----------------------------------------------*/ +void UnityAssertEqualNumber(const UNITY_INT expected, + const UNITY_INT actual, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_DISPLAY_STYLE_T style) +{ + RETURN_IF_FAIL_OR_IGNORE; + + if (expected != actual) + { + UnityTestResultsFailBegin(lineNumber); + UnityPrint(UnityStrExpected); + UnityPrintNumberByStyle(expected, style); + UnityPrint(UnityStrWas); + UnityPrintNumberByStyle(actual, style); + UnityAddMsgIfSpecified(msg); + UNITY_FAIL_AND_BAIL; + } +} + +/*-----------------------------------------------*/ +void UnityAssertGreaterOrLessOrEqualNumber(const UNITY_INT threshold, + const UNITY_INT actual, + const UNITY_COMPARISON_T compare, + const char *msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_DISPLAY_STYLE_T style) +{ + int failed = 0; + RETURN_IF_FAIL_OR_IGNORE; + + if ((threshold == actual) && (compare & UNITY_EQUAL_TO)) { return; } + if ((threshold == actual)) { failed = 1; } + + if ((style & UNITY_DISPLAY_RANGE_INT) == UNITY_DISPLAY_RANGE_INT) + { + if ((actual > threshold) && (compare & UNITY_SMALLER_THAN)) { failed = 1; } + if ((actual < threshold) && (compare & UNITY_GREATER_THAN)) { failed = 1; } + } + else /* UINT or HEX */ + { + if (((UNITY_UINT)actual > (UNITY_UINT)threshold) && (compare & UNITY_SMALLER_THAN)) { failed = 1; } + if (((UNITY_UINT)actual < (UNITY_UINT)threshold) && (compare & UNITY_GREATER_THAN)) { failed = 1; } + } + + if (failed) + { + UnityTestResultsFailBegin(lineNumber); + UnityPrint(UnityStrExpected); + UnityPrintNumberByStyle(actual, style); + if (compare & UNITY_GREATER_THAN) { UnityPrint(UnityStrGt); } + if (compare & UNITY_SMALLER_THAN) { UnityPrint(UnityStrLt); } + if (compare & UNITY_EQUAL_TO) { UnityPrint(UnityStrOrEqual); } + if (compare == UNITY_NOT_EQUAL) { UnityPrint(UnityStrNotEqual); } + UnityPrintNumberByStyle(threshold, style); + UnityAddMsgIfSpecified(msg); + UNITY_FAIL_AND_BAIL; + } +} + +#define UnityPrintPointlessAndBail() \ +do { \ + UnityTestResultsFailBegin(lineNumber); \ + UnityPrint(UnityStrPointless); \ + UnityAddMsgIfSpecified(msg); \ + UNITY_FAIL_AND_BAIL; \ +} while (0) + +/*-----------------------------------------------*/ +void UnityAssertEqualIntArray(UNITY_INTERNAL_PTR expected, + UNITY_INTERNAL_PTR actual, + const UNITY_UINT32 num_elements, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_DISPLAY_STYLE_T style, + const UNITY_FLAGS_T flags) +{ + UNITY_UINT32 elements = num_elements; + unsigned int length = style & 0xF; + unsigned int increment = 0; + + RETURN_IF_FAIL_OR_IGNORE; + + if (num_elements == 0) + { + UnityPrintPointlessAndBail(); + } + + if (expected == actual) + { + return; /* Both are NULL or same pointer */ + } + + if (UnityIsOneArrayNull(expected, actual, lineNumber, msg)) + { + UNITY_FAIL_AND_BAIL; + } + + while ((elements > 0) && (elements--)) + { + UNITY_INT expect_val; + UNITY_INT actual_val; + + switch (length) + { + case 1: + expect_val = *(UNITY_PTR_ATTRIBUTE const UNITY_INT8*)expected; + actual_val = *(UNITY_PTR_ATTRIBUTE const UNITY_INT8*)actual; + increment = sizeof(UNITY_INT8); + break; + + case 2: + expect_val = *(UNITY_PTR_ATTRIBUTE const UNITY_INT16*)expected; + actual_val = *(UNITY_PTR_ATTRIBUTE const UNITY_INT16*)actual; + increment = sizeof(UNITY_INT16); + break; + +#ifdef UNITY_SUPPORT_64 + case 8: + expect_val = *(UNITY_PTR_ATTRIBUTE const UNITY_INT64*)expected; + actual_val = *(UNITY_PTR_ATTRIBUTE const UNITY_INT64*)actual; + increment = sizeof(UNITY_INT64); + break; +#endif + + default: /* default is length 4 bytes */ + case 4: + expect_val = *(UNITY_PTR_ATTRIBUTE const UNITY_INT32*)expected; + actual_val = *(UNITY_PTR_ATTRIBUTE const UNITY_INT32*)actual; + increment = sizeof(UNITY_INT32); + length = 4; + break; + } + + if (expect_val != actual_val) + { + if ((style & UNITY_DISPLAY_RANGE_UINT) && (length < (UNITY_INT_WIDTH / 8))) + { /* For UINT, remove sign extension (padding 1's) from signed type casts above */ + UNITY_INT mask = 1; + mask = (mask << 8 * length) - 1; + expect_val &= mask; + actual_val &= mask; + } + UnityTestResultsFailBegin(lineNumber); + UnityPrint(UnityStrElement); + UnityPrintNumberUnsigned(num_elements - elements - 1); + UnityPrint(UnityStrExpected); + UnityPrintNumberByStyle(expect_val, style); + UnityPrint(UnityStrWas); + UnityPrintNumberByStyle(actual_val, style); + UnityAddMsgIfSpecified(msg); + UNITY_FAIL_AND_BAIL; + } + /* Walk through array by incrementing the pointers */ + if (flags == UNITY_ARRAY_TO_ARRAY) + { + expected = (UNITY_INTERNAL_PTR)((const char*)expected + increment); + } + actual = (UNITY_INTERNAL_PTR)((const char*)actual + increment); + } +} + +/*-----------------------------------------------*/ +#ifndef UNITY_EXCLUDE_FLOAT +/* Wrap this define in a function with variable types as float or double */ +#define UNITY_FLOAT_OR_DOUBLE_WITHIN(delta, expected, actual, diff) \ + if (isinf(expected) && isinf(actual) && (((expected) < 0) == ((actual) < 0))) return 1; \ + if (UNITY_NAN_CHECK) return 1; \ + (diff) = (actual) - (expected); \ + if ((diff) < 0) (diff) = -(diff); \ + if ((delta) < 0) (delta) = -(delta); \ + return !(isnan(diff) || isinf(diff) || ((diff) > (delta))) + /* This first part of this condition will catch any NaN or Infinite values */ +#ifndef UNITY_NAN_NOT_EQUAL_NAN + #define UNITY_NAN_CHECK isnan(expected) && isnan(actual) +#else + #define UNITY_NAN_CHECK 0 +#endif + +#ifndef UNITY_EXCLUDE_FLOAT_PRINT + #define UNITY_PRINT_EXPECTED_AND_ACTUAL_FLOAT(expected, actual) \ + do { \ + UnityPrint(UnityStrExpected); \ + UnityPrintFloat(expected); \ + UnityPrint(UnityStrWas); \ + UnityPrintFloat(actual); \ + } while (0) +#else + #define UNITY_PRINT_EXPECTED_AND_ACTUAL_FLOAT(expected, actual) \ + UnityPrint(UnityStrDelta) +#endif /* UNITY_EXCLUDE_FLOAT_PRINT */ + +/*-----------------------------------------------*/ +static int UnityFloatsWithin(UNITY_FLOAT delta, UNITY_FLOAT expected, UNITY_FLOAT actual) +{ + UNITY_FLOAT diff; + UNITY_FLOAT_OR_DOUBLE_WITHIN(delta, expected, actual, diff); +} + +/*-----------------------------------------------*/ +void UnityAssertEqualFloatArray(UNITY_PTR_ATTRIBUTE const UNITY_FLOAT* expected, + UNITY_PTR_ATTRIBUTE const UNITY_FLOAT* actual, + const UNITY_UINT32 num_elements, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_FLAGS_T flags) +{ + UNITY_UINT32 elements = num_elements; + UNITY_PTR_ATTRIBUTE const UNITY_FLOAT* ptr_expected = expected; + UNITY_PTR_ATTRIBUTE const UNITY_FLOAT* ptr_actual = actual; + + RETURN_IF_FAIL_OR_IGNORE; + + if (elements == 0) + { + UnityPrintPointlessAndBail(); + } + + if (expected == actual) + { + return; /* Both are NULL or same pointer */ + } + + if (UnityIsOneArrayNull((UNITY_INTERNAL_PTR)expected, (UNITY_INTERNAL_PTR)actual, lineNumber, msg)) + { + UNITY_FAIL_AND_BAIL; + } + + while (elements--) + { + if (!UnityFloatsWithin(*ptr_expected * UNITY_FLOAT_PRECISION, *ptr_expected, *ptr_actual)) + { + UnityTestResultsFailBegin(lineNumber); + UnityPrint(UnityStrElement); + UnityPrintNumberUnsigned(num_elements - elements - 1); + UNITY_PRINT_EXPECTED_AND_ACTUAL_FLOAT((UNITY_DOUBLE)*ptr_expected, (UNITY_DOUBLE)*ptr_actual); + UnityAddMsgIfSpecified(msg); + UNITY_FAIL_AND_BAIL; + } + if (flags == UNITY_ARRAY_TO_ARRAY) + { + ptr_expected++; + } + ptr_actual++; + } +} + +/*-----------------------------------------------*/ +void UnityAssertFloatsWithin(const UNITY_FLOAT delta, + const UNITY_FLOAT expected, + const UNITY_FLOAT actual, + const char* msg, + const UNITY_LINE_TYPE lineNumber) +{ + RETURN_IF_FAIL_OR_IGNORE; + + + if (!UnityFloatsWithin(delta, expected, actual)) + { + UnityTestResultsFailBegin(lineNumber); + UNITY_PRINT_EXPECTED_AND_ACTUAL_FLOAT((UNITY_DOUBLE)expected, (UNITY_DOUBLE)actual); + UnityAddMsgIfSpecified(msg); + UNITY_FAIL_AND_BAIL; + } +} + +/*-----------------------------------------------*/ +void UnityAssertFloatSpecial(const UNITY_FLOAT actual, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_FLOAT_TRAIT_T style) +{ + const char* trait_names[] = {UnityStrInf, UnityStrNegInf, UnityStrNaN, UnityStrDet}; + UNITY_INT should_be_trait = ((UNITY_INT)style & 1); + UNITY_INT is_trait = !should_be_trait; + UNITY_INT trait_index = (UNITY_INT)(style >> 1); + + RETURN_IF_FAIL_OR_IGNORE; + + switch (style) + { + case UNITY_FLOAT_IS_INF: + case UNITY_FLOAT_IS_NOT_INF: + is_trait = isinf(actual) && (actual > 0); + break; + case UNITY_FLOAT_IS_NEG_INF: + case UNITY_FLOAT_IS_NOT_NEG_INF: + is_trait = isinf(actual) && (actual < 0); + break; + + case UNITY_FLOAT_IS_NAN: + case UNITY_FLOAT_IS_NOT_NAN: + is_trait = isnan(actual) ? 1 : 0; + break; + + case UNITY_FLOAT_IS_DET: /* A determinate number is non infinite and not NaN. */ + case UNITY_FLOAT_IS_NOT_DET: + is_trait = !isinf(actual) && !isnan(actual); + break; + + default: /* including UNITY_FLOAT_INVALID_TRAIT */ + trait_index = 0; + trait_names[0] = UnityStrInvalidFloatTrait; + break; + } + + if (is_trait != should_be_trait) + { + UnityTestResultsFailBegin(lineNumber); + UnityPrint(UnityStrExpected); + if (!should_be_trait) + { + UnityPrint(UnityStrNot); + } + UnityPrint(trait_names[trait_index]); + UnityPrint(UnityStrWas); +#ifndef UNITY_EXCLUDE_FLOAT_PRINT + UnityPrintFloat((UNITY_DOUBLE)actual); +#else + if (should_be_trait) + { + UnityPrint(UnityStrNot); + } + UnityPrint(trait_names[trait_index]); +#endif + UnityAddMsgIfSpecified(msg); + UNITY_FAIL_AND_BAIL; + } +} + +#endif /* not UNITY_EXCLUDE_FLOAT */ + +/*-----------------------------------------------*/ +#ifndef UNITY_EXCLUDE_DOUBLE +static int UnityDoublesWithin(UNITY_DOUBLE delta, UNITY_DOUBLE expected, UNITY_DOUBLE actual) +{ + UNITY_DOUBLE diff; + UNITY_FLOAT_OR_DOUBLE_WITHIN(delta, expected, actual, diff); +} + +/*-----------------------------------------------*/ +void UnityAssertEqualDoubleArray(UNITY_PTR_ATTRIBUTE const UNITY_DOUBLE* expected, + UNITY_PTR_ATTRIBUTE const UNITY_DOUBLE* actual, + const UNITY_UINT32 num_elements, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_FLAGS_T flags) +{ + UNITY_UINT32 elements = num_elements; + UNITY_PTR_ATTRIBUTE const UNITY_DOUBLE* ptr_expected = expected; + UNITY_PTR_ATTRIBUTE const UNITY_DOUBLE* ptr_actual = actual; + + RETURN_IF_FAIL_OR_IGNORE; + + if (elements == 0) + { + UnityPrintPointlessAndBail(); + } + + if (expected == actual) + { + return; /* Both are NULL or same pointer */ + } + + if (UnityIsOneArrayNull((UNITY_INTERNAL_PTR)expected, (UNITY_INTERNAL_PTR)actual, lineNumber, msg)) + { + UNITY_FAIL_AND_BAIL; + } + + while (elements--) + { + if (!UnityDoublesWithin(*ptr_expected * UNITY_DOUBLE_PRECISION, *ptr_expected, *ptr_actual)) + { + UnityTestResultsFailBegin(lineNumber); + UnityPrint(UnityStrElement); + UnityPrintNumberUnsigned(num_elements - elements - 1); + UNITY_PRINT_EXPECTED_AND_ACTUAL_FLOAT(*ptr_expected, *ptr_actual); + UnityAddMsgIfSpecified(msg); + UNITY_FAIL_AND_BAIL; + } + if (flags == UNITY_ARRAY_TO_ARRAY) + { + ptr_expected++; + } + ptr_actual++; + } +} + +/*-----------------------------------------------*/ +void UnityAssertDoublesWithin(const UNITY_DOUBLE delta, + const UNITY_DOUBLE expected, + const UNITY_DOUBLE actual, + const char* msg, + const UNITY_LINE_TYPE lineNumber) +{ + RETURN_IF_FAIL_OR_IGNORE; + + if (!UnityDoublesWithin(delta, expected, actual)) + { + UnityTestResultsFailBegin(lineNumber); + UNITY_PRINT_EXPECTED_AND_ACTUAL_FLOAT(expected, actual); + UnityAddMsgIfSpecified(msg); + UNITY_FAIL_AND_BAIL; + } +} + +/*-----------------------------------------------*/ +void UnityAssertDoubleSpecial(const UNITY_DOUBLE actual, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_FLOAT_TRAIT_T style) +{ + const char* trait_names[] = {UnityStrInf, UnityStrNegInf, UnityStrNaN, UnityStrDet}; + UNITY_INT should_be_trait = ((UNITY_INT)style & 1); + UNITY_INT is_trait = !should_be_trait; + UNITY_INT trait_index = (UNITY_INT)(style >> 1); + + RETURN_IF_FAIL_OR_IGNORE; + + switch (style) + { + case UNITY_FLOAT_IS_INF: + case UNITY_FLOAT_IS_NOT_INF: + is_trait = isinf(actual) && (actual > 0); + break; + case UNITY_FLOAT_IS_NEG_INF: + case UNITY_FLOAT_IS_NOT_NEG_INF: + is_trait = isinf(actual) && (actual < 0); + break; + + case UNITY_FLOAT_IS_NAN: + case UNITY_FLOAT_IS_NOT_NAN: + is_trait = isnan(actual) ? 1 : 0; + break; + + case UNITY_FLOAT_IS_DET: /* A determinate number is non infinite and not NaN. */ + case UNITY_FLOAT_IS_NOT_DET: + is_trait = !isinf(actual) && !isnan(actual); + break; + + default: /* including UNITY_FLOAT_INVALID_TRAIT */ + trait_index = 0; + trait_names[0] = UnityStrInvalidFloatTrait; + break; + } + + if (is_trait != should_be_trait) + { + UnityTestResultsFailBegin(lineNumber); + UnityPrint(UnityStrExpected); + if (!should_be_trait) + { + UnityPrint(UnityStrNot); + } + UnityPrint(trait_names[trait_index]); + UnityPrint(UnityStrWas); +#ifndef UNITY_EXCLUDE_FLOAT_PRINT + UnityPrintFloat(actual); +#else + if (should_be_trait) + { + UnityPrint(UnityStrNot); + } + UnityPrint(trait_names[trait_index]); +#endif + UnityAddMsgIfSpecified(msg); + UNITY_FAIL_AND_BAIL; + } +} + +#endif /* not UNITY_EXCLUDE_DOUBLE */ + +/*-----------------------------------------------*/ +void UnityAssertNumbersWithin(const UNITY_UINT delta, + const UNITY_INT expected, + const UNITY_INT actual, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_DISPLAY_STYLE_T style) +{ + RETURN_IF_FAIL_OR_IGNORE; + + if ((style & UNITY_DISPLAY_RANGE_INT) == UNITY_DISPLAY_RANGE_INT) + { + if (actual > expected) + { + Unity.CurrentTestFailed = (((UNITY_UINT)actual - (UNITY_UINT)expected) > delta); + } + else + { + Unity.CurrentTestFailed = (((UNITY_UINT)expected - (UNITY_UINT)actual) > delta); + } + } + else + { + if ((UNITY_UINT)actual > (UNITY_UINT)expected) + { + Unity.CurrentTestFailed = (((UNITY_UINT)actual - (UNITY_UINT)expected) > delta); + } + else + { + Unity.CurrentTestFailed = (((UNITY_UINT)expected - (UNITY_UINT)actual) > delta); + } + } + + if (Unity.CurrentTestFailed) + { + UnityTestResultsFailBegin(lineNumber); + UnityPrint(UnityStrDelta); + UnityPrintNumberByStyle((UNITY_INT)delta, style); + UnityPrint(UnityStrExpected); + UnityPrintNumberByStyle(expected, style); + UnityPrint(UnityStrWas); + UnityPrintNumberByStyle(actual, style); + UnityAddMsgIfSpecified(msg); + UNITY_FAIL_AND_BAIL; + } +} + +/*-----------------------------------------------*/ +void UnityAssertNumbersArrayWithin(const UNITY_UINT delta, + UNITY_INTERNAL_PTR expected, + UNITY_INTERNAL_PTR actual, + const UNITY_UINT32 num_elements, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_DISPLAY_STYLE_T style, + const UNITY_FLAGS_T flags) +{ + UNITY_UINT32 elements = num_elements; + unsigned int length = style & 0xF; + unsigned int increment = 0; + + RETURN_IF_FAIL_OR_IGNORE; + + if (num_elements == 0) + { + UnityPrintPointlessAndBail(); + } + + if (expected == actual) + { + return; /* Both are NULL or same pointer */ + } + + if (UnityIsOneArrayNull(expected, actual, lineNumber, msg)) + { + UNITY_FAIL_AND_BAIL; + } + + while ((elements > 0) && (elements--)) + { + UNITY_INT expect_val; + UNITY_INT actual_val; + + switch (length) + { + case 1: + expect_val = *(UNITY_PTR_ATTRIBUTE const UNITY_INT8*)expected; + actual_val = *(UNITY_PTR_ATTRIBUTE const UNITY_INT8*)actual; + increment = sizeof(UNITY_INT8); + break; + + case 2: + expect_val = *(UNITY_PTR_ATTRIBUTE const UNITY_INT16*)expected; + actual_val = *(UNITY_PTR_ATTRIBUTE const UNITY_INT16*)actual; + increment = sizeof(UNITY_INT16); + break; + +#ifdef UNITY_SUPPORT_64 + case 8: + expect_val = *(UNITY_PTR_ATTRIBUTE const UNITY_INT64*)expected; + actual_val = *(UNITY_PTR_ATTRIBUTE const UNITY_INT64*)actual; + increment = sizeof(UNITY_INT64); + break; +#endif + + default: /* default is length 4 bytes */ + case 4: + expect_val = *(UNITY_PTR_ATTRIBUTE const UNITY_INT32*)expected; + actual_val = *(UNITY_PTR_ATTRIBUTE const UNITY_INT32*)actual; + increment = sizeof(UNITY_INT32); + length = 4; + break; + } + + if ((style & UNITY_DISPLAY_RANGE_INT) == UNITY_DISPLAY_RANGE_INT) + { + if (actual_val > expect_val) + { + Unity.CurrentTestFailed = (((UNITY_UINT)actual_val - (UNITY_UINT)expect_val) > delta); + } + else + { + Unity.CurrentTestFailed = (((UNITY_UINT)expect_val - (UNITY_UINT)actual_val) > delta); + } + } + else + { + if ((UNITY_UINT)actual_val > (UNITY_UINT)expect_val) + { + Unity.CurrentTestFailed = (((UNITY_UINT)actual_val - (UNITY_UINT)expect_val) > delta); + } + else + { + Unity.CurrentTestFailed = (((UNITY_UINT)expect_val - (UNITY_UINT)actual_val) > delta); + } + } + + if (Unity.CurrentTestFailed) + { + if ((style & UNITY_DISPLAY_RANGE_UINT) && (length < (UNITY_INT_WIDTH / 8))) + { /* For UINT, remove sign extension (padding 1's) from signed type casts above */ + UNITY_INT mask = 1; + mask = (mask << 8 * length) - 1; + expect_val &= mask; + actual_val &= mask; + } + UnityTestResultsFailBegin(lineNumber); + UnityPrint(UnityStrDelta); + UnityPrintNumberByStyle((UNITY_INT)delta, style); + UnityPrint(UnityStrElement); + UnityPrintNumberUnsigned(num_elements - elements - 1); + UnityPrint(UnityStrExpected); + UnityPrintNumberByStyle(expect_val, style); + UnityPrint(UnityStrWas); + UnityPrintNumberByStyle(actual_val, style); + UnityAddMsgIfSpecified(msg); + UNITY_FAIL_AND_BAIL; + } + /* Walk through array by incrementing the pointers */ + if (flags == UNITY_ARRAY_TO_ARRAY) + { + expected = (UNITY_INTERNAL_PTR)((const char*)expected + increment); + } + actual = (UNITY_INTERNAL_PTR)((const char*)actual + increment); + } +} + +/*-----------------------------------------------*/ +void UnityAssertEqualString(const char* expected, + const char* actual, + const char* msg, + const UNITY_LINE_TYPE lineNumber) +{ + UNITY_UINT32 i; + + RETURN_IF_FAIL_OR_IGNORE; + + /* if both pointers not null compare the strings */ + if (expected && actual) + { + for (i = 0; expected[i] || actual[i]; i++) + { + if (expected[i] != actual[i]) + { + Unity.CurrentTestFailed = 1; + break; + } + } + } + else + { /* handle case of one pointers being null (if both null, test should pass) */ + if (expected != actual) + { + Unity.CurrentTestFailed = 1; + } + } + + if (Unity.CurrentTestFailed) + { + UnityTestResultsFailBegin(lineNumber); + UnityPrintExpectedAndActualStrings(expected, actual); + UnityAddMsgIfSpecified(msg); + UNITY_FAIL_AND_BAIL; + } +} + +/*-----------------------------------------------*/ +void UnityAssertEqualStringLen(const char* expected, + const char* actual, + const UNITY_UINT32 length, + const char* msg, + const UNITY_LINE_TYPE lineNumber) +{ + UNITY_UINT32 i; + + RETURN_IF_FAIL_OR_IGNORE; + + /* if both pointers not null compare the strings */ + if (expected && actual) + { + for (i = 0; (i < length) && (expected[i] || actual[i]); i++) + { + if (expected[i] != actual[i]) + { + Unity.CurrentTestFailed = 1; + break; + } + } + } + else + { /* handle case of one pointers being null (if both null, test should pass) */ + if (expected != actual) + { + Unity.CurrentTestFailed = 1; + } + } + + if (Unity.CurrentTestFailed) + { + UnityTestResultsFailBegin(lineNumber); + UnityPrintExpectedAndActualStringsLen(expected, actual, length); + UnityAddMsgIfSpecified(msg); + UNITY_FAIL_AND_BAIL; + } +} + +/*-----------------------------------------------*/ +void UnityAssertEqualStringArray(UNITY_INTERNAL_PTR expected, + const char** actual, + const UNITY_UINT32 num_elements, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_FLAGS_T flags) +{ + UNITY_UINT32 i = 0; + UNITY_UINT32 j = 0; + const char* expd = NULL; + const char* act = NULL; + + RETURN_IF_FAIL_OR_IGNORE; + + /* if no elements, it's an error */ + if (num_elements == 0) + { + UnityPrintPointlessAndBail(); + } + + if ((const void*)expected == (const void*)actual) + { + return; /* Both are NULL or same pointer */ + } + + if (UnityIsOneArrayNull((UNITY_INTERNAL_PTR)expected, (UNITY_INTERNAL_PTR)actual, lineNumber, msg)) + { + UNITY_FAIL_AND_BAIL; + } + + if (flags != UNITY_ARRAY_TO_ARRAY) + { + expd = (const char*)expected; + } + + do + { + act = actual[j]; + if (flags == UNITY_ARRAY_TO_ARRAY) + { + expd = ((const char* const*)expected)[j]; + } + + /* if both pointers not null compare the strings */ + if (expd && act) + { + for (i = 0; expd[i] || act[i]; i++) + { + if (expd[i] != act[i]) + { + Unity.CurrentTestFailed = 1; + break; + } + } + } + else + { /* handle case of one pointers being null (if both null, test should pass) */ + if (expd != act) + { + Unity.CurrentTestFailed = 1; + } + } + + if (Unity.CurrentTestFailed) + { + UnityTestResultsFailBegin(lineNumber); + if (num_elements > 1) + { + UnityPrint(UnityStrElement); + UnityPrintNumberUnsigned(j); + } + UnityPrintExpectedAndActualStrings(expd, act); + UnityAddMsgIfSpecified(msg); + UNITY_FAIL_AND_BAIL; + } + } while (++j < num_elements); +} + +/*-----------------------------------------------*/ +void UnityAssertEqualMemory(UNITY_INTERNAL_PTR expected, + UNITY_INTERNAL_PTR actual, + const UNITY_UINT32 length, + const UNITY_UINT32 num_elements, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_FLAGS_T flags) +{ + UNITY_PTR_ATTRIBUTE const unsigned char* ptr_exp = (UNITY_PTR_ATTRIBUTE const unsigned char*)expected; + UNITY_PTR_ATTRIBUTE const unsigned char* ptr_act = (UNITY_PTR_ATTRIBUTE const unsigned char*)actual; + UNITY_UINT32 elements = num_elements; + UNITY_UINT32 bytes; + + RETURN_IF_FAIL_OR_IGNORE; + + if ((elements == 0) || (length == 0)) + { + UnityPrintPointlessAndBail(); + } + + if (expected == actual) + { + return; /* Both are NULL or same pointer */ + } + + if (UnityIsOneArrayNull(expected, actual, lineNumber, msg)) + { + UNITY_FAIL_AND_BAIL; + } + + while (elements--) + { + bytes = length; + while (bytes--) + { + if (*ptr_exp != *ptr_act) + { + UnityTestResultsFailBegin(lineNumber); + UnityPrint(UnityStrMemory); + if (num_elements > 1) + { + UnityPrint(UnityStrElement); + UnityPrintNumberUnsigned(num_elements - elements - 1); + } + UnityPrint(UnityStrByte); + UnityPrintNumberUnsigned(length - bytes - 1); + UnityPrint(UnityStrExpected); + UnityPrintNumberByStyle(*ptr_exp, UNITY_DISPLAY_STYLE_HEX8); + UnityPrint(UnityStrWas); + UnityPrintNumberByStyle(*ptr_act, UNITY_DISPLAY_STYLE_HEX8); + UnityAddMsgIfSpecified(msg); + UNITY_FAIL_AND_BAIL; + } + ptr_exp++; + ptr_act++; + } + if (flags == UNITY_ARRAY_TO_VAL) + { + ptr_exp = (UNITY_PTR_ATTRIBUTE const unsigned char*)expected; + } + } +} + +/*-----------------------------------------------*/ + +static union +{ + UNITY_INT8 i8; + UNITY_INT16 i16; + UNITY_INT32 i32; +#ifdef UNITY_SUPPORT_64 + UNITY_INT64 i64; +#endif +#ifndef UNITY_EXCLUDE_FLOAT + float f; +#endif +#ifndef UNITY_EXCLUDE_DOUBLE + double d; +#endif +} UnityQuickCompare; + +UNITY_INTERNAL_PTR UnityNumToPtr(const UNITY_INT num, const UNITY_UINT8 size) +{ + switch(size) + { + case 1: + UnityQuickCompare.i8 = (UNITY_INT8)num; + return (UNITY_INTERNAL_PTR)(&UnityQuickCompare.i8); + + case 2: + UnityQuickCompare.i16 = (UNITY_INT16)num; + return (UNITY_INTERNAL_PTR)(&UnityQuickCompare.i16); + +#ifdef UNITY_SUPPORT_64 + case 8: + UnityQuickCompare.i64 = (UNITY_INT64)num; + return (UNITY_INTERNAL_PTR)(&UnityQuickCompare.i64); +#endif + + default: /* 4 bytes */ + UnityQuickCompare.i32 = (UNITY_INT32)num; + return (UNITY_INTERNAL_PTR)(&UnityQuickCompare.i32); + } +} + +#ifndef UNITY_EXCLUDE_FLOAT +/*-----------------------------------------------*/ +UNITY_INTERNAL_PTR UnityFloatToPtr(const float num) +{ + UnityQuickCompare.f = num; + return (UNITY_INTERNAL_PTR)(&UnityQuickCompare.f); +} +#endif + +#ifndef UNITY_EXCLUDE_DOUBLE +/*-----------------------------------------------*/ +UNITY_INTERNAL_PTR UnityDoubleToPtr(const double num) +{ + UnityQuickCompare.d = num; + return (UNITY_INTERNAL_PTR)(&UnityQuickCompare.d); +} +#endif + +/*----------------------------------------------- + * printf helper function + *-----------------------------------------------*/ +#ifdef UNITY_INCLUDE_PRINT_FORMATTED +static void UnityPrintFVA(const char* format, va_list va) +{ + const char* pch = format; + if (pch != NULL) + { + while (*pch) + { + /* format identification character */ + if (*pch == '%') + { + pch++; + + if (pch != NULL) + { + switch (*pch) + { + case 'd': + case 'i': + { + const int number = va_arg(va, int); + UnityPrintNumber((UNITY_INT)number); + break; + } +#ifndef UNITY_EXCLUDE_FLOAT_PRINT + case 'f': + case 'g': + { + const double number = va_arg(va, double); + UnityPrintFloat((UNITY_DOUBLE)number); + break; + } +#endif + case 'u': + { + const unsigned int number = va_arg(va, unsigned int); + UnityPrintNumberUnsigned((UNITY_UINT)number); + break; + } + case 'b': + { + const unsigned int number = va_arg(va, unsigned int); + const UNITY_UINT mask = (UNITY_UINT)0 - (UNITY_UINT)1; + UNITY_OUTPUT_CHAR('0'); + UNITY_OUTPUT_CHAR('b'); + UnityPrintMask(mask, (UNITY_UINT)number); + break; + } + case 'x': + case 'X': + case 'p': + { + const unsigned int number = va_arg(va, unsigned int); + UNITY_OUTPUT_CHAR('0'); + UNITY_OUTPUT_CHAR('x'); + UnityPrintNumberHex((UNITY_UINT)number, 8); + break; + } + case 'c': + { + const int ch = va_arg(va, int); + UnityPrintChar((const char *)&ch); + break; + } + case 's': + { + const char * string = va_arg(va, const char *); + UnityPrint(string); + break; + } + case '%': + { + UnityPrintChar(pch); + break; + } + default: + { + /* print the unknown format character */ + UNITY_OUTPUT_CHAR('%'); + UnityPrintChar(pch); + break; + } + } + } + } +#ifdef UNITY_OUTPUT_COLOR + /* print ANSI escape code */ + else if ((*pch == 27) && (*(pch + 1) == '[')) + { + pch += UnityPrintAnsiEscapeString(pch); + continue; + } +#endif + else if (*pch == '\n') + { + UNITY_PRINT_EOL(); + } + else + { + UnityPrintChar(pch); + } + + pch++; + } + } +} + +void UnityPrintF(const UNITY_LINE_TYPE line, const char* format, ...) +{ + UnityTestResultsBegin(Unity.TestFile, line); + UnityPrint("INFO"); + if(format != NULL) + { + UnityPrint(": "); + va_list va; + va_start(va, format); + UnityPrintFVA(format, va); + va_end(va); + } + UNITY_PRINT_EOL(); +} +#endif /* ! UNITY_INCLUDE_PRINT_FORMATTED */ + + +/*----------------------------------------------- + * Control Functions + *-----------------------------------------------*/ + +/*-----------------------------------------------*/ +void UnityFail(const char* msg, const UNITY_LINE_TYPE line) +{ + RETURN_IF_FAIL_OR_IGNORE; + + UnityTestResultsBegin(Unity.TestFile, line); + UnityPrint(UnityStrFail); + if (msg != NULL) + { + UNITY_OUTPUT_CHAR(':'); + +#ifdef UNITY_PRINT_TEST_CONTEXT + UNITY_PRINT_TEST_CONTEXT(); +#endif +#ifndef UNITY_EXCLUDE_DETAILS + if (Unity.CurrentDetail1) + { + UnityPrint(UnityStrDetail1Name); + UnityPrint(Unity.CurrentDetail1); + if (Unity.CurrentDetail2) + { + UnityPrint(UnityStrDetail2Name); + UnityPrint(Unity.CurrentDetail2); + } + UnityPrint(UnityStrSpacer); + } +#endif + if (msg[0] != ' ') + { + UNITY_OUTPUT_CHAR(' '); + } + UnityPrint(msg); + } + + UNITY_FAIL_AND_BAIL; +} + +/*-----------------------------------------------*/ +void UnityIgnore(const char* msg, const UNITY_LINE_TYPE line) +{ + RETURN_IF_FAIL_OR_IGNORE; + + UnityTestResultsBegin(Unity.TestFile, line); + UnityPrint(UnityStrIgnore); + if (msg != NULL) + { + UNITY_OUTPUT_CHAR(':'); + UNITY_OUTPUT_CHAR(' '); + UnityPrint(msg); + } + UNITY_IGNORE_AND_BAIL; +} + +/*-----------------------------------------------*/ +void UnityMessage(const char* msg, const UNITY_LINE_TYPE line) +{ + UnityTestResultsBegin(Unity.TestFile, line); + UnityPrint("INFO"); + if (msg != NULL) + { + UNITY_OUTPUT_CHAR(':'); + UNITY_OUTPUT_CHAR(' '); + UnityPrint(msg); + } + UNITY_PRINT_EOL(); +} + +/*-----------------------------------------------*/ +/* If we have not defined our own test runner, then include our default test runner to make life easier */ +#ifndef UNITY_SKIP_DEFAULT_RUNNER +void UnityDefaultTestRun(UnityTestFunction Func, const char* FuncName, const int FuncLineNum) +{ + Unity.CurrentTestName = FuncName; + Unity.CurrentTestLineNumber = (UNITY_LINE_TYPE)FuncLineNum; + Unity.NumberOfTests++; + UNITY_CLR_DETAILS(); + UNITY_EXEC_TIME_START(); + if (TEST_PROTECT()) + { + setUp(); + Func(); + } + if (TEST_PROTECT()) + { + tearDown(); + } + UNITY_EXEC_TIME_STOP(); + UnityConcludeTest(); +} +#endif + +/*-----------------------------------------------*/ +void UnitySetTestFile(const char* filename) +{ + Unity.TestFile = filename; +} + +/*-----------------------------------------------*/ +void UnityBegin(const char* filename) +{ + Unity.TestFile = filename; + Unity.CurrentTestName = NULL; + Unity.CurrentTestLineNumber = 0; + Unity.NumberOfTests = 0; + Unity.TestFailures = 0; + Unity.TestIgnores = 0; + Unity.CurrentTestFailed = 0; + Unity.CurrentTestIgnored = 0; + + UNITY_CLR_DETAILS(); + UNITY_OUTPUT_START(); +} + +/*-----------------------------------------------*/ +int UnityEnd(void) +{ + UNITY_PRINT_EOL(); + UnityPrint(UnityStrBreaker); + UNITY_PRINT_EOL(); + UnityPrintNumber((UNITY_INT)(Unity.NumberOfTests)); + UnityPrint(UnityStrResultsTests); + UnityPrintNumber((UNITY_INT)(Unity.TestFailures)); + UnityPrint(UnityStrResultsFailures); + UnityPrintNumber((UNITY_INT)(Unity.TestIgnores)); + UnityPrint(UnityStrResultsIgnored); + UNITY_PRINT_EOL(); + if (Unity.TestFailures == 0U) + { + UnityPrint(UnityStrOk); + } + else + { + UnityPrint(UnityStrFail); +#ifdef UNITY_DIFFERENTIATE_FINAL_FAIL + UNITY_OUTPUT_CHAR('E'); UNITY_OUTPUT_CHAR('D'); +#endif + } + UNITY_PRINT_EOL(); + UNITY_FLUSH_CALL(); + UNITY_OUTPUT_COMPLETE(); + return (int)(Unity.TestFailures); +} + +/*----------------------------------------------- + * Command Line Argument Support + *-----------------------------------------------*/ +#ifdef UNITY_USE_COMMAND_LINE_ARGS + +char* UnityOptionIncludeNamed = NULL; +char* UnityOptionExcludeNamed = NULL; +int UnityVerbosity = 1; + +/*-----------------------------------------------*/ +int UnityParseOptions(int argc, char** argv) +{ + int i; + UnityOptionIncludeNamed = NULL; + UnityOptionExcludeNamed = NULL; + + for (i = 1; i < argc; i++) + { + if (argv[i][0] == '-') + { + switch (argv[i][1]) + { + case 'l': /* list tests */ + return -1; + case 'n': /* include tests with name including this string */ + case 'f': /* an alias for -n */ + if (argv[i][2] == '=') + { + UnityOptionIncludeNamed = &argv[i][3]; + } + else if (++i < argc) + { + UnityOptionIncludeNamed = argv[i]; + } + else + { + UnityPrint("ERROR: No Test String to Include Matches For"); + UNITY_PRINT_EOL(); + return 1; + } + break; + case 'q': /* quiet */ + UnityVerbosity = 0; + break; + case 'v': /* verbose */ + UnityVerbosity = 2; + break; + case 'x': /* exclude tests with name including this string */ + if (argv[i][2] == '=') + { + UnityOptionExcludeNamed = &argv[i][3]; + } + else if (++i < argc) + { + UnityOptionExcludeNamed = argv[i]; + } + else + { + UnityPrint("ERROR: No Test String to Exclude Matches For"); + UNITY_PRINT_EOL(); + return 1; + } + break; + default: + UnityPrint("ERROR: Unknown Option "); + UNITY_OUTPUT_CHAR(argv[i][1]); + UNITY_PRINT_EOL(); + return 1; + } + } + } + + return 0; +} + +/*-----------------------------------------------*/ +int IsStringInBiggerString(const char* longstring, const char* shortstring) +{ + const char* lptr = longstring; + const char* sptr = shortstring; + const char* lnext = lptr; + + if (*sptr == '*') + { + return 1; + } + + while (*lptr) + { + lnext = lptr + 1; + + /* If they current bytes match, go on to the next bytes */ + while (*lptr && *sptr && (*lptr == *sptr)) + { + lptr++; + sptr++; + + /* We're done if we match the entire string or up to a wildcard */ + if (*sptr == '*') + return 1; + if (*sptr == ',') + return 1; + if (*sptr == '"') + return 1; + if (*sptr == '\'') + return 1; + if (*sptr == ':') + return 2; + if (*sptr == 0) + return 1; + } + + /* Otherwise we start in the long pointer 1 character further and try again */ + lptr = lnext; + sptr = shortstring; + } + + return 0; +} + +/*-----------------------------------------------*/ +int UnityStringArgumentMatches(const char* str) +{ + int retval; + const char* ptr1; + const char* ptr2; + const char* ptrf; + + /* Go through the options and get the substrings for matching one at a time */ + ptr1 = str; + while (ptr1[0] != 0) + { + if ((ptr1[0] == '"') || (ptr1[0] == '\'')) + { + ptr1++; + } + + /* look for the start of the next partial */ + ptr2 = ptr1; + ptrf = 0; + do + { + ptr2++; + if ((ptr2[0] == ':') && (ptr2[1] != 0) && (ptr2[0] != '\'') && (ptr2[0] != '"') && (ptr2[0] != ',')) + { + ptrf = &ptr2[1]; + } + } while ((ptr2[0] != 0) && (ptr2[0] != '\'') && (ptr2[0] != '"') && (ptr2[0] != ',')); + + while ((ptr2[0] != 0) && ((ptr2[0] == ':') || (ptr2[0] == '\'') || (ptr2[0] == '"') || (ptr2[0] == ','))) + { + ptr2++; + } + + /* done if complete filename match */ + retval = IsStringInBiggerString(Unity.TestFile, ptr1); + if (retval == 1) + { + return retval; + } + + /* done if testname match after filename partial match */ + if ((retval == 2) && (ptrf != 0)) + { + if (IsStringInBiggerString(Unity.CurrentTestName, ptrf)) + { + return 1; + } + } + + /* done if complete testname match */ + if (IsStringInBiggerString(Unity.CurrentTestName, ptr1) == 1) + { + return 1; + } + + ptr1 = ptr2; + } + + /* we couldn't find a match for any substrings */ + return 0; +} + +/*-----------------------------------------------*/ +int UnityTestMatches(void) +{ + /* Check if this test name matches the included test pattern */ + int retval; + if (UnityOptionIncludeNamed) + { + retval = UnityStringArgumentMatches(UnityOptionIncludeNamed); + } + else + { + retval = 1; + } + + /* Check if this test name matches the excluded test pattern */ + if (UnityOptionExcludeNamed) + { + if (UnityStringArgumentMatches(UnityOptionExcludeNamed)) + { + retval = 0; + } + } + + return retval; +} + +#endif /* UNITY_USE_COMMAND_LINE_ARGS */ +/*-----------------------------------------------*/ diff --git a/integration-tests/src/bindings_tests/unity/unity.h b/integration-tests/src/bindings_tests/unity/unity.h new file mode 100644 index 000000000..14225a354 --- /dev/null +++ b/integration-tests/src/bindings_tests/unity/unity.h @@ -0,0 +1,661 @@ +/* ========================================== + Unity Project - A Test Framework for C + Copyright (c) 2007-21 Mike Karlesky, Mark VanderVoord, Greg Williams + [Released under MIT License. Please refer to license.txt for details] +========================================== */ + +#ifndef UNITY_FRAMEWORK_H +#define UNITY_FRAMEWORK_H +#define UNITY + +#define UNITY_VERSION_MAJOR 2 +#define UNITY_VERSION_MINOR 5 +#define UNITY_VERSION_BUILD 4 +#define UNITY_VERSION ((UNITY_VERSION_MAJOR << 16) | (UNITY_VERSION_MINOR << 8) | UNITY_VERSION_BUILD) + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "unity_internals.h" + +/*------------------------------------------------------- + * Test Setup / Teardown + *-------------------------------------------------------*/ + +/* These functions are intended to be called before and after each test. + * If using unity directly, these will need to be provided for each test + * executable built. If you are using the test runner generator and/or + * Ceedling, these are optional. */ +void setUp(void); +void tearDown(void); + +/* These functions are intended to be called at the beginning and end of an + * entire test suite. suiteTearDown() is passed the number of tests that + * failed, and its return value becomes the exit code of main(). If using + * Unity directly, you're in charge of calling these if they are desired. + * If using Ceedling or the test runner generator, these will be called + * automatically if they exist. */ +void suiteSetUp(void); +int suiteTearDown(int num_failures); + +/*------------------------------------------------------- + * Test Reset and Verify + *-------------------------------------------------------*/ + +/* These functions are intended to be called before during tests in order + * to support complex test loops, etc. Both are NOT built into Unity. Instead + * the test runner generator will create them. resetTest will run teardown and + * setup again, verifying any end-of-test needs between. verifyTest will only + * run the verification. */ +void resetTest(void); +void verifyTest(void); + +/*------------------------------------------------------- + * Configuration Options + *------------------------------------------------------- + * All options described below should be passed as a compiler flag to all files using Unity. If you must add #defines, place them BEFORE the #include above. + + * Integers/longs/pointers + * - Unity attempts to automatically discover your integer sizes + * - define UNITY_EXCLUDE_STDINT_H to stop attempting to look in + * - define UNITY_EXCLUDE_LIMITS_H to stop attempting to look in + * - If you cannot use the automatic methods above, you can force Unity by using these options: + * - define UNITY_SUPPORT_64 + * - set UNITY_INT_WIDTH + * - set UNITY_LONG_WIDTH + * - set UNITY_POINTER_WIDTH + + * Floats + * - define UNITY_EXCLUDE_FLOAT to disallow floating point comparisons + * - define UNITY_FLOAT_PRECISION to specify the precision to use when doing TEST_ASSERT_EQUAL_FLOAT + * - define UNITY_FLOAT_TYPE to specify doubles instead of single precision floats + * - define UNITY_INCLUDE_DOUBLE to allow double floating point comparisons + * - define UNITY_EXCLUDE_DOUBLE to disallow double floating point comparisons (default) + * - define UNITY_DOUBLE_PRECISION to specify the precision to use when doing TEST_ASSERT_EQUAL_DOUBLE + * - define UNITY_DOUBLE_TYPE to specify something other than double + * - define UNITY_EXCLUDE_FLOAT_PRINT to trim binary size, won't print floating point values in errors + + * Output + * - by default, Unity prints to standard out with putchar. define UNITY_OUTPUT_CHAR(a) with a different function if desired + * - define UNITY_DIFFERENTIATE_FINAL_FAIL to print FAILED (vs. FAIL) at test end summary - for automated search for failure + + * Optimization + * - by default, line numbers are stored in unsigned shorts. Define UNITY_LINE_TYPE with a different type if your files are huge + * - by default, test and failure counters are unsigned shorts. Define UNITY_COUNTER_TYPE with a different type if you want to save space or have more than 65535 Tests. + + * Test Cases + * - define UNITY_SUPPORT_TEST_CASES to include the TEST_CASE macro, though really it's mostly about the runner generator script + + * Parameterized Tests + * - you'll want to create a define of TEST_CASE(...) which basically evaluates to nothing + + * Tests with Arguments + * - you'll want to define UNITY_USE_COMMAND_LINE_ARGS if you have the test runner passing arguments to Unity + + *------------------------------------------------------- + * Basic Fail and Ignore + *-------------------------------------------------------*/ + +#define TEST_FAIL_MESSAGE(message) UNITY_TEST_FAIL(__LINE__, (message)) +#define TEST_FAIL() UNITY_TEST_FAIL(__LINE__, NULL) +#define TEST_IGNORE_MESSAGE(message) UNITY_TEST_IGNORE(__LINE__, (message)) +#define TEST_IGNORE() UNITY_TEST_IGNORE(__LINE__, NULL) +#define TEST_MESSAGE(message) UnityMessage((message), __LINE__) +#define TEST_ONLY() +#ifdef UNITY_INCLUDE_PRINT_FORMATTED +#define TEST_PRINTF(message, ...) UnityPrintF(__LINE__, (message), __VA_ARGS__) +#endif + +/* It is not necessary for you to call PASS. A PASS condition is assumed if nothing fails. + * This method allows you to abort a test immediately with a PASS state, ignoring the remainder of the test. */ +#define TEST_PASS() TEST_ABORT() +#define TEST_PASS_MESSAGE(message) do { UnityMessage((message), __LINE__); TEST_ABORT(); } while (0) + +/* This macro does nothing, but it is useful for build tools (like Ceedling) to make use of this to figure out + * which files should be linked to in order to perform a test. Use it like TEST_FILE("sandwiches.c") */ +#define TEST_FILE(a) + +/*------------------------------------------------------- + * Test Asserts (simple) + *-------------------------------------------------------*/ + +/* Boolean */ +#define TEST_ASSERT(condition) UNITY_TEST_ASSERT( (condition), __LINE__, " Expression Evaluated To FALSE") +#define TEST_ASSERT_TRUE(condition) UNITY_TEST_ASSERT( (condition), __LINE__, " Expected TRUE Was FALSE") +#define TEST_ASSERT_UNLESS(condition) UNITY_TEST_ASSERT( !(condition), __LINE__, " Expression Evaluated To TRUE") +#define TEST_ASSERT_FALSE(condition) UNITY_TEST_ASSERT( !(condition), __LINE__, " Expected FALSE Was TRUE") +#define TEST_ASSERT_NULL(pointer) UNITY_TEST_ASSERT_NULL( (pointer), __LINE__, " Expected NULL") +#define TEST_ASSERT_NOT_NULL(pointer) UNITY_TEST_ASSERT_NOT_NULL((pointer), __LINE__, " Expected Non-NULL") +#define TEST_ASSERT_EMPTY(pointer) UNITY_TEST_ASSERT_EMPTY( (pointer), __LINE__, " Expected Empty") +#define TEST_ASSERT_NOT_EMPTY(pointer) UNITY_TEST_ASSERT_NOT_EMPTY((pointer), __LINE__, " Expected Non-Empty") + +/* Integers (of all sizes) */ +#define TEST_ASSERT_EQUAL_INT(expected, actual) UNITY_TEST_ASSERT_EQUAL_INT((expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_INT8(expected, actual) UNITY_TEST_ASSERT_EQUAL_INT8((expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_INT16(expected, actual) UNITY_TEST_ASSERT_EQUAL_INT16((expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_INT32(expected, actual) UNITY_TEST_ASSERT_EQUAL_INT32((expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_INT64(expected, actual) UNITY_TEST_ASSERT_EQUAL_INT64((expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_UINT(expected, actual) UNITY_TEST_ASSERT_EQUAL_UINT( (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_UINT8(expected, actual) UNITY_TEST_ASSERT_EQUAL_UINT8( (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_UINT16(expected, actual) UNITY_TEST_ASSERT_EQUAL_UINT16( (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_UINT32(expected, actual) UNITY_TEST_ASSERT_EQUAL_UINT32( (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_UINT64(expected, actual) UNITY_TEST_ASSERT_EQUAL_UINT64( (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_size_t(expected, actual) UNITY_TEST_ASSERT_EQUAL_UINT((expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_HEX(expected, actual) UNITY_TEST_ASSERT_EQUAL_HEX32((expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_HEX8(expected, actual) UNITY_TEST_ASSERT_EQUAL_HEX8( (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_HEX16(expected, actual) UNITY_TEST_ASSERT_EQUAL_HEX16((expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_HEX32(expected, actual) UNITY_TEST_ASSERT_EQUAL_HEX32((expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_HEX64(expected, actual) UNITY_TEST_ASSERT_EQUAL_HEX64((expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_CHAR(expected, actual) UNITY_TEST_ASSERT_EQUAL_CHAR((expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_BITS(mask, expected, actual) UNITY_TEST_ASSERT_BITS((mask), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_BITS_HIGH(mask, actual) UNITY_TEST_ASSERT_BITS((mask), (UNITY_UINT)(-1), (actual), __LINE__, NULL) +#define TEST_ASSERT_BITS_LOW(mask, actual) UNITY_TEST_ASSERT_BITS((mask), (UNITY_UINT)(0), (actual), __LINE__, NULL) +#define TEST_ASSERT_BIT_HIGH(bit, actual) UNITY_TEST_ASSERT_BITS(((UNITY_UINT)1 << (bit)), (UNITY_UINT)(-1), (actual), __LINE__, NULL) +#define TEST_ASSERT_BIT_LOW(bit, actual) UNITY_TEST_ASSERT_BITS(((UNITY_UINT)1 << (bit)), (UNITY_UINT)(0), (actual), __LINE__, NULL) + +/* Integer Not Equal To (of all sizes) */ +#define TEST_ASSERT_NOT_EQUAL_INT(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_INT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL_INT8(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_INT8((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL_INT16(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_INT16((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL_INT32(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_INT32((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL_INT64(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_INT64((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL_UINT(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_UINT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL_UINT8(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_UINT8((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL_UINT16(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_UINT16((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL_UINT32(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_UINT32((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL_UINT64(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_UINT64((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL_size_t(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_UINT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL_HEX8(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_HEX8((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL_HEX16(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_HEX16((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL_HEX32(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_HEX32((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL_HEX64(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_HEX64((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL_CHAR(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_CHAR((threshold), (actual), __LINE__, NULL) + +/* Integer Greater Than/ Less Than (of all sizes) */ +#define TEST_ASSERT_GREATER_THAN(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_INT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_THAN_INT(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_INT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_THAN_INT8(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_INT8((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_THAN_INT16(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_INT16((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_THAN_INT32(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_INT32((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_THAN_INT64(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_INT64((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_THAN_UINT(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_UINT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_THAN_UINT8(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_UINT8((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_THAN_UINT16(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_UINT16((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_THAN_UINT32(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_UINT32((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_THAN_UINT64(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_UINT64((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_THAN_size_t(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_UINT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_THAN_HEX8(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_HEX8((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_THAN_HEX16(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_HEX16((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_THAN_HEX32(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_HEX32((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_THAN_HEX64(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_HEX64((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_THAN_CHAR(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_CHAR((threshold), (actual), __LINE__, NULL) + +#define TEST_ASSERT_LESS_THAN(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_INT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_THAN_INT(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_INT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_THAN_INT8(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_INT8((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_THAN_INT16(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_INT16((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_THAN_INT32(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_INT32((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_THAN_INT64(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_INT64((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_THAN_UINT(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_UINT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_THAN_UINT8(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_UINT8((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_THAN_UINT16(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_UINT16((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_THAN_UINT32(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_UINT32((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_THAN_UINT64(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_UINT64((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_THAN_size_t(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_UINT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_THAN_HEX8(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_HEX8((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_THAN_HEX16(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_HEX16((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_THAN_HEX32(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_HEX32((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_THAN_HEX64(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_HEX64((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_THAN_CHAR(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_CHAR((threshold), (actual), __LINE__, NULL) + +#define TEST_ASSERT_GREATER_OR_EQUAL(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_OR_EQUAL_INT(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_OR_EQUAL_INT8(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT8((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_OR_EQUAL_INT16(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT16((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_OR_EQUAL_INT32(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT32((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_OR_EQUAL_INT64(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT64((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_OR_EQUAL_UINT(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_OR_EQUAL_UINT8(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT8((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_OR_EQUAL_UINT16(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT16((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_OR_EQUAL_UINT32(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT32((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_OR_EQUAL_UINT64(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT64((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_OR_EQUAL_size_t(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_OR_EQUAL_HEX8(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX8((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_OR_EQUAL_HEX16(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX16((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_OR_EQUAL_HEX32(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX32((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_OR_EQUAL_HEX64(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX64((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_GREATER_OR_EQUAL_CHAR(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_CHAR((threshold), (actual), __LINE__, NULL) + +#define TEST_ASSERT_LESS_OR_EQUAL(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_OR_EQUAL_INT(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_OR_EQUAL_INT8(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT8((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_OR_EQUAL_INT16(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT16((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_OR_EQUAL_INT32(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT32((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_OR_EQUAL_INT64(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT64((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_OR_EQUAL_UINT(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_OR_EQUAL_UINT8(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT8((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_OR_EQUAL_UINT16(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT16((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_OR_EQUAL_UINT32(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT32((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_OR_EQUAL_UINT64(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT64((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_OR_EQUAL_size_t(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_OR_EQUAL_HEX8(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX8((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_OR_EQUAL_HEX16(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX16((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_OR_EQUAL_HEX32(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX32((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_OR_EQUAL_HEX64(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX64((threshold), (actual), __LINE__, NULL) +#define TEST_ASSERT_LESS_OR_EQUAL_CHAR(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_CHAR((threshold), (actual), __LINE__, NULL) + +/* Integer Ranges (of all sizes) */ +#define TEST_ASSERT_INT_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_INT_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_INT8_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_INT8_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_INT16_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_INT16_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_INT32_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_INT32_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_INT64_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_INT64_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_UINT_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_UINT_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_UINT8_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_UINT8_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_UINT16_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_UINT16_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_UINT32_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_UINT32_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_UINT64_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_UINT64_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_size_t_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_UINT_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_HEX_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_HEX32_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_HEX8_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_HEX8_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_HEX16_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_HEX16_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_HEX32_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_HEX32_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_HEX64_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_HEX64_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_CHAR_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_CHAR_WITHIN((delta), (expected), (actual), __LINE__, NULL) + +/* Integer Array Ranges (of all sizes) */ +#define TEST_ASSERT_INT_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_INT_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL) +#define TEST_ASSERT_INT8_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_INT8_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL) +#define TEST_ASSERT_INT16_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_INT16_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL) +#define TEST_ASSERT_INT32_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_INT32_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL) +#define TEST_ASSERT_INT64_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_INT64_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL) +#define TEST_ASSERT_UINT_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_UINT_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL) +#define TEST_ASSERT_UINT8_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_UINT8_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL) +#define TEST_ASSERT_UINT16_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_UINT16_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL) +#define TEST_ASSERT_UINT32_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_UINT32_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL) +#define TEST_ASSERT_UINT64_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_UINT64_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL) +#define TEST_ASSERT_size_t_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_UINT_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL) +#define TEST_ASSERT_HEX_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_HEX32_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL) +#define TEST_ASSERT_HEX8_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_HEX8_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL) +#define TEST_ASSERT_HEX16_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_HEX16_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL) +#define TEST_ASSERT_HEX32_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_HEX32_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL) +#define TEST_ASSERT_HEX64_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_HEX64_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL) +#define TEST_ASSERT_CHAR_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_CHAR_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL) + + +/* Structs and Strings */ +#define TEST_ASSERT_EQUAL_PTR(expected, actual) UNITY_TEST_ASSERT_EQUAL_PTR((expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_STRING(expected, actual) UNITY_TEST_ASSERT_EQUAL_STRING((expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_STRING_LEN(expected, actual, len) UNITY_TEST_ASSERT_EQUAL_STRING_LEN((expected), (actual), (len), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_MEMORY(expected, actual, len) UNITY_TEST_ASSERT_EQUAL_MEMORY((expected), (actual), (len), __LINE__, NULL) + +/* Arrays */ +#define TEST_ASSERT_EQUAL_INT_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_INT_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_INT8_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_INT8_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_INT16_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_INT16_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_INT32_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_INT32_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_INT64_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_INT64_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_UINT_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_UINT_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_UINT8_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_UINT16_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_UINT16_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_UINT32_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_UINT32_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_UINT64_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_UINT64_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_size_t_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_UINT_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_HEX_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_HEX32_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_HEX8_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_HEX16_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_HEX16_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_HEX32_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_HEX32_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_HEX64_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_HEX64_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_PTR_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_PTR_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_STRING_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_STRING_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_MEMORY_ARRAY(expected, actual, len, num_elements) UNITY_TEST_ASSERT_EQUAL_MEMORY_ARRAY((expected), (actual), (len), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_CHAR_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_CHAR_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) + +/* Arrays Compared To Single Value */ +#define TEST_ASSERT_EACH_EQUAL_INT(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_INT((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_INT8(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_INT8((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_INT16(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_INT16((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_INT32(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_INT32((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_INT64(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_INT64((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_UINT(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_UINT((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_UINT8(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_UINT8((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_UINT16(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_UINT16((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_UINT32(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_UINT32((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_UINT64(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_UINT64((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_size_t(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_UINT((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_HEX(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_HEX32((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_HEX8(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_HEX8((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_HEX16(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_HEX16((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_HEX32(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_HEX32((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_HEX64(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_HEX64((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_PTR(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_PTR((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_STRING(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_STRING((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_MEMORY(expected, actual, len, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_MEMORY((expected), (actual), (len), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_CHAR(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_CHAR((expected), (actual), (num_elements), __LINE__, NULL) + +/* Floating Point (If Enabled) */ +#define TEST_ASSERT_FLOAT_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_FLOAT_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_FLOAT(expected, actual) UNITY_TEST_ASSERT_EQUAL_FLOAT((expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_FLOAT_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_FLOAT_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_FLOAT(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_FLOAT((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_FLOAT_IS_INF(actual) UNITY_TEST_ASSERT_FLOAT_IS_INF((actual), __LINE__, NULL) +#define TEST_ASSERT_FLOAT_IS_NEG_INF(actual) UNITY_TEST_ASSERT_FLOAT_IS_NEG_INF((actual), __LINE__, NULL) +#define TEST_ASSERT_FLOAT_IS_NAN(actual) UNITY_TEST_ASSERT_FLOAT_IS_NAN((actual), __LINE__, NULL) +#define TEST_ASSERT_FLOAT_IS_DETERMINATE(actual) UNITY_TEST_ASSERT_FLOAT_IS_DETERMINATE((actual), __LINE__, NULL) +#define TEST_ASSERT_FLOAT_IS_NOT_INF(actual) UNITY_TEST_ASSERT_FLOAT_IS_NOT_INF((actual), __LINE__, NULL) +#define TEST_ASSERT_FLOAT_IS_NOT_NEG_INF(actual) UNITY_TEST_ASSERT_FLOAT_IS_NOT_NEG_INF((actual), __LINE__, NULL) +#define TEST_ASSERT_FLOAT_IS_NOT_NAN(actual) UNITY_TEST_ASSERT_FLOAT_IS_NOT_NAN((actual), __LINE__, NULL) +#define TEST_ASSERT_FLOAT_IS_NOT_DETERMINATE(actual) UNITY_TEST_ASSERT_FLOAT_IS_NOT_DETERMINATE((actual), __LINE__, NULL) + +/* Double (If Enabled) */ +#define TEST_ASSERT_DOUBLE_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_DOUBLE_WITHIN((delta), (expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_DOUBLE(expected, actual) UNITY_TEST_ASSERT_EQUAL_DOUBLE((expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_EQUAL_DOUBLE_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_DOUBLE_ARRAY((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_EACH_EQUAL_DOUBLE(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_DOUBLE((expected), (actual), (num_elements), __LINE__, NULL) +#define TEST_ASSERT_DOUBLE_IS_INF(actual) UNITY_TEST_ASSERT_DOUBLE_IS_INF((actual), __LINE__, NULL) +#define TEST_ASSERT_DOUBLE_IS_NEG_INF(actual) UNITY_TEST_ASSERT_DOUBLE_IS_NEG_INF((actual), __LINE__, NULL) +#define TEST_ASSERT_DOUBLE_IS_NAN(actual) UNITY_TEST_ASSERT_DOUBLE_IS_NAN((actual), __LINE__, NULL) +#define TEST_ASSERT_DOUBLE_IS_DETERMINATE(actual) UNITY_TEST_ASSERT_DOUBLE_IS_DETERMINATE((actual), __LINE__, NULL) +#define TEST_ASSERT_DOUBLE_IS_NOT_INF(actual) UNITY_TEST_ASSERT_DOUBLE_IS_NOT_INF((actual), __LINE__, NULL) +#define TEST_ASSERT_DOUBLE_IS_NOT_NEG_INF(actual) UNITY_TEST_ASSERT_DOUBLE_IS_NOT_NEG_INF((actual), __LINE__, NULL) +#define TEST_ASSERT_DOUBLE_IS_NOT_NAN(actual) UNITY_TEST_ASSERT_DOUBLE_IS_NOT_NAN((actual), __LINE__, NULL) +#define TEST_ASSERT_DOUBLE_IS_NOT_DETERMINATE(actual) UNITY_TEST_ASSERT_DOUBLE_IS_NOT_DETERMINATE((actual), __LINE__, NULL) + +/* Shorthand */ +#ifdef UNITY_SHORTHAND_AS_OLD +#define TEST_ASSERT_EQUAL(expected, actual) UNITY_TEST_ASSERT_EQUAL_INT((expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL(expected, actual) UNITY_TEST_ASSERT(((expected) != (actual)), __LINE__, " Expected Not-Equal") +#endif +#ifdef UNITY_SHORTHAND_AS_INT +#define TEST_ASSERT_EQUAL(expected, actual) UNITY_TEST_ASSERT_EQUAL_INT((expected), (actual), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL(expected, actual) UNITY_TEST_FAIL(__LINE__, UnityStrErrShorthand) +#endif +#ifdef UNITY_SHORTHAND_AS_MEM +#define TEST_ASSERT_EQUAL(expected, actual) UNITY_TEST_ASSERT_EQUAL_MEMORY((&expected), (&actual), sizeof(expected), __LINE__, NULL) +#define TEST_ASSERT_NOT_EQUAL(expected, actual) UNITY_TEST_FAIL(__LINE__, UnityStrErrShorthand) +#endif +#ifdef UNITY_SHORTHAND_AS_RAW +#define TEST_ASSERT_EQUAL(expected, actual) UNITY_TEST_ASSERT(((expected) == (actual)), __LINE__, " Expected Equal") +#define TEST_ASSERT_NOT_EQUAL(expected, actual) UNITY_TEST_ASSERT(((expected) != (actual)), __LINE__, " Expected Not-Equal") +#endif +#ifdef UNITY_SHORTHAND_AS_NONE +#define TEST_ASSERT_EQUAL(expected, actual) UNITY_TEST_FAIL(__LINE__, UnityStrErrShorthand) +#define TEST_ASSERT_NOT_EQUAL(expected, actual) UNITY_TEST_FAIL(__LINE__, UnityStrErrShorthand) +#endif + +/*------------------------------------------------------- + * Test Asserts (with additional messages) + *-------------------------------------------------------*/ + +/* Boolean */ +#define TEST_ASSERT_MESSAGE(condition, message) UNITY_TEST_ASSERT( (condition), __LINE__, (message)) +#define TEST_ASSERT_TRUE_MESSAGE(condition, message) UNITY_TEST_ASSERT( (condition), __LINE__, (message)) +#define TEST_ASSERT_UNLESS_MESSAGE(condition, message) UNITY_TEST_ASSERT( !(condition), __LINE__, (message)) +#define TEST_ASSERT_FALSE_MESSAGE(condition, message) UNITY_TEST_ASSERT( !(condition), __LINE__, (message)) +#define TEST_ASSERT_NULL_MESSAGE(pointer, message) UNITY_TEST_ASSERT_NULL( (pointer), __LINE__, (message)) +#define TEST_ASSERT_NOT_NULL_MESSAGE(pointer, message) UNITY_TEST_ASSERT_NOT_NULL((pointer), __LINE__, (message)) +#define TEST_ASSERT_EMPTY_MESSAGE(pointer, message) UNITY_TEST_ASSERT_EMPTY( (pointer), __LINE__, (message)) +#define TEST_ASSERT_NOT_EMPTY_MESSAGE(pointer, message) UNITY_TEST_ASSERT_NOT_EMPTY((pointer), __LINE__, (message)) + +/* Integers (of all sizes) */ +#define TEST_ASSERT_EQUAL_INT_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_INT((expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_INT8_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_INT8((expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_INT16_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_INT16((expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_INT32_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_INT32((expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_INT64_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_INT64((expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_UINT_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_UINT( (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_UINT8_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_UINT8( (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_UINT16_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_UINT16( (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_UINT32_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_UINT32( (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_UINT64_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_UINT64( (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_size_t_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_UINT( (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_HEX_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_HEX32((expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_HEX8_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_HEX8( (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_HEX16_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_HEX16((expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_HEX32_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_HEX32((expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_HEX64_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_HEX64((expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_BITS_MESSAGE(mask, expected, actual, message) UNITY_TEST_ASSERT_BITS((mask), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_BITS_HIGH_MESSAGE(mask, actual, message) UNITY_TEST_ASSERT_BITS((mask), (UNITY_UINT32)(-1), (actual), __LINE__, (message)) +#define TEST_ASSERT_BITS_LOW_MESSAGE(mask, actual, message) UNITY_TEST_ASSERT_BITS((mask), (UNITY_UINT32)(0), (actual), __LINE__, (message)) +#define TEST_ASSERT_BIT_HIGH_MESSAGE(bit, actual, message) UNITY_TEST_ASSERT_BITS(((UNITY_UINT32)1 << (bit)), (UNITY_UINT32)(-1), (actual), __LINE__, (message)) +#define TEST_ASSERT_BIT_LOW_MESSAGE(bit, actual, message) UNITY_TEST_ASSERT_BITS(((UNITY_UINT32)1 << (bit)), (UNITY_UINT32)(0), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_CHAR_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_CHAR((expected), (actual), __LINE__, (message)) + +/* Integer Not Equal To (of all sizes) */ +#define TEST_ASSERT_NOT_EQUAL_INT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_INT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_NOT_EQUAL_INT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_INT8((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_NOT_EQUAL_INT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_INT16((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_NOT_EQUAL_INT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_INT32((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_NOT_EQUAL_INT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_INT64((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_NOT_EQUAL_UINT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_UINT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_NOT_EQUAL_UINT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_UINT8((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_NOT_EQUAL_UINT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_UINT16((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_NOT_EQUAL_UINT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_UINT32((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_NOT_EQUAL_UINT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_UINT64((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_NOT_EQUAL_size_t_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_UINT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_NOT_EQUAL_HEX8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_HEX8((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_NOT_EQUAL_HEX16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_HEX16((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_NOT_EQUAL_HEX32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_HEX32((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_NOT_EQUAL_HEX64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_HEX64((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_NOT_EQUAL_CHAR_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_CHAR((threshold), (actual), __LINE__, (message)) + + +/* Integer Greater Than/ Less Than (of all sizes) */ +#define TEST_ASSERT_GREATER_THAN_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_INT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_THAN_INT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_INT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_THAN_INT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_INT8((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_THAN_INT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_INT16((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_THAN_INT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_INT32((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_THAN_INT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_INT64((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_THAN_UINT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_UINT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_THAN_UINT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_UINT8((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_THAN_UINT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_UINT16((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_UINT32((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_THAN_UINT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_UINT64((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_THAN_size_t_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_UINT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_THAN_HEX8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_HEX8((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_THAN_HEX16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_HEX16((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_THAN_HEX32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_HEX32((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_THAN_HEX64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_HEX64((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_THAN_CHAR_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_CHAR((threshold), (actual), __LINE__, (message)) + +#define TEST_ASSERT_LESS_THAN_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_INT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_THAN_INT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_INT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_THAN_INT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_INT8((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_THAN_INT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_INT16((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_THAN_INT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_INT32((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_THAN_INT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_INT64((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_THAN_UINT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_UINT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_THAN_UINT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_UINT8((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_THAN_UINT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_UINT16((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_THAN_UINT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_UINT32((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_THAN_UINT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_UINT64((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_THAN_size_t_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_UINT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_THAN_HEX8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_HEX8((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_THAN_HEX16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_HEX16((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_THAN_HEX32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_HEX32((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_THAN_HEX64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_HEX64((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_THAN_CHAR_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_CHAR((threshold), (actual), __LINE__, (message)) + +#define TEST_ASSERT_GREATER_OR_EQUAL_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_OR_EQUAL_INT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_OR_EQUAL_INT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT8((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_OR_EQUAL_INT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT16((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_OR_EQUAL_INT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT32((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_OR_EQUAL_INT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT64((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_OR_EQUAL_UINT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_OR_EQUAL_UINT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT8((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_OR_EQUAL_UINT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT16((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_OR_EQUAL_UINT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT32((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_OR_EQUAL_UINT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT64((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_OR_EQUAL_size_t_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_OR_EQUAL_HEX8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX8((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_OR_EQUAL_HEX16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX16((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_OR_EQUAL_HEX32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX32((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_OR_EQUAL_HEX64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX64((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_GREATER_OR_EQUAL_CHAR_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_CHAR((threshold), (actual), __LINE__, (message)) + +#define TEST_ASSERT_LESS_OR_EQUAL_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_OR_EQUAL_INT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_OR_EQUAL_INT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT8((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_OR_EQUAL_INT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT16((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_OR_EQUAL_INT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT32((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_OR_EQUAL_INT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT64((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_OR_EQUAL_UINT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_OR_EQUAL_UINT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT8((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_OR_EQUAL_UINT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT16((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_OR_EQUAL_UINT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT32((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_OR_EQUAL_UINT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT64((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_OR_EQUAL_size_t_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_OR_EQUAL_HEX8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX8((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_OR_EQUAL_HEX16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX16((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_OR_EQUAL_HEX32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX32((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_OR_EQUAL_HEX64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX64((threshold), (actual), __LINE__, (message)) +#define TEST_ASSERT_LESS_OR_EQUAL_CHAR_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_CHAR((threshold), (actual), __LINE__, (message)) + +/* Integer Ranges (of all sizes) */ +#define TEST_ASSERT_INT_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_INT_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_INT8_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_INT8_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_INT16_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_INT16_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_INT32_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_INT32_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_INT64_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_INT64_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_UINT_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_UINT_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_UINT8_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_UINT8_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_UINT16_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_UINT16_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_UINT32_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_UINT32_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_UINT64_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_UINT64_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_size_t_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_UINT_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_HEX_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_HEX32_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_HEX8_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_HEX8_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_HEX16_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_HEX16_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_HEX32_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_HEX32_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_HEX64_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_HEX64_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_CHAR_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_CHAR_WITHIN((delta), (expected), (actual), __LINE__, (message)) + +/* Integer Array Ranges (of all sizes) */ +#define TEST_ASSERT_INT_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_INT_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message)) +#define TEST_ASSERT_INT8_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_INT8_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message)) +#define TEST_ASSERT_INT16_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_INT16_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message)) +#define TEST_ASSERT_INT32_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_INT32_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message)) +#define TEST_ASSERT_INT64_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_INT64_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message)) +#define TEST_ASSERT_UINT_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_UINT_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message)) +#define TEST_ASSERT_UINT8_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_UINT8_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message)) +#define TEST_ASSERT_UINT16_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_UINT16_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message)) +#define TEST_ASSERT_UINT32_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_UINT32_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message)) +#define TEST_ASSERT_UINT64_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_UINT64_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message)) +#define TEST_ASSERT_size_t_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_UINT_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message)) +#define TEST_ASSERT_HEX_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_HEX32_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message)) +#define TEST_ASSERT_HEX8_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_HEX8_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message)) +#define TEST_ASSERT_HEX16_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_HEX16_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message)) +#define TEST_ASSERT_HEX32_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_HEX32_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message)) +#define TEST_ASSERT_HEX64_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_HEX64_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message)) +#define TEST_ASSERT_CHAR_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_CHAR_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message)) + + +/* Structs and Strings */ +#define TEST_ASSERT_EQUAL_PTR_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_PTR((expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_STRING_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_STRING((expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_STRING_LEN_MESSAGE(expected, actual, len, message) UNITY_TEST_ASSERT_EQUAL_STRING_LEN((expected), (actual), (len), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_MEMORY_MESSAGE(expected, actual, len, message) UNITY_TEST_ASSERT_EQUAL_MEMORY((expected), (actual), (len), __LINE__, (message)) + +/* Arrays */ +#define TEST_ASSERT_EQUAL_INT_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_INT_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_INT8_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_INT8_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_INT16_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_INT16_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_INT32_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_INT32_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_INT64_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_INT64_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_UINT_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_UINT_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_UINT8_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_UINT8_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_UINT16_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_UINT16_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_UINT32_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_UINT32_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_UINT64_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_UINT64_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_size_t_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_UINT_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_HEX_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_HEX32_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_HEX8_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_HEX8_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_HEX16_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_HEX16_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_HEX32_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_HEX32_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_HEX64_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_HEX64_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_PTR_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_PTR_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_STRING_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_STRING_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_MEMORY_ARRAY_MESSAGE(expected, actual, len, num_elements, message) UNITY_TEST_ASSERT_EQUAL_MEMORY_ARRAY((expected), (actual), (len), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_CHAR_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_CHAR_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) + +/* Arrays Compared To Single Value*/ +#define TEST_ASSERT_EACH_EQUAL_INT_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_INT((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_INT8_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_INT8((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_INT16_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_INT16((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_INT32_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_INT32((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_INT64_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_INT64((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_UINT_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_UINT((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_UINT8_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_UINT8((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_UINT16_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_UINT16((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_UINT32_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_UINT32((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_UINT64_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_UINT64((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_size_t_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_UINT((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_HEX_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_HEX32((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_HEX8_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_HEX8((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_HEX16_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_HEX16((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_HEX32_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_HEX32((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_HEX64_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_HEX64((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_PTR_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_PTR((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_STRING_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_STRING((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_MEMORY_MESSAGE(expected, actual, len, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_MEMORY((expected), (actual), (len), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_CHAR_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_CHAR((expected), (actual), (num_elements), __LINE__, (message)) + +/* Floating Point (If Enabled) */ +#define TEST_ASSERT_FLOAT_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_FLOAT_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_FLOAT_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_FLOAT((expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_FLOAT_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_FLOAT_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_FLOAT_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_FLOAT((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_FLOAT_IS_INF_MESSAGE(actual, message) UNITY_TEST_ASSERT_FLOAT_IS_INF((actual), __LINE__, (message)) +#define TEST_ASSERT_FLOAT_IS_NEG_INF_MESSAGE(actual, message) UNITY_TEST_ASSERT_FLOAT_IS_NEG_INF((actual), __LINE__, (message)) +#define TEST_ASSERT_FLOAT_IS_NAN_MESSAGE(actual, message) UNITY_TEST_ASSERT_FLOAT_IS_NAN((actual), __LINE__, (message)) +#define TEST_ASSERT_FLOAT_IS_DETERMINATE_MESSAGE(actual, message) UNITY_TEST_ASSERT_FLOAT_IS_DETERMINATE((actual), __LINE__, (message)) +#define TEST_ASSERT_FLOAT_IS_NOT_INF_MESSAGE(actual, message) UNITY_TEST_ASSERT_FLOAT_IS_NOT_INF((actual), __LINE__, (message)) +#define TEST_ASSERT_FLOAT_IS_NOT_NEG_INF_MESSAGE(actual, message) UNITY_TEST_ASSERT_FLOAT_IS_NOT_NEG_INF((actual), __LINE__, (message)) +#define TEST_ASSERT_FLOAT_IS_NOT_NAN_MESSAGE(actual, message) UNITY_TEST_ASSERT_FLOAT_IS_NOT_NAN((actual), __LINE__, (message)) +#define TEST_ASSERT_FLOAT_IS_NOT_DETERMINATE_MESSAGE(actual, message) UNITY_TEST_ASSERT_FLOAT_IS_NOT_DETERMINATE((actual), __LINE__, (message)) + +/* Double (If Enabled) */ +#define TEST_ASSERT_DOUBLE_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_DOUBLE_WITHIN((delta), (expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_DOUBLE_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_DOUBLE((expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_EQUAL_DOUBLE_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_DOUBLE_ARRAY((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_EACH_EQUAL_DOUBLE_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_DOUBLE((expected), (actual), (num_elements), __LINE__, (message)) +#define TEST_ASSERT_DOUBLE_IS_INF_MESSAGE(actual, message) UNITY_TEST_ASSERT_DOUBLE_IS_INF((actual), __LINE__, (message)) +#define TEST_ASSERT_DOUBLE_IS_NEG_INF_MESSAGE(actual, message) UNITY_TEST_ASSERT_DOUBLE_IS_NEG_INF((actual), __LINE__, (message)) +#define TEST_ASSERT_DOUBLE_IS_NAN_MESSAGE(actual, message) UNITY_TEST_ASSERT_DOUBLE_IS_NAN((actual), __LINE__, (message)) +#define TEST_ASSERT_DOUBLE_IS_DETERMINATE_MESSAGE(actual, message) UNITY_TEST_ASSERT_DOUBLE_IS_DETERMINATE((actual), __LINE__, (message)) +#define TEST_ASSERT_DOUBLE_IS_NOT_INF_MESSAGE(actual, message) UNITY_TEST_ASSERT_DOUBLE_IS_NOT_INF((actual), __LINE__, (message)) +#define TEST_ASSERT_DOUBLE_IS_NOT_NEG_INF_MESSAGE(actual, message) UNITY_TEST_ASSERT_DOUBLE_IS_NOT_NEG_INF((actual), __LINE__, (message)) +#define TEST_ASSERT_DOUBLE_IS_NOT_NAN_MESSAGE(actual, message) UNITY_TEST_ASSERT_DOUBLE_IS_NOT_NAN((actual), __LINE__, (message)) +#define TEST_ASSERT_DOUBLE_IS_NOT_DETERMINATE_MESSAGE(actual, message) UNITY_TEST_ASSERT_DOUBLE_IS_NOT_DETERMINATE((actual), __LINE__, (message)) + +/* Shorthand */ +#ifdef UNITY_SHORTHAND_AS_OLD +#define TEST_ASSERT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_INT((expected), (actual), __LINE__, (message)) +#define TEST_ASSERT_NOT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT(((expected) != (actual)), __LINE__, (message)) +#endif +#ifdef UNITY_SHORTHAND_AS_INT +#define TEST_ASSERT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_INT((expected), (actual), __LINE__, message) +#define TEST_ASSERT_NOT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_FAIL(__LINE__, UnityStrErrShorthand) +#endif +#ifdef UNITY_SHORTHAND_AS_MEM +#define TEST_ASSERT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_MEMORY((&expected), (&actual), sizeof(expected), __LINE__, message) +#define TEST_ASSERT_NOT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_FAIL(__LINE__, UnityStrErrShorthand) +#endif +#ifdef UNITY_SHORTHAND_AS_RAW +#define TEST_ASSERT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT(((expected) == (actual)), __LINE__, message) +#define TEST_ASSERT_NOT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT(((expected) != (actual)), __LINE__, message) +#endif +#ifdef UNITY_SHORTHAND_AS_NONE +#define TEST_ASSERT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_FAIL(__LINE__, UnityStrErrShorthand) +#define TEST_ASSERT_NOT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_FAIL(__LINE__, UnityStrErrShorthand) +#endif + +/* end of UNITY_FRAMEWORK_H */ +#ifdef __cplusplus +} +#endif +#endif diff --git a/integration-tests/src/bindings_tests/unity/unity_internals.h b/integration-tests/src/bindings_tests/unity/unity_internals.h new file mode 100644 index 000000000..d303e8fe7 --- /dev/null +++ b/integration-tests/src/bindings_tests/unity/unity_internals.h @@ -0,0 +1,1053 @@ +/* ========================================== + Unity Project - A Test Framework for C + Copyright (c) 2007-21 Mike Karlesky, Mark VanderVoord, Greg Williams + [Released under MIT License. Please refer to license.txt for details] +========================================== */ + +#ifndef UNITY_INTERNALS_H +#define UNITY_INTERNALS_H + +#ifdef UNITY_INCLUDE_CONFIG_H +#include "unity_config.h" +#endif + +#ifndef UNITY_EXCLUDE_SETJMP_H +#include +#endif + +#ifndef UNITY_EXCLUDE_MATH_H +#include +#endif + +#ifndef UNITY_EXCLUDE_STDDEF_H +#include +#endif + +#ifdef UNITY_INCLUDE_PRINT_FORMATTED +#include +#endif + +/* Unity Attempts to Auto-Detect Integer Types + * Attempt 1: UINT_MAX, ULONG_MAX in , or default to 32 bits + * Attempt 2: UINTPTR_MAX in , or default to same size as long + * The user may override any of these derived constants: + * UNITY_INT_WIDTH, UNITY_LONG_WIDTH, UNITY_POINTER_WIDTH */ +#ifndef UNITY_EXCLUDE_STDINT_H +#include +#endif + +#ifndef UNITY_EXCLUDE_LIMITS_H +#include +#endif + +#if defined(__GNUC__) || defined(__clang__) + #define UNITY_FUNCTION_ATTR(a) __attribute__((a)) +#else + #define UNITY_FUNCTION_ATTR(a) /* ignore */ +#endif + +#ifndef UNITY_NORETURN + #if defined(__cplusplus) + #if __cplusplus >= 201103L + #define UNITY_NORETURN [[ noreturn ]] + #endif + #elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L + #include + #define UNITY_NORETURN noreturn + #endif +#endif +#ifndef UNITY_NORETURN + #define UNITY_NORETURN UNITY_FUNCTION_ATTR(noreturn) +#endif + +/*------------------------------------------------------- + * Guess Widths If Not Specified + *-------------------------------------------------------*/ + +/* Determine the size of an int, if not already specified. + * We cannot use sizeof(int), because it is not yet defined + * at this stage in the translation of the C program. + * Also sizeof(int) does return the size in addressable units on all platforms, + * which may not necessarily be the size in bytes. + * Therefore, infer it from UINT_MAX if possible. */ +#ifndef UNITY_INT_WIDTH + #ifdef UINT_MAX + #if (UINT_MAX == 0xFFFF) + #define UNITY_INT_WIDTH (16) + #elif (UINT_MAX == 0xFFFFFFFF) + #define UNITY_INT_WIDTH (32) + #elif (UINT_MAX == 0xFFFFFFFFFFFFFFFF) + #define UNITY_INT_WIDTH (64) + #endif + #else /* Set to default */ + #define UNITY_INT_WIDTH (32) + #endif /* UINT_MAX */ +#endif + +/* Determine the size of a long, if not already specified. */ +#ifndef UNITY_LONG_WIDTH + #ifdef ULONG_MAX + #if (ULONG_MAX == 0xFFFF) + #define UNITY_LONG_WIDTH (16) + #elif (ULONG_MAX == 0xFFFFFFFF) + #define UNITY_LONG_WIDTH (32) + #elif (ULONG_MAX == 0xFFFFFFFFFFFFFFFF) + #define UNITY_LONG_WIDTH (64) + #endif + #else /* Set to default */ + #define UNITY_LONG_WIDTH (32) + #endif /* ULONG_MAX */ +#endif + +/* Determine the size of a pointer, if not already specified. */ +#ifndef UNITY_POINTER_WIDTH + #ifdef UINTPTR_MAX + #if (UINTPTR_MAX <= 0xFFFF) + #define UNITY_POINTER_WIDTH (16) + #elif (UINTPTR_MAX <= 0xFFFFFFFF) + #define UNITY_POINTER_WIDTH (32) + #elif (UINTPTR_MAX <= 0xFFFFFFFFFFFFFFFF) + #define UNITY_POINTER_WIDTH (64) + #endif + #else /* Set to default */ + #define UNITY_POINTER_WIDTH UNITY_LONG_WIDTH + #endif /* UINTPTR_MAX */ +#endif + +/*------------------------------------------------------- + * Int Support (Define types based on detected sizes) + *-------------------------------------------------------*/ + +#if (UNITY_INT_WIDTH == 32) + typedef unsigned char UNITY_UINT8; + typedef unsigned short UNITY_UINT16; + typedef unsigned int UNITY_UINT32; + typedef signed char UNITY_INT8; + typedef signed short UNITY_INT16; + typedef signed int UNITY_INT32; +#elif (UNITY_INT_WIDTH == 16) + typedef unsigned char UNITY_UINT8; + typedef unsigned int UNITY_UINT16; + typedef unsigned long UNITY_UINT32; + typedef signed char UNITY_INT8; + typedef signed int UNITY_INT16; + typedef signed long UNITY_INT32; +#else + #error Invalid UNITY_INT_WIDTH specified! (16 or 32 are supported) +#endif + +/*------------------------------------------------------- + * 64-bit Support + *-------------------------------------------------------*/ + +/* Auto-detect 64 Bit Support */ +#ifndef UNITY_SUPPORT_64 + #if UNITY_LONG_WIDTH == 64 || UNITY_POINTER_WIDTH == 64 + #define UNITY_SUPPORT_64 + #endif +#endif + +/* 64-Bit Support Dependent Configuration */ +#ifndef UNITY_SUPPORT_64 + /* No 64-bit Support */ + typedef UNITY_UINT32 UNITY_UINT; + typedef UNITY_INT32 UNITY_INT; + #define UNITY_MAX_NIBBLES (8) /* Maximum number of nibbles in a UNITY_(U)INT */ +#else + /* 64-bit Support */ + #if (UNITY_LONG_WIDTH == 32) + typedef unsigned long long UNITY_UINT64; + typedef signed long long UNITY_INT64; + #elif (UNITY_LONG_WIDTH == 64) + typedef unsigned long UNITY_UINT64; + typedef signed long UNITY_INT64; + #else + #error Invalid UNITY_LONG_WIDTH specified! (32 or 64 are supported) + #endif + typedef UNITY_UINT64 UNITY_UINT; + typedef UNITY_INT64 UNITY_INT; + #define UNITY_MAX_NIBBLES (16) /* Maximum number of nibbles in a UNITY_(U)INT */ +#endif + +/*------------------------------------------------------- + * Pointer Support + *-------------------------------------------------------*/ + +#if (UNITY_POINTER_WIDTH == 32) + #define UNITY_PTR_TO_INT UNITY_INT32 + #define UNITY_DISPLAY_STYLE_POINTER UNITY_DISPLAY_STYLE_HEX32 +#elif (UNITY_POINTER_WIDTH == 64) + #define UNITY_PTR_TO_INT UNITY_INT64 + #define UNITY_DISPLAY_STYLE_POINTER UNITY_DISPLAY_STYLE_HEX64 +#elif (UNITY_POINTER_WIDTH == 16) + #define UNITY_PTR_TO_INT UNITY_INT16 + #define UNITY_DISPLAY_STYLE_POINTER UNITY_DISPLAY_STYLE_HEX16 +#else + #error Invalid UNITY_POINTER_WIDTH specified! (16, 32 or 64 are supported) +#endif + +#ifndef UNITY_PTR_ATTRIBUTE + #define UNITY_PTR_ATTRIBUTE +#endif + +#ifndef UNITY_INTERNAL_PTR + #define UNITY_INTERNAL_PTR UNITY_PTR_ATTRIBUTE const void* +#endif + +/*------------------------------------------------------- + * Float Support + *-------------------------------------------------------*/ + +#ifdef UNITY_EXCLUDE_FLOAT + +/* No Floating Point Support */ +#ifndef UNITY_EXCLUDE_DOUBLE +#define UNITY_EXCLUDE_DOUBLE /* Remove double when excluding float support */ +#endif +#ifndef UNITY_EXCLUDE_FLOAT_PRINT +#define UNITY_EXCLUDE_FLOAT_PRINT +#endif + +#else + +/* Floating Point Support */ +#ifndef UNITY_FLOAT_PRECISION +#define UNITY_FLOAT_PRECISION (0.00001f) +#endif +#ifndef UNITY_FLOAT_TYPE +#define UNITY_FLOAT_TYPE float +#endif +typedef UNITY_FLOAT_TYPE UNITY_FLOAT; + +/* isinf & isnan macros should be provided by math.h */ +#ifndef isinf +/* The value of Inf - Inf is NaN */ +#define isinf(n) (isnan((n) - (n)) && !isnan(n)) +#endif + +#ifndef isnan +/* NaN is the only floating point value that does NOT equal itself. + * Therefore if n != n, then it is NaN. */ +#define isnan(n) ((n != n) ? 1 : 0) +#endif + +#endif + +/*------------------------------------------------------- + * Double Float Support + *-------------------------------------------------------*/ + +/* unlike float, we DON'T include by default */ +#if defined(UNITY_EXCLUDE_DOUBLE) || !defined(UNITY_INCLUDE_DOUBLE) + + /* No Floating Point Support */ + #ifndef UNITY_EXCLUDE_DOUBLE + #define UNITY_EXCLUDE_DOUBLE + #else + #undef UNITY_INCLUDE_DOUBLE + #endif + + #ifndef UNITY_EXCLUDE_FLOAT + #ifndef UNITY_DOUBLE_TYPE + #define UNITY_DOUBLE_TYPE double + #endif + typedef UNITY_FLOAT UNITY_DOUBLE; + /* For parameter in UnityPrintFloat(UNITY_DOUBLE), which aliases to double or float */ + #endif + +#else + + /* Double Floating Point Support */ + #ifndef UNITY_DOUBLE_PRECISION + #define UNITY_DOUBLE_PRECISION (1e-12) + #endif + + #ifndef UNITY_DOUBLE_TYPE + #define UNITY_DOUBLE_TYPE double + #endif + typedef UNITY_DOUBLE_TYPE UNITY_DOUBLE; + +#endif + +/*------------------------------------------------------- + * Output Method: stdout (DEFAULT) + *-------------------------------------------------------*/ +#ifndef UNITY_OUTPUT_CHAR + /* Default to using putchar, which is defined in stdio.h */ + #include + #define UNITY_OUTPUT_CHAR(a) (void)putchar(a) +#else + /* If defined as something else, make sure we declare it here so it's ready for use */ + #ifdef UNITY_OUTPUT_CHAR_HEADER_DECLARATION + extern void UNITY_OUTPUT_CHAR_HEADER_DECLARATION; + #endif +#endif + +#ifndef UNITY_OUTPUT_FLUSH + #ifdef UNITY_USE_FLUSH_STDOUT + /* We want to use the stdout flush utility */ + #include + #define UNITY_OUTPUT_FLUSH() (void)fflush(stdout) + #else + /* We've specified nothing, therefore flush should just be ignored */ + #define UNITY_OUTPUT_FLUSH() (void)0 + #endif +#else + /* If defined as something else, make sure we declare it here so it's ready for use */ + #ifdef UNITY_OUTPUT_FLUSH_HEADER_DECLARATION + extern void UNITY_OUTPUT_FLUSH_HEADER_DECLARATION; + #endif +#endif + +#ifndef UNITY_OUTPUT_FLUSH +#define UNITY_FLUSH_CALL() +#else +#define UNITY_FLUSH_CALL() UNITY_OUTPUT_FLUSH() +#endif + +#ifndef UNITY_PRINT_EOL +#define UNITY_PRINT_EOL() UNITY_OUTPUT_CHAR('\n') +#endif + +#ifndef UNITY_OUTPUT_START +#define UNITY_OUTPUT_START() +#endif + +#ifndef UNITY_OUTPUT_COMPLETE +#define UNITY_OUTPUT_COMPLETE() +#endif + +#ifdef UNITY_INCLUDE_EXEC_TIME + #if !defined(UNITY_EXEC_TIME_START) && \ + !defined(UNITY_EXEC_TIME_STOP) && \ + !defined(UNITY_PRINT_EXEC_TIME) && \ + !defined(UNITY_TIME_TYPE) + /* If none any of these macros are defined then try to provide a default implementation */ + + #if defined(UNITY_CLOCK_MS) + /* This is a simple way to get a default implementation on platforms that support getting a millisecond counter */ + #define UNITY_TIME_TYPE UNITY_UINT + #define UNITY_EXEC_TIME_START() Unity.CurrentTestStartTime = UNITY_CLOCK_MS() + #define UNITY_EXEC_TIME_STOP() Unity.CurrentTestStopTime = UNITY_CLOCK_MS() + #define UNITY_PRINT_EXEC_TIME() { \ + UNITY_UINT execTimeMs = (Unity.CurrentTestStopTime - Unity.CurrentTestStartTime); \ + UnityPrint(" ("); \ + UnityPrintNumberUnsigned(execTimeMs); \ + UnityPrint(" ms)"); \ + } + #elif defined(_WIN32) + #include + #define UNITY_TIME_TYPE clock_t + #define UNITY_GET_TIME(t) t = (clock_t)((clock() * 1000) / CLOCKS_PER_SEC) + #define UNITY_EXEC_TIME_START() UNITY_GET_TIME(Unity.CurrentTestStartTime) + #define UNITY_EXEC_TIME_STOP() UNITY_GET_TIME(Unity.CurrentTestStopTime) + #define UNITY_PRINT_EXEC_TIME() { \ + UNITY_UINT execTimeMs = (Unity.CurrentTestStopTime - Unity.CurrentTestStartTime); \ + UnityPrint(" ("); \ + UnityPrintNumberUnsigned(execTimeMs); \ + UnityPrint(" ms)"); \ + } + #elif defined(__unix__) || defined(__APPLE__) + #include + #define UNITY_TIME_TYPE struct timespec + #define UNITY_GET_TIME(t) clock_gettime(CLOCK_MONOTONIC, &t) + #define UNITY_EXEC_TIME_START() UNITY_GET_TIME(Unity.CurrentTestStartTime) + #define UNITY_EXEC_TIME_STOP() UNITY_GET_TIME(Unity.CurrentTestStopTime) + #define UNITY_PRINT_EXEC_TIME() { \ + UNITY_UINT execTimeMs = ((Unity.CurrentTestStopTime.tv_sec - Unity.CurrentTestStartTime.tv_sec) * 1000L); \ + execTimeMs += ((Unity.CurrentTestStopTime.tv_nsec - Unity.CurrentTestStartTime.tv_nsec) / 1000000L); \ + UnityPrint(" ("); \ + UnityPrintNumberUnsigned(execTimeMs); \ + UnityPrint(" ms)"); \ + } + #endif + #endif +#endif + +#ifndef UNITY_EXEC_TIME_START +#define UNITY_EXEC_TIME_START() do { /* nothing*/ } while (0) +#endif + +#ifndef UNITY_EXEC_TIME_STOP +#define UNITY_EXEC_TIME_STOP() do { /* nothing*/ } while (0) +#endif + +#ifndef UNITY_TIME_TYPE +#define UNITY_TIME_TYPE UNITY_UINT +#endif + +#ifndef UNITY_PRINT_EXEC_TIME +#define UNITY_PRINT_EXEC_TIME() do { /* nothing*/ } while (0) +#endif + +/*------------------------------------------------------- + * Footprint + *-------------------------------------------------------*/ + +#ifndef UNITY_LINE_TYPE +#define UNITY_LINE_TYPE UNITY_UINT +#endif + +#ifndef UNITY_COUNTER_TYPE +#define UNITY_COUNTER_TYPE UNITY_UINT +#endif + +/*------------------------------------------------------- + * Internal Structs Needed + *-------------------------------------------------------*/ + +typedef void (*UnityTestFunction)(void); + +#define UNITY_DISPLAY_RANGE_INT (0x10) +#define UNITY_DISPLAY_RANGE_UINT (0x20) +#define UNITY_DISPLAY_RANGE_HEX (0x40) +#define UNITY_DISPLAY_RANGE_CHAR (0x80) + +typedef enum +{ + UNITY_DISPLAY_STYLE_INT = (UNITY_INT_WIDTH / 8) + UNITY_DISPLAY_RANGE_INT, + UNITY_DISPLAY_STYLE_INT8 = 1 + UNITY_DISPLAY_RANGE_INT, + UNITY_DISPLAY_STYLE_INT16 = 2 + UNITY_DISPLAY_RANGE_INT, + UNITY_DISPLAY_STYLE_INT32 = 4 + UNITY_DISPLAY_RANGE_INT, +#ifdef UNITY_SUPPORT_64 + UNITY_DISPLAY_STYLE_INT64 = 8 + UNITY_DISPLAY_RANGE_INT, +#endif + + UNITY_DISPLAY_STYLE_UINT = (UNITY_INT_WIDTH / 8) + UNITY_DISPLAY_RANGE_UINT, + UNITY_DISPLAY_STYLE_UINT8 = 1 + UNITY_DISPLAY_RANGE_UINT, + UNITY_DISPLAY_STYLE_UINT16 = 2 + UNITY_DISPLAY_RANGE_UINT, + UNITY_DISPLAY_STYLE_UINT32 = 4 + UNITY_DISPLAY_RANGE_UINT, +#ifdef UNITY_SUPPORT_64 + UNITY_DISPLAY_STYLE_UINT64 = 8 + UNITY_DISPLAY_RANGE_UINT, +#endif + + UNITY_DISPLAY_STYLE_HEX8 = 1 + UNITY_DISPLAY_RANGE_HEX, + UNITY_DISPLAY_STYLE_HEX16 = 2 + UNITY_DISPLAY_RANGE_HEX, + UNITY_DISPLAY_STYLE_HEX32 = 4 + UNITY_DISPLAY_RANGE_HEX, +#ifdef UNITY_SUPPORT_64 + UNITY_DISPLAY_STYLE_HEX64 = 8 + UNITY_DISPLAY_RANGE_HEX, +#endif + + UNITY_DISPLAY_STYLE_CHAR = 1 + UNITY_DISPLAY_RANGE_CHAR + UNITY_DISPLAY_RANGE_INT, + + UNITY_DISPLAY_STYLE_UNKNOWN +} UNITY_DISPLAY_STYLE_T; + +typedef enum +{ + UNITY_WITHIN = 0x0, + UNITY_EQUAL_TO = 0x1, + UNITY_GREATER_THAN = 0x2, + UNITY_GREATER_OR_EQUAL = 0x2 + UNITY_EQUAL_TO, + UNITY_SMALLER_THAN = 0x4, + UNITY_SMALLER_OR_EQUAL = 0x4 + UNITY_EQUAL_TO, + UNITY_NOT_EQUAL = 0x0, + UNITY_UNKNOWN +} UNITY_COMPARISON_T; + +#ifndef UNITY_EXCLUDE_FLOAT +typedef enum UNITY_FLOAT_TRAIT +{ + UNITY_FLOAT_IS_NOT_INF = 0, + UNITY_FLOAT_IS_INF, + UNITY_FLOAT_IS_NOT_NEG_INF, + UNITY_FLOAT_IS_NEG_INF, + UNITY_FLOAT_IS_NOT_NAN, + UNITY_FLOAT_IS_NAN, + UNITY_FLOAT_IS_NOT_DET, + UNITY_FLOAT_IS_DET, + UNITY_FLOAT_INVALID_TRAIT +} UNITY_FLOAT_TRAIT_T; +#endif + +typedef enum +{ + UNITY_ARRAY_TO_VAL = 0, + UNITY_ARRAY_TO_ARRAY, + UNITY_ARRAY_UNKNOWN +} UNITY_FLAGS_T; + +struct UNITY_STORAGE_T +{ + const char* TestFile; + const char* CurrentTestName; +#ifndef UNITY_EXCLUDE_DETAILS + const char* CurrentDetail1; + const char* CurrentDetail2; +#endif + UNITY_LINE_TYPE CurrentTestLineNumber; + UNITY_COUNTER_TYPE NumberOfTests; + UNITY_COUNTER_TYPE TestFailures; + UNITY_COUNTER_TYPE TestIgnores; + UNITY_COUNTER_TYPE CurrentTestFailed; + UNITY_COUNTER_TYPE CurrentTestIgnored; +#ifdef UNITY_INCLUDE_EXEC_TIME + UNITY_TIME_TYPE CurrentTestStartTime; + UNITY_TIME_TYPE CurrentTestStopTime; +#endif +#ifndef UNITY_EXCLUDE_SETJMP_H + jmp_buf AbortFrame; +#endif +}; + +extern struct UNITY_STORAGE_T Unity; + +/*------------------------------------------------------- + * Test Suite Management + *-------------------------------------------------------*/ + +void UnityBegin(const char* filename); +int UnityEnd(void); +void UnitySetTestFile(const char* filename); +void UnityConcludeTest(void); + +#ifndef RUN_TEST +void UnityDefaultTestRun(UnityTestFunction Func, const char* FuncName, const int FuncLineNum); +#else +#define UNITY_SKIP_DEFAULT_RUNNER +#endif + +/*------------------------------------------------------- + * Details Support + *-------------------------------------------------------*/ + +#ifdef UNITY_EXCLUDE_DETAILS +#define UNITY_CLR_DETAILS() +#define UNITY_SET_DETAIL(d1) +#define UNITY_SET_DETAILS(d1,d2) +#else +#define UNITY_CLR_DETAILS() do { Unity.CurrentDetail1 = 0; Unity.CurrentDetail2 = 0; } while (0) +#define UNITY_SET_DETAIL(d1) do { Unity.CurrentDetail1 = (d1); Unity.CurrentDetail2 = 0; } while (0) +#define UNITY_SET_DETAILS(d1,d2) do { Unity.CurrentDetail1 = (d1); Unity.CurrentDetail2 = (d2); } while (0) + +#ifndef UNITY_DETAIL1_NAME +#define UNITY_DETAIL1_NAME "Function" +#endif + +#ifndef UNITY_DETAIL2_NAME +#define UNITY_DETAIL2_NAME "Argument" +#endif +#endif + +#ifdef UNITY_PRINT_TEST_CONTEXT +void UNITY_PRINT_TEST_CONTEXT(void); +#endif + +/*------------------------------------------------------- + * Test Output + *-------------------------------------------------------*/ + +void UnityPrint(const char* string); + +#ifdef UNITY_INCLUDE_PRINT_FORMATTED +void UnityPrintF(const UNITY_LINE_TYPE line, const char* format, ...); +#endif + +void UnityPrintLen(const char* string, const UNITY_UINT32 length); +void UnityPrintMask(const UNITY_UINT mask, const UNITY_UINT number); +void UnityPrintNumberByStyle(const UNITY_INT number, const UNITY_DISPLAY_STYLE_T style); +void UnityPrintNumber(const UNITY_INT number_to_print); +void UnityPrintNumberUnsigned(const UNITY_UINT number); +void UnityPrintNumberHex(const UNITY_UINT number, const char nibbles_to_print); + +#ifndef UNITY_EXCLUDE_FLOAT_PRINT +void UnityPrintFloat(const UNITY_DOUBLE input_number); +#endif + +/*------------------------------------------------------- + * Test Assertion Functions + *------------------------------------------------------- + * Use the macros below this section instead of calling + * these directly. The macros have a consistent naming + * convention and will pull in file and line information + * for you. */ + +void UnityAssertEqualNumber(const UNITY_INT expected, + const UNITY_INT actual, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_DISPLAY_STYLE_T style); + +void UnityAssertGreaterOrLessOrEqualNumber(const UNITY_INT threshold, + const UNITY_INT actual, + const UNITY_COMPARISON_T compare, + const char *msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_DISPLAY_STYLE_T style); + +void UnityAssertEqualIntArray(UNITY_INTERNAL_PTR expected, + UNITY_INTERNAL_PTR actual, + const UNITY_UINT32 num_elements, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_DISPLAY_STYLE_T style, + const UNITY_FLAGS_T flags); + +void UnityAssertBits(const UNITY_INT mask, + const UNITY_INT expected, + const UNITY_INT actual, + const char* msg, + const UNITY_LINE_TYPE lineNumber); + +void UnityAssertEqualString(const char* expected, + const char* actual, + const char* msg, + const UNITY_LINE_TYPE lineNumber); + +void UnityAssertEqualStringLen(const char* expected, + const char* actual, + const UNITY_UINT32 length, + const char* msg, + const UNITY_LINE_TYPE lineNumber); + +void UnityAssertEqualStringArray( UNITY_INTERNAL_PTR expected, + const char** actual, + const UNITY_UINT32 num_elements, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_FLAGS_T flags); + +void UnityAssertEqualMemory( UNITY_INTERNAL_PTR expected, + UNITY_INTERNAL_PTR actual, + const UNITY_UINT32 length, + const UNITY_UINT32 num_elements, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_FLAGS_T flags); + +void UnityAssertNumbersWithin(const UNITY_UINT delta, + const UNITY_INT expected, + const UNITY_INT actual, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_DISPLAY_STYLE_T style); + +void UnityAssertNumbersArrayWithin(const UNITY_UINT delta, + UNITY_INTERNAL_PTR expected, + UNITY_INTERNAL_PTR actual, + const UNITY_UINT32 num_elements, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_DISPLAY_STYLE_T style, + const UNITY_FLAGS_T flags); + +#ifndef UNITY_EXCLUDE_SETJMP_H +UNITY_NORETURN void UnityFail(const char* message, const UNITY_LINE_TYPE line); +UNITY_NORETURN void UnityIgnore(const char* message, const UNITY_LINE_TYPE line); +#else +void UnityFail(const char* message, const UNITY_LINE_TYPE line); +void UnityIgnore(const char* message, const UNITY_LINE_TYPE line); +#endif + +void UnityMessage(const char* message, const UNITY_LINE_TYPE line); + +#ifndef UNITY_EXCLUDE_FLOAT +void UnityAssertFloatsWithin(const UNITY_FLOAT delta, + const UNITY_FLOAT expected, + const UNITY_FLOAT actual, + const char* msg, + const UNITY_LINE_TYPE lineNumber); + +void UnityAssertEqualFloatArray(UNITY_PTR_ATTRIBUTE const UNITY_FLOAT* expected, + UNITY_PTR_ATTRIBUTE const UNITY_FLOAT* actual, + const UNITY_UINT32 num_elements, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_FLAGS_T flags); + +void UnityAssertFloatSpecial(const UNITY_FLOAT actual, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_FLOAT_TRAIT_T style); +#endif + +#ifndef UNITY_EXCLUDE_DOUBLE +void UnityAssertDoublesWithin(const UNITY_DOUBLE delta, + const UNITY_DOUBLE expected, + const UNITY_DOUBLE actual, + const char* msg, + const UNITY_LINE_TYPE lineNumber); + +void UnityAssertEqualDoubleArray(UNITY_PTR_ATTRIBUTE const UNITY_DOUBLE* expected, + UNITY_PTR_ATTRIBUTE const UNITY_DOUBLE* actual, + const UNITY_UINT32 num_elements, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_FLAGS_T flags); + +void UnityAssertDoubleSpecial(const UNITY_DOUBLE actual, + const char* msg, + const UNITY_LINE_TYPE lineNumber, + const UNITY_FLOAT_TRAIT_T style); +#endif + +/*------------------------------------------------------- + * Helpers + *-------------------------------------------------------*/ + +UNITY_INTERNAL_PTR UnityNumToPtr(const UNITY_INT num, const UNITY_UINT8 size); +#ifndef UNITY_EXCLUDE_FLOAT +UNITY_INTERNAL_PTR UnityFloatToPtr(const float num); +#endif +#ifndef UNITY_EXCLUDE_DOUBLE +UNITY_INTERNAL_PTR UnityDoubleToPtr(const double num); +#endif + +/*------------------------------------------------------- + * Error Strings We Might Need + *-------------------------------------------------------*/ + +extern const char UnityStrOk[]; +extern const char UnityStrPass[]; +extern const char UnityStrFail[]; +extern const char UnityStrIgnore[]; + +extern const char UnityStrErrFloat[]; +extern const char UnityStrErrDouble[]; +extern const char UnityStrErr64[]; +extern const char UnityStrErrShorthand[]; + +/*------------------------------------------------------- + * Test Running Macros + *-------------------------------------------------------*/ + +#ifndef UNITY_EXCLUDE_SETJMP_H +#define TEST_PROTECT() (setjmp(Unity.AbortFrame) == 0) +#define TEST_ABORT() longjmp(Unity.AbortFrame, 1) +#else +#define TEST_PROTECT() 1 +#define TEST_ABORT() return +#endif + +/* This tricky series of macros gives us an optional line argument to treat it as RUN_TEST(func, num=__LINE__) */ +#ifndef RUN_TEST +#ifdef __STDC_VERSION__ +#if __STDC_VERSION__ >= 199901L +#define UNITY_SUPPORT_VARIADIC_MACROS +#endif +#endif +#ifdef UNITY_SUPPORT_VARIADIC_MACROS +#define RUN_TEST(...) RUN_TEST_AT_LINE(__VA_ARGS__, __LINE__, throwaway) +#define RUN_TEST_AT_LINE(func, line, ...) UnityDefaultTestRun(func, #func, line) +#endif +#endif + +/* If we can't do the tricky version, we'll just have to require them to always include the line number */ +#ifndef RUN_TEST +#ifdef CMOCK +#define RUN_TEST(func, num) UnityDefaultTestRun(func, #func, num) +#else +#define RUN_TEST(func) UnityDefaultTestRun(func, #func, __LINE__) +#endif +#endif + +#define TEST_LINE_NUM (Unity.CurrentTestLineNumber) +#define TEST_IS_IGNORED (Unity.CurrentTestIgnored) +#define UNITY_NEW_TEST(a) \ + Unity.CurrentTestName = (a); \ + Unity.CurrentTestLineNumber = (UNITY_LINE_TYPE)(__LINE__); \ + Unity.NumberOfTests++; + +#ifndef UNITY_BEGIN +#define UNITY_BEGIN() UnityBegin(__FILE__) +#endif + +#ifndef UNITY_END +#define UNITY_END() UnityEnd() +#endif + +#ifndef UNITY_SHORTHAND_AS_INT +#ifndef UNITY_SHORTHAND_AS_MEM +#ifndef UNITY_SHORTHAND_AS_NONE +#ifndef UNITY_SHORTHAND_AS_RAW +#define UNITY_SHORTHAND_AS_OLD +#endif +#endif +#endif +#endif + +/*----------------------------------------------- + * Command Line Argument Support + *-----------------------------------------------*/ + +#ifdef UNITY_USE_COMMAND_LINE_ARGS +int UnityParseOptions(int argc, char** argv); +int UnityTestMatches(void); +#endif + +/*------------------------------------------------------- + * Basic Fail and Ignore + *-------------------------------------------------------*/ + +#define UNITY_TEST_FAIL(line, message) UnityFail( (message), (UNITY_LINE_TYPE)(line)) +#define UNITY_TEST_IGNORE(line, message) UnityIgnore( (message), (UNITY_LINE_TYPE)(line)) + +/*------------------------------------------------------- + * Test Asserts + *-------------------------------------------------------*/ + +#define UNITY_TEST_ASSERT(condition, line, message) do { if (condition) { /* nothing*/ } else { UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), (message)); } } while (0) +#define UNITY_TEST_ASSERT_NULL(pointer, line, message) UNITY_TEST_ASSERT(((pointer) == NULL), (UNITY_LINE_TYPE)(line), (message)) +#define UNITY_TEST_ASSERT_NOT_NULL(pointer, line, message) UNITY_TEST_ASSERT(((pointer) != NULL), (UNITY_LINE_TYPE)(line), (message)) +#define UNITY_TEST_ASSERT_EMPTY(pointer, line, message) UNITY_TEST_ASSERT(((pointer[0]) == 0), (UNITY_LINE_TYPE)(line), (message)) +#define UNITY_TEST_ASSERT_NOT_EMPTY(pointer, line, message) UNITY_TEST_ASSERT(((pointer[0]) != 0), (UNITY_LINE_TYPE)(line), (message)) + +#define UNITY_TEST_ASSERT_EQUAL_INT(expected, actual, line, message) UnityAssertEqualNumber((UNITY_INT)(expected), (UNITY_INT)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT) +#define UNITY_TEST_ASSERT_EQUAL_INT8(expected, actual, line, message) UnityAssertEqualNumber((UNITY_INT)(UNITY_INT8 )(expected), (UNITY_INT)(UNITY_INT8 )(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT8) +#define UNITY_TEST_ASSERT_EQUAL_INT16(expected, actual, line, message) UnityAssertEqualNumber((UNITY_INT)(UNITY_INT16)(expected), (UNITY_INT)(UNITY_INT16)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT16) +#define UNITY_TEST_ASSERT_EQUAL_INT32(expected, actual, line, message) UnityAssertEqualNumber((UNITY_INT)(UNITY_INT32)(expected), (UNITY_INT)(UNITY_INT32)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT32) +#define UNITY_TEST_ASSERT_EQUAL_UINT(expected, actual, line, message) UnityAssertEqualNumber((UNITY_INT)(expected), (UNITY_INT)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT) +#define UNITY_TEST_ASSERT_EQUAL_UINT8(expected, actual, line, message) UnityAssertEqualNumber((UNITY_INT)(UNITY_UINT8 )(expected), (UNITY_INT)(UNITY_UINT8 )(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT8) +#define UNITY_TEST_ASSERT_EQUAL_UINT16(expected, actual, line, message) UnityAssertEqualNumber((UNITY_INT)(UNITY_UINT16)(expected), (UNITY_INT)(UNITY_UINT16)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT16) +#define UNITY_TEST_ASSERT_EQUAL_UINT32(expected, actual, line, message) UnityAssertEqualNumber((UNITY_INT)(UNITY_UINT32)(expected), (UNITY_INT)(UNITY_UINT32)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT32) +#define UNITY_TEST_ASSERT_EQUAL_HEX8(expected, actual, line, message) UnityAssertEqualNumber((UNITY_INT)(UNITY_INT8 )(expected), (UNITY_INT)(UNITY_INT8 )(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX8) +#define UNITY_TEST_ASSERT_EQUAL_HEX16(expected, actual, line, message) UnityAssertEqualNumber((UNITY_INT)(UNITY_INT16)(expected), (UNITY_INT)(UNITY_INT16)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX16) +#define UNITY_TEST_ASSERT_EQUAL_HEX32(expected, actual, line, message) UnityAssertEqualNumber((UNITY_INT)(UNITY_INT32)(expected), (UNITY_INT)(UNITY_INT32)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX32) +#define UNITY_TEST_ASSERT_EQUAL_CHAR(expected, actual, line, message) UnityAssertEqualNumber((UNITY_INT)(UNITY_INT8 )(expected), (UNITY_INT)(UNITY_INT8 )(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_CHAR) +#define UNITY_TEST_ASSERT_BITS(mask, expected, actual, line, message) UnityAssertBits((UNITY_INT)(mask), (UNITY_INT)(expected), (UNITY_INT)(actual), (message), (UNITY_LINE_TYPE)(line)) + +#define UNITY_TEST_ASSERT_NOT_EQUAL_INT(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_NOT_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT) +#define UNITY_TEST_ASSERT_NOT_EQUAL_INT8(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT8 )(threshold), (UNITY_INT)(UNITY_INT8 )(actual), UNITY_NOT_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT8) +#define UNITY_TEST_ASSERT_NOT_EQUAL_INT16(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT16)(threshold), (UNITY_INT)(UNITY_INT16)(actual), UNITY_NOT_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT16) +#define UNITY_TEST_ASSERT_NOT_EQUAL_INT32(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT32)(threshold), (UNITY_INT)(UNITY_INT32)(actual), UNITY_NOT_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT32) +#define UNITY_TEST_ASSERT_NOT_EQUAL_UINT(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_NOT_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT) +#define UNITY_TEST_ASSERT_NOT_EQUAL_UINT8(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT8 )(threshold), (UNITY_INT)(UNITY_UINT8 )(actual), UNITY_NOT_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT8) +#define UNITY_TEST_ASSERT_NOT_EQUAL_UINT16(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT16)(threshold), (UNITY_INT)(UNITY_UINT16)(actual), UNITY_NOT_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT16) +#define UNITY_TEST_ASSERT_NOT_EQUAL_UINT32(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT32)(threshold), (UNITY_INT)(UNITY_UINT32)(actual), UNITY_NOT_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT32) +#define UNITY_TEST_ASSERT_NOT_EQUAL_HEX8(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT8 )(threshold), (UNITY_INT)(UNITY_UINT8 )(actual), UNITY_NOT_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX8) +#define UNITY_TEST_ASSERT_NOT_EQUAL_HEX16(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT16)(threshold), (UNITY_INT)(UNITY_UINT16)(actual), UNITY_NOT_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX16) +#define UNITY_TEST_ASSERT_NOT_EQUAL_HEX32(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT32)(threshold), (UNITY_INT)(UNITY_UINT32)(actual), UNITY_NOT_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX32) +#define UNITY_TEST_ASSERT_NOT_EQUAL_CHAR(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT8 )(threshold), (UNITY_INT)(UNITY_INT8 )(actual), UNITY_NOT_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_CHAR) + +#define UNITY_TEST_ASSERT_GREATER_THAN_INT(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_GREATER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT) +#define UNITY_TEST_ASSERT_GREATER_THAN_INT8(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT8 )(threshold), (UNITY_INT)(UNITY_INT8 )(actual), UNITY_GREATER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT8) +#define UNITY_TEST_ASSERT_GREATER_THAN_INT16(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT16)(threshold), (UNITY_INT)(UNITY_INT16)(actual), UNITY_GREATER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT16) +#define UNITY_TEST_ASSERT_GREATER_THAN_INT32(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT32)(threshold), (UNITY_INT)(UNITY_INT32)(actual), UNITY_GREATER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT32) +#define UNITY_TEST_ASSERT_GREATER_THAN_UINT(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_GREATER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT) +#define UNITY_TEST_ASSERT_GREATER_THAN_UINT8(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT8 )(threshold), (UNITY_INT)(UNITY_UINT8 )(actual), UNITY_GREATER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT8) +#define UNITY_TEST_ASSERT_GREATER_THAN_UINT16(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT16)(threshold), (UNITY_INT)(UNITY_UINT16)(actual), UNITY_GREATER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT16) +#define UNITY_TEST_ASSERT_GREATER_THAN_UINT32(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT32)(threshold), (UNITY_INT)(UNITY_UINT32)(actual), UNITY_GREATER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT32) +#define UNITY_TEST_ASSERT_GREATER_THAN_HEX8(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT8 )(threshold), (UNITY_INT)(UNITY_UINT8 )(actual), UNITY_GREATER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX8) +#define UNITY_TEST_ASSERT_GREATER_THAN_HEX16(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT16)(threshold), (UNITY_INT)(UNITY_UINT16)(actual), UNITY_GREATER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX16) +#define UNITY_TEST_ASSERT_GREATER_THAN_HEX32(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT32)(threshold), (UNITY_INT)(UNITY_UINT32)(actual), UNITY_GREATER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX32) +#define UNITY_TEST_ASSERT_GREATER_THAN_CHAR(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT8 )(threshold), (UNITY_INT)(UNITY_INT8 )(actual), UNITY_GREATER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_CHAR) + +#define UNITY_TEST_ASSERT_SMALLER_THAN_INT(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_SMALLER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT) +#define UNITY_TEST_ASSERT_SMALLER_THAN_INT8(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT8 )(threshold), (UNITY_INT)(UNITY_INT8 )(actual), UNITY_SMALLER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT8) +#define UNITY_TEST_ASSERT_SMALLER_THAN_INT16(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT16)(threshold), (UNITY_INT)(UNITY_INT16)(actual), UNITY_SMALLER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT16) +#define UNITY_TEST_ASSERT_SMALLER_THAN_INT32(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT32)(threshold), (UNITY_INT)(UNITY_INT32)(actual), UNITY_SMALLER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT32) +#define UNITY_TEST_ASSERT_SMALLER_THAN_UINT(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_SMALLER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT) +#define UNITY_TEST_ASSERT_SMALLER_THAN_UINT8(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT8 )(threshold), (UNITY_INT)(UNITY_UINT8 )(actual), UNITY_SMALLER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT8) +#define UNITY_TEST_ASSERT_SMALLER_THAN_UINT16(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT16)(threshold), (UNITY_INT)(UNITY_UINT16)(actual), UNITY_SMALLER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT16) +#define UNITY_TEST_ASSERT_SMALLER_THAN_UINT32(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT32)(threshold), (UNITY_INT)(UNITY_UINT32)(actual), UNITY_SMALLER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT32) +#define UNITY_TEST_ASSERT_SMALLER_THAN_HEX8(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT8 )(threshold), (UNITY_INT)(UNITY_UINT8 )(actual), UNITY_SMALLER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX8) +#define UNITY_TEST_ASSERT_SMALLER_THAN_HEX16(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT16)(threshold), (UNITY_INT)(UNITY_UINT16)(actual), UNITY_SMALLER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX16) +#define UNITY_TEST_ASSERT_SMALLER_THAN_HEX32(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT32)(threshold), (UNITY_INT)(UNITY_UINT32)(actual), UNITY_SMALLER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX32) +#define UNITY_TEST_ASSERT_SMALLER_THAN_CHAR(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT8 )(threshold), (UNITY_INT)(UNITY_INT8 )(actual), UNITY_SMALLER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_CHAR) + +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT) (threshold), (UNITY_INT) (actual), UNITY_GREATER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT) +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT8(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT8 ) (threshold), (UNITY_INT)(UNITY_INT8 ) (actual), UNITY_GREATER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT8) +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT16(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT16) (threshold), (UNITY_INT)(UNITY_INT16) (actual), UNITY_GREATER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT16) +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT32(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT32) (threshold), (UNITY_INT)(UNITY_INT32) (actual), UNITY_GREATER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT32) +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT) (threshold), (UNITY_INT) (actual), UNITY_GREATER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT) +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT8(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT8 )(threshold), (UNITY_INT)(UNITY_UINT8 )(actual), UNITY_GREATER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT8) +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT16(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT16)(threshold), (UNITY_INT)(UNITY_UINT16)(actual), UNITY_GREATER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT16) +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT32(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT32)(threshold), (UNITY_INT)(UNITY_UINT32)(actual), UNITY_GREATER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT32) +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX8(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT8 )(threshold), (UNITY_INT)(UNITY_UINT8 )(actual), UNITY_GREATER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX8) +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX16(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT16)(threshold), (UNITY_INT)(UNITY_UINT16)(actual), UNITY_GREATER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX16) +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX32(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT32)(threshold), (UNITY_INT)(UNITY_UINT32)(actual), UNITY_GREATER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX32) +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_CHAR(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT8 ) (threshold), (UNITY_INT)(UNITY_INT8 ) (actual), UNITY_GREATER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_CHAR) + +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT) (threshold), (UNITY_INT) (actual), UNITY_SMALLER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT) +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT8(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT8 )(threshold), (UNITY_INT)(UNITY_INT8 ) (actual), UNITY_SMALLER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT8) +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT16(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT16)(threshold), (UNITY_INT)(UNITY_INT16) (actual), UNITY_SMALLER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT16) +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT32(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT32)(threshold), (UNITY_INT)(UNITY_INT32) (actual), UNITY_SMALLER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT32) +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT) (threshold), (UNITY_INT) (actual), UNITY_SMALLER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT) +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT8(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT8 )(threshold), (UNITY_INT)(UNITY_UINT8 )(actual), UNITY_SMALLER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT8) +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT16(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT16)(threshold), (UNITY_INT)(UNITY_UINT16)(actual), UNITY_SMALLER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT16) +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT32(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT32)(threshold), (UNITY_INT)(UNITY_UINT32)(actual), UNITY_SMALLER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT32) +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX8(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT8 )(threshold), (UNITY_INT)(UNITY_UINT8 )(actual), UNITY_SMALLER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX8) +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX16(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT16)(threshold), (UNITY_INT)(UNITY_UINT16)(actual), UNITY_SMALLER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX16) +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX32(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_UINT32)(threshold), (UNITY_INT)(UNITY_UINT32)(actual), UNITY_SMALLER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX32) +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_CHAR(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(UNITY_INT8 )(threshold), (UNITY_INT)(UNITY_INT8 ) (actual), UNITY_SMALLER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_CHAR) + +#define UNITY_TEST_ASSERT_INT_WITHIN(delta, expected, actual, line, message) UnityAssertNumbersWithin( (delta), (UNITY_INT) (expected), (UNITY_INT) (actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT) +#define UNITY_TEST_ASSERT_INT8_WITHIN(delta, expected, actual, line, message) UnityAssertNumbersWithin((UNITY_UINT8 )(delta), (UNITY_INT)(UNITY_INT8 ) (expected), (UNITY_INT)(UNITY_INT8 ) (actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT8) +#define UNITY_TEST_ASSERT_INT16_WITHIN(delta, expected, actual, line, message) UnityAssertNumbersWithin((UNITY_UINT16)(delta), (UNITY_INT)(UNITY_INT16) (expected), (UNITY_INT)(UNITY_INT16) (actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT16) +#define UNITY_TEST_ASSERT_INT32_WITHIN(delta, expected, actual, line, message) UnityAssertNumbersWithin((UNITY_UINT32)(delta), (UNITY_INT)(UNITY_INT32) (expected), (UNITY_INT)(UNITY_INT32) (actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT32) +#define UNITY_TEST_ASSERT_UINT_WITHIN(delta, expected, actual, line, message) UnityAssertNumbersWithin( (delta), (UNITY_INT) (expected), (UNITY_INT) (actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT) +#define UNITY_TEST_ASSERT_UINT8_WITHIN(delta, expected, actual, line, message) UnityAssertNumbersWithin((UNITY_UINT8 )(delta), (UNITY_INT)(UNITY_UINT)(UNITY_UINT8 )(expected), (UNITY_INT)(UNITY_UINT)(UNITY_UINT8 )(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT8) +#define UNITY_TEST_ASSERT_UINT16_WITHIN(delta, expected, actual, line, message) UnityAssertNumbersWithin((UNITY_UINT16)(delta), (UNITY_INT)(UNITY_UINT)(UNITY_UINT16)(expected), (UNITY_INT)(UNITY_UINT)(UNITY_UINT16)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT16) +#define UNITY_TEST_ASSERT_UINT32_WITHIN(delta, expected, actual, line, message) UnityAssertNumbersWithin((UNITY_UINT32)(delta), (UNITY_INT)(UNITY_UINT)(UNITY_UINT32)(expected), (UNITY_INT)(UNITY_UINT)(UNITY_UINT32)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT32) +#define UNITY_TEST_ASSERT_HEX8_WITHIN(delta, expected, actual, line, message) UnityAssertNumbersWithin((UNITY_UINT8 )(delta), (UNITY_INT)(UNITY_UINT)(UNITY_UINT8 )(expected), (UNITY_INT)(UNITY_UINT)(UNITY_UINT8 )(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX8) +#define UNITY_TEST_ASSERT_HEX16_WITHIN(delta, expected, actual, line, message) UnityAssertNumbersWithin((UNITY_UINT16)(delta), (UNITY_INT)(UNITY_UINT)(UNITY_UINT16)(expected), (UNITY_INT)(UNITY_UINT)(UNITY_UINT16)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX16) +#define UNITY_TEST_ASSERT_HEX32_WITHIN(delta, expected, actual, line, message) UnityAssertNumbersWithin((UNITY_UINT32)(delta), (UNITY_INT)(UNITY_UINT)(UNITY_UINT32)(expected), (UNITY_INT)(UNITY_UINT)(UNITY_UINT32)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX32) +#define UNITY_TEST_ASSERT_CHAR_WITHIN(delta, expected, actual, line, message) UnityAssertNumbersWithin((UNITY_UINT8 )(delta), (UNITY_INT)(UNITY_INT8 ) (expected), (UNITY_INT)(UNITY_INT8 ) (actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_CHAR) + +#define UNITY_TEST_ASSERT_INT_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UnityAssertNumbersArrayWithin( (delta), (UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), ((UNITY_UINT32)(num_elements)), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_INT8_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UnityAssertNumbersArrayWithin((UNITY_UINT8 )(delta), (UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), ((UNITY_UINT32)(num_elements)), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT8, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_INT16_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UnityAssertNumbersArrayWithin((UNITY_UINT16)(delta), (UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), ((UNITY_UINT32)(num_elements)), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT16, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_INT32_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UnityAssertNumbersArrayWithin((UNITY_UINT32)(delta), (UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), ((UNITY_UINT32)(num_elements)), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT32, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_UINT_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UnityAssertNumbersArrayWithin( (delta), (UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), ((UNITY_UINT32)(num_elements)), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_UINT8_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UnityAssertNumbersArrayWithin((UNITY_UINT16)(delta), (UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), ((UNITY_UINT32)(num_elements)), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT8, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_UINT16_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UnityAssertNumbersArrayWithin((UNITY_UINT16)(delta), (UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), ((UNITY_UINT32)(num_elements)), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT16, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_UINT32_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UnityAssertNumbersArrayWithin((UNITY_UINT32)(delta), (UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), ((UNITY_UINT32)(num_elements)), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT32, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_HEX8_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UnityAssertNumbersArrayWithin((UNITY_UINT8 )(delta), (UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), ((UNITY_UINT32)(num_elements)), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX8, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_HEX16_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UnityAssertNumbersArrayWithin((UNITY_UINT16)(delta), (UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), ((UNITY_UINT32)(num_elements)), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX16, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_HEX32_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UnityAssertNumbersArrayWithin((UNITY_UINT32)(delta), (UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), ((UNITY_UINT32)(num_elements)), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX32, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_CHAR_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UnityAssertNumbersArrayWithin((UNITY_UINT8 )(delta), (UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), ((UNITY_UINT32)(num_elements)), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_CHAR, UNITY_ARRAY_TO_ARRAY) + + +#define UNITY_TEST_ASSERT_EQUAL_PTR(expected, actual, line, message) UnityAssertEqualNumber((UNITY_PTR_TO_INT)(expected), (UNITY_PTR_TO_INT)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_POINTER) +#define UNITY_TEST_ASSERT_EQUAL_STRING(expected, actual, line, message) UnityAssertEqualString((const char*)(expected), (const char*)(actual), (message), (UNITY_LINE_TYPE)(line)) +#define UNITY_TEST_ASSERT_EQUAL_STRING_LEN(expected, actual, len, line, message) UnityAssertEqualStringLen((const char*)(expected), (const char*)(actual), (UNITY_UINT32)(len), (message), (UNITY_LINE_TYPE)(line)) +#define UNITY_TEST_ASSERT_EQUAL_MEMORY(expected, actual, len, line, message) UnityAssertEqualMemory((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(len), 1, (message), (UNITY_LINE_TYPE)(line), UNITY_ARRAY_TO_ARRAY) + +#define UNITY_TEST_ASSERT_EQUAL_INT_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualIntArray((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EQUAL_INT8_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualIntArray((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT8, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EQUAL_INT16_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualIntArray((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT16, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EQUAL_INT32_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualIntArray((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT32, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EQUAL_UINT_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualIntArray((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualIntArray((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT8, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EQUAL_UINT16_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualIntArray((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT16, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EQUAL_UINT32_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualIntArray((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT32, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualIntArray((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX8, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EQUAL_HEX16_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualIntArray((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX16, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EQUAL_HEX32_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualIntArray((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX32, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EQUAL_PTR_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualIntArray((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_POINTER, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EQUAL_STRING_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualStringArray((UNITY_INTERNAL_PTR)(expected), (const char**)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EQUAL_MEMORY_ARRAY(expected, actual, len, num_elements, line, message) UnityAssertEqualMemory((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(len), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EQUAL_CHAR_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualIntArray((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_CHAR, UNITY_ARRAY_TO_ARRAY) + +#define UNITY_TEST_ASSERT_EACH_EQUAL_INT(expected, actual, num_elements, line, message) UnityAssertEqualIntArray(UnityNumToPtr((UNITY_INT) (expected), (UNITY_INT_WIDTH / 8)), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT, UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_EACH_EQUAL_INT8(expected, actual, num_elements, line, message) UnityAssertEqualIntArray(UnityNumToPtr((UNITY_INT)(UNITY_INT8 )(expected), 1), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT8, UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_EACH_EQUAL_INT16(expected, actual, num_elements, line, message) UnityAssertEqualIntArray(UnityNumToPtr((UNITY_INT)(UNITY_INT16 )(expected), 2), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT16, UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_EACH_EQUAL_INT32(expected, actual, num_elements, line, message) UnityAssertEqualIntArray(UnityNumToPtr((UNITY_INT)(UNITY_INT32 )(expected), 4), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT32, UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_EACH_EQUAL_UINT(expected, actual, num_elements, line, message) UnityAssertEqualIntArray(UnityNumToPtr((UNITY_INT) (expected), (UNITY_INT_WIDTH / 8)), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT, UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_EACH_EQUAL_UINT8(expected, actual, num_elements, line, message) UnityAssertEqualIntArray(UnityNumToPtr((UNITY_INT)(UNITY_UINT8 )(expected), 1), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT8, UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_EACH_EQUAL_UINT16(expected, actual, num_elements, line, message) UnityAssertEqualIntArray(UnityNumToPtr((UNITY_INT)(UNITY_UINT16)(expected), 2), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT16, UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_EACH_EQUAL_UINT32(expected, actual, num_elements, line, message) UnityAssertEqualIntArray(UnityNumToPtr((UNITY_INT)(UNITY_UINT32)(expected), 4), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT32, UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_EACH_EQUAL_HEX8(expected, actual, num_elements, line, message) UnityAssertEqualIntArray(UnityNumToPtr((UNITY_INT)(UNITY_INT8 )(expected), 1), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX8, UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_EACH_EQUAL_HEX16(expected, actual, num_elements, line, message) UnityAssertEqualIntArray(UnityNumToPtr((UNITY_INT)(UNITY_INT16 )(expected), 2), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX16, UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_EACH_EQUAL_HEX32(expected, actual, num_elements, line, message) UnityAssertEqualIntArray(UnityNumToPtr((UNITY_INT)(UNITY_INT32 )(expected), 4), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX32, UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_EACH_EQUAL_PTR(expected, actual, num_elements, line, message) UnityAssertEqualIntArray(UnityNumToPtr((UNITY_PTR_TO_INT) (expected), (UNITY_POINTER_WIDTH / 8)), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_POINTER, UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_EACH_EQUAL_STRING(expected, actual, num_elements, line, message) UnityAssertEqualStringArray((UNITY_INTERNAL_PTR)(expected), (const char**)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_EACH_EQUAL_MEMORY(expected, actual, len, num_elements, line, message) UnityAssertEqualMemory((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(len), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_EACH_EQUAL_CHAR(expected, actual, num_elements, line, message) UnityAssertEqualIntArray(UnityNumToPtr((UNITY_INT)(UNITY_INT8 )(expected), 1), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_CHAR, UNITY_ARRAY_TO_VAL) + +#ifdef UNITY_SUPPORT_64 +#define UNITY_TEST_ASSERT_EQUAL_INT64(expected, actual, line, message) UnityAssertEqualNumber((UNITY_INT)(expected), (UNITY_INT)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT64) +#define UNITY_TEST_ASSERT_EQUAL_UINT64(expected, actual, line, message) UnityAssertEqualNumber((UNITY_INT)(expected), (UNITY_INT)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT64) +#define UNITY_TEST_ASSERT_EQUAL_HEX64(expected, actual, line, message) UnityAssertEqualNumber((UNITY_INT)(expected), (UNITY_INT)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX64) +#define UNITY_TEST_ASSERT_EQUAL_INT64_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualIntArray((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT64, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EQUAL_UINT64_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualIntArray((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT64, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EQUAL_HEX64_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualIntArray((UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX64, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EACH_EQUAL_INT64(expected, actual, num_elements, line, message) UnityAssertEqualIntArray(UnityNumToPtr((UNITY_INT)(UNITY_INT64)(expected), 8), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT64, UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_EACH_EQUAL_UINT64(expected, actual, num_elements, line, message) UnityAssertEqualIntArray(UnityNumToPtr((UNITY_INT)(UNITY_UINT64)(expected), 8), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT64, UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_EACH_EQUAL_HEX64(expected, actual, num_elements, line, message) UnityAssertEqualIntArray(UnityNumToPtr((UNITY_INT)(UNITY_INT64)(expected), 8), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX64, UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_INT64_WITHIN(delta, expected, actual, line, message) UnityAssertNumbersWithin((delta), (UNITY_INT)(expected), (UNITY_INT)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT64) +#define UNITY_TEST_ASSERT_UINT64_WITHIN(delta, expected, actual, line, message) UnityAssertNumbersWithin((delta), (UNITY_INT)(expected), (UNITY_INT)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT64) +#define UNITY_TEST_ASSERT_HEX64_WITHIN(delta, expected, actual, line, message) UnityAssertNumbersWithin((delta), (UNITY_INT)(expected), (UNITY_INT)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX64) +#define UNITY_TEST_ASSERT_NOT_EQUAL_INT64(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_NOT_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT64) +#define UNITY_TEST_ASSERT_NOT_EQUAL_UINT64(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_NOT_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT64) +#define UNITY_TEST_ASSERT_NOT_EQUAL_HEX64(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_NOT_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX64) +#define UNITY_TEST_ASSERT_GREATER_THAN_INT64(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_GREATER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT64) +#define UNITY_TEST_ASSERT_GREATER_THAN_UINT64(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_GREATER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT64) +#define UNITY_TEST_ASSERT_GREATER_THAN_HEX64(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_GREATER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX64) +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT64(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_GREATER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT64) +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT64(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_GREATER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT64) +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX64(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_GREATER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX64) +#define UNITY_TEST_ASSERT_SMALLER_THAN_INT64(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_SMALLER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT64) +#define UNITY_TEST_ASSERT_SMALLER_THAN_UINT64(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_SMALLER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT64) +#define UNITY_TEST_ASSERT_SMALLER_THAN_HEX64(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_SMALLER_THAN, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX64) +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT64(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_SMALLER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT64) +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT64(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_SMALLER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT64) +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX64(threshold, actual, line, message) UnityAssertGreaterOrLessOrEqualNumber((UNITY_INT)(threshold), (UNITY_INT)(actual), UNITY_SMALLER_OR_EQUAL, (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX64) +#define UNITY_TEST_ASSERT_INT64_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UnityAssertNumbersArrayWithin((UNITY_UINT64)(delta), (UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_INT64, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_UINT64_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UnityAssertNumbersArrayWithin((UNITY_UINT64)(delta), (UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_UINT64, UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_HEX64_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UnityAssertNumbersArrayWithin((UNITY_UINT64)(delta), (UNITY_INTERNAL_PTR)(expected), (UNITY_INTERNAL_PTR)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_DISPLAY_STYLE_HEX64, UNITY_ARRAY_TO_ARRAY) +#else +#define UNITY_TEST_ASSERT_EQUAL_INT64(expected, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_EQUAL_UINT64(expected, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_EQUAL_HEX64(expected, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_EQUAL_INT64_ARRAY(expected, actual, num_elements, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_EQUAL_UINT64_ARRAY(expected, actual, num_elements, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_EQUAL_HEX64_ARRAY(expected, actual, num_elements, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_INT64_WITHIN(delta, expected, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_UINT64_WITHIN(delta, expected, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_HEX64_WITHIN(delta, expected, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_GREATER_THAN_INT64(threshold, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_GREATER_THAN_UINT64(threshold, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_GREATER_THAN_HEX64(threshold, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT64(threshold, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT64(threshold, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX64(threshold, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_SMALLER_THAN_INT64(threshold, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_SMALLER_THAN_UINT64(threshold, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_SMALLER_THAN_HEX64(threshold, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT64(threshold, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT64(threshold, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX64(threshold, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_INT64_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_UINT64_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#define UNITY_TEST_ASSERT_HEX64_ARRAY_WITHIN(delta, expected, actual, num_elements, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErr64) +#endif + +#ifdef UNITY_EXCLUDE_FLOAT +#define UNITY_TEST_ASSERT_FLOAT_WITHIN(delta, expected, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrFloat) +#define UNITY_TEST_ASSERT_EQUAL_FLOAT(expected, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrFloat) +#define UNITY_TEST_ASSERT_EQUAL_FLOAT_ARRAY(expected, actual, num_elements, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrFloat) +#define UNITY_TEST_ASSERT_EACH_EQUAL_FLOAT(expected, actual, num_elements, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrFloat) +#define UNITY_TEST_ASSERT_FLOAT_IS_INF(actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrFloat) +#define UNITY_TEST_ASSERT_FLOAT_IS_NEG_INF(actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrFloat) +#define UNITY_TEST_ASSERT_FLOAT_IS_NAN(actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrFloat) +#define UNITY_TEST_ASSERT_FLOAT_IS_DETERMINATE(actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrFloat) +#define UNITY_TEST_ASSERT_FLOAT_IS_NOT_INF(actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrFloat) +#define UNITY_TEST_ASSERT_FLOAT_IS_NOT_NEG_INF(actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrFloat) +#define UNITY_TEST_ASSERT_FLOAT_IS_NOT_NAN(actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrFloat) +#define UNITY_TEST_ASSERT_FLOAT_IS_NOT_DETERMINATE(actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrFloat) +#else +#define UNITY_TEST_ASSERT_FLOAT_WITHIN(delta, expected, actual, line, message) UnityAssertFloatsWithin((UNITY_FLOAT)(delta), (UNITY_FLOAT)(expected), (UNITY_FLOAT)(actual), (message), (UNITY_LINE_TYPE)(line)) +#define UNITY_TEST_ASSERT_EQUAL_FLOAT(expected, actual, line, message) UNITY_TEST_ASSERT_FLOAT_WITHIN((UNITY_FLOAT)(expected) * (UNITY_FLOAT)UNITY_FLOAT_PRECISION, (UNITY_FLOAT)(expected), (UNITY_FLOAT)(actual), (UNITY_LINE_TYPE)(line), (message)) +#define UNITY_TEST_ASSERT_EQUAL_FLOAT_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualFloatArray((UNITY_FLOAT*)(expected), (UNITY_FLOAT*)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EACH_EQUAL_FLOAT(expected, actual, num_elements, line, message) UnityAssertEqualFloatArray(UnityFloatToPtr(expected), (UNITY_FLOAT*)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_FLOAT_IS_INF(actual, line, message) UnityAssertFloatSpecial((UNITY_FLOAT)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_FLOAT_IS_INF) +#define UNITY_TEST_ASSERT_FLOAT_IS_NEG_INF(actual, line, message) UnityAssertFloatSpecial((UNITY_FLOAT)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_FLOAT_IS_NEG_INF) +#define UNITY_TEST_ASSERT_FLOAT_IS_NAN(actual, line, message) UnityAssertFloatSpecial((UNITY_FLOAT)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_FLOAT_IS_NAN) +#define UNITY_TEST_ASSERT_FLOAT_IS_DETERMINATE(actual, line, message) UnityAssertFloatSpecial((UNITY_FLOAT)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_FLOAT_IS_DET) +#define UNITY_TEST_ASSERT_FLOAT_IS_NOT_INF(actual, line, message) UnityAssertFloatSpecial((UNITY_FLOAT)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_FLOAT_IS_NOT_INF) +#define UNITY_TEST_ASSERT_FLOAT_IS_NOT_NEG_INF(actual, line, message) UnityAssertFloatSpecial((UNITY_FLOAT)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_FLOAT_IS_NOT_NEG_INF) +#define UNITY_TEST_ASSERT_FLOAT_IS_NOT_NAN(actual, line, message) UnityAssertFloatSpecial((UNITY_FLOAT)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_FLOAT_IS_NOT_NAN) +#define UNITY_TEST_ASSERT_FLOAT_IS_NOT_DETERMINATE(actual, line, message) UnityAssertFloatSpecial((UNITY_FLOAT)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_FLOAT_IS_NOT_DET) +#endif + +#ifdef UNITY_EXCLUDE_DOUBLE +#define UNITY_TEST_ASSERT_DOUBLE_WITHIN(delta, expected, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrDouble) +#define UNITY_TEST_ASSERT_EQUAL_DOUBLE(expected, actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrDouble) +#define UNITY_TEST_ASSERT_EQUAL_DOUBLE_ARRAY(expected, actual, num_elements, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrDouble) +#define UNITY_TEST_ASSERT_EACH_EQUAL_DOUBLE(expected, actual, num_elements, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrDouble) +#define UNITY_TEST_ASSERT_DOUBLE_IS_INF(actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrDouble) +#define UNITY_TEST_ASSERT_DOUBLE_IS_NEG_INF(actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrDouble) +#define UNITY_TEST_ASSERT_DOUBLE_IS_NAN(actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrDouble) +#define UNITY_TEST_ASSERT_DOUBLE_IS_DETERMINATE(actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrDouble) +#define UNITY_TEST_ASSERT_DOUBLE_IS_NOT_INF(actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrDouble) +#define UNITY_TEST_ASSERT_DOUBLE_IS_NOT_NEG_INF(actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrDouble) +#define UNITY_TEST_ASSERT_DOUBLE_IS_NOT_NAN(actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrDouble) +#define UNITY_TEST_ASSERT_DOUBLE_IS_NOT_DETERMINATE(actual, line, message) UNITY_TEST_FAIL((UNITY_LINE_TYPE)(line), UnityStrErrDouble) +#else +#define UNITY_TEST_ASSERT_DOUBLE_WITHIN(delta, expected, actual, line, message) UnityAssertDoublesWithin((UNITY_DOUBLE)(delta), (UNITY_DOUBLE)(expected), (UNITY_DOUBLE)(actual), (message), (UNITY_LINE_TYPE)(line)) +#define UNITY_TEST_ASSERT_EQUAL_DOUBLE(expected, actual, line, message) UNITY_TEST_ASSERT_DOUBLE_WITHIN((UNITY_DOUBLE)(expected) * (UNITY_DOUBLE)UNITY_DOUBLE_PRECISION, (UNITY_DOUBLE)(expected), (UNITY_DOUBLE)(actual), (UNITY_LINE_TYPE)(line), (message)) +#define UNITY_TEST_ASSERT_EQUAL_DOUBLE_ARRAY(expected, actual, num_elements, line, message) UnityAssertEqualDoubleArray((UNITY_DOUBLE*)(expected), (UNITY_DOUBLE*)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_ARRAY_TO_ARRAY) +#define UNITY_TEST_ASSERT_EACH_EQUAL_DOUBLE(expected, actual, num_elements, line, message) UnityAssertEqualDoubleArray(UnityDoubleToPtr(expected), (UNITY_DOUBLE*)(actual), (UNITY_UINT32)(num_elements), (message), (UNITY_LINE_TYPE)(line), UNITY_ARRAY_TO_VAL) +#define UNITY_TEST_ASSERT_DOUBLE_IS_INF(actual, line, message) UnityAssertDoubleSpecial((UNITY_DOUBLE)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_FLOAT_IS_INF) +#define UNITY_TEST_ASSERT_DOUBLE_IS_NEG_INF(actual, line, message) UnityAssertDoubleSpecial((UNITY_DOUBLE)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_FLOAT_IS_NEG_INF) +#define UNITY_TEST_ASSERT_DOUBLE_IS_NAN(actual, line, message) UnityAssertDoubleSpecial((UNITY_DOUBLE)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_FLOAT_IS_NAN) +#define UNITY_TEST_ASSERT_DOUBLE_IS_DETERMINATE(actual, line, message) UnityAssertDoubleSpecial((UNITY_DOUBLE)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_FLOAT_IS_DET) +#define UNITY_TEST_ASSERT_DOUBLE_IS_NOT_INF(actual, line, message) UnityAssertDoubleSpecial((UNITY_DOUBLE)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_FLOAT_IS_NOT_INF) +#define UNITY_TEST_ASSERT_DOUBLE_IS_NOT_NEG_INF(actual, line, message) UnityAssertDoubleSpecial((UNITY_DOUBLE)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_FLOAT_IS_NOT_NEG_INF) +#define UNITY_TEST_ASSERT_DOUBLE_IS_NOT_NAN(actual, line, message) UnityAssertDoubleSpecial((UNITY_DOUBLE)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_FLOAT_IS_NOT_NAN) +#define UNITY_TEST_ASSERT_DOUBLE_IS_NOT_DETERMINATE(actual, line, message) UnityAssertDoubleSpecial((UNITY_DOUBLE)(actual), (message), (UNITY_LINE_TYPE)(line), UNITY_FLOAT_IS_NOT_DET) +#endif + +/* End of UNITY_INTERNALS_H */ +#endif diff --git a/integration-tests/src/bindings_tests/uuid.c b/integration-tests/src/bindings_tests/uuid.c new file mode 100644 index 000000000..869d0cc17 --- /dev/null +++ b/integration-tests/src/bindings_tests/uuid.c @@ -0,0 +1,44 @@ +#include +#include "unity.h" +#include "taskchampion.h" + +// creating UUIDs does not crash +static void test_uuid_creation(void) { + tc_uuid_new_v4(); + tc_uuid_nil(); +} + +// converting UUIDs from string works +static void test_uuid_conversion_to_string(void) { + TEST_ASSERT_EQUAL(TC_UUID_STRING_BYTES, 36); + + TCUuid u2 = tc_uuid_nil(); + + char u2str[TC_UUID_STRING_BYTES]; + tc_uuid_to_str(u2, u2str); + TEST_ASSERT_EQUAL_MEMORY("00000000-0000-0000-0000-000000000000", u2str, TC_UUID_STRING_BYTES); +} + +// converting invalid UUIDs from string fails as expected +static void test_uuid_invalid_string_fails(void) { + TCUuid u; + char ustr[36] = "not-a-valid-uuid"; + TEST_ASSERT_FALSE(tc_uuid_from_str(ustr, &u)); +} + +// converting invalid UTF-8 UUIDs from string fails as expected +static void test_uuid_bad_utf8(void) { + TCUuid u; + char ustr[36] = "\xf0\x28\x8c\xbc"; + TEST_ASSERT_FALSE(tc_uuid_from_str(ustr, &u)); +} + +int uuid_tests(void) { + UNITY_BEGIN(); + // each test case above should be named here, in order. + RUN_TEST(test_uuid_creation); + RUN_TEST(test_uuid_conversion_to_string); + RUN_TEST(test_uuid_invalid_string_fails); + RUN_TEST(test_uuid_bad_utf8); + return UNITY_END(); +} diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs new file mode 100644 index 000000000..71b0b274c --- /dev/null +++ b/integration-tests/src/lib.rs @@ -0,0 +1 @@ +pub mod bindings_tests; diff --git a/integration-tests/tests/bindings.rs b/integration-tests/tests/bindings.rs new file mode 100644 index 000000000..b4df3a85a --- /dev/null +++ b/integration-tests/tests/bindings.rs @@ -0,0 +1,15 @@ +macro_rules! suite( + { $s:ident } => { + #[test] + fn $s() { + assert_eq!(integration_tests::bindings_tests::$s(), 0); + } + }; +); + +// keep this list in sync with integration-tests/build.rs and +// integration-tests/src/bindings_tests/mod.rs +suite!(uuid_tests); +suite!(string_tests); +suite!(task_tests); +suite!(replica_tests); diff --git a/lib/build.rs b/lib/build.rs index d1ebc7bf1..462b707a4 100644 --- a/lib/build.rs +++ b/lib/build.rs @@ -7,10 +7,12 @@ fn main() { Builder::new() .with_crate(crate_dir) - .with_language(Language::C) .with_config(Config { + language: Language::C, cpp_compat: true, + sys_includes: vec!["stdbool.h".into(), "stdint.h".into()], usize_is_size_t: true, + no_includes: true, enumeration: EnumConfig { // this appears to still default to true for C enum_class: false, diff --git a/lib/src/replica.rs b/lib/src/replica.rs index c137b105a..08bab398f 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -1,14 +1,14 @@ use crate::{status::TCStatus, string::TCString, task::TCTask}; -use libc::c_char; -use std::ffi::CString; use taskchampion::{Replica, StorageConfig}; /// A replica represents an instance of a user's task data, providing an easy interface /// for querying and modifying that data. +/// +/// TCReplicas are not threadsafe. pub struct TCReplica { // TODO: make this a RefCell so that it can be take()n when holding a mut ref inner: Replica, - error: Option, + error: Option>, } /// Utility function to safely convert *mut TCReplica into &mut TCReplica @@ -17,6 +17,10 @@ fn rep_ref(rep: *mut TCReplica) -> &'static mut TCReplica { unsafe { &mut *rep } } +fn err_to_tcstring(e: impl std::string::ToString) -> TCString<'static> { + TCString::from(e.to_string()) +} + /// Utility function to allow using `?` notation to return an error value. fn wrap<'a, T, F>(rep: *mut TCReplica, f: F, err_value: T) -> T where @@ -26,41 +30,48 @@ where match f(&mut rep.inner) { Ok(v) => v, Err(e) => { - let error = e.to_string(); - let error = match CString::new(error.as_bytes()) { - Ok(e) => e, - Err(_) => CString::new("(invalid error message)".as_bytes()).unwrap(), - }; - rep.error = Some(error); + rep.error = Some(err_to_tcstring(e)); err_value } } } -/// Create a new TCReplica. -/// -/// If path is NULL, then an in-memory replica is created. Otherwise, path is the path to the -/// on-disk storage for this replica. The path argument is no longer referenced after return. -/// -/// Returns NULL on error; see tc_replica_error. -/// -/// TCReplicas are not threadsafe. +/// Create a new TCReplica with an in-memory database. The contents of the database will be +/// lost when it is freed. #[no_mangle] -pub extern "C" fn tc_replica_new<'a>(path: *mut TCString) -> *mut TCReplica { - let storage_res = if path.is_null() { - StorageConfig::InMemory.into_storage() - } else { - let path = TCString::from_arg(path); - StorageConfig::OnDisk { - taskdb_dir: path.to_path_buf(), - } +pub extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica { + let storage = StorageConfig::InMemory .into_storage() - }; + .expect("in-memory always succeeds"); + Box::into_raw(Box::new(TCReplica { + inner: Replica::new(storage), + error: None, + })) +} + +/// Create a new TCReplica with an on-disk database. On error, a string is written to the +/// `error_out` parameter (if it is not NULL) and NULL is returned. +#[no_mangle] +pub extern "C" fn tc_replica_new_on_disk<'a>( + path: *mut TCString, + error_out: *mut *mut TCString, +) -> *mut TCReplica { + let path = TCString::from_arg(path); + let storage_res = StorageConfig::OnDisk { + taskdb_dir: path.to_path_buf(), + } + .into_storage(); let storage = match storage_res { Ok(storage) => storage, - // TODO: report errors somehow - Err(_) => return std::ptr::null_mut(), + Err(e) => { + if !error_out.is_null() { + unsafe { + *error_out = err_to_tcstring(e).return_val(); + } + } + return std::ptr::null_mut(); + } }; Box::into_raw(Box::new(TCReplica { @@ -111,15 +122,15 @@ pub extern "C" fn tc_replica_undo<'a>(rep: *mut TCReplica) -> i32 { } /// Get the latest error for a replica, or NULL if the last operation succeeded. -/// -/// The returned string is valid until the next replica operation. +/// Subsequent calls to this function will return NULL. The caller must free the +/// returned string. #[no_mangle] -pub extern "C" fn tc_replica_error<'a>(rep: *mut TCReplica) -> *const c_char { - let rep: &'a TCReplica = rep_ref(rep); - if let Some(ref e) = rep.error { - e.as_ptr() +pub extern "C" fn tc_replica_error<'a>(rep: *mut TCReplica) -> *mut TCString<'static> { + let rep: &'a mut TCReplica = rep_ref(rep); + if let Some(tcstring) = rep.error.take() { + tcstring.return_val() } else { - std::ptr::null() + std::ptr::null_mut() } } diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 560f23eb1..02e2284fd 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -1,131 +1,185 @@ -#include -#include -#include -#include -#include -#include +#include +#include -/// The status of a task, as defined by the task data model. -enum TCStatus { +/** + * The status of a task, as defined by the task data model. + */ +typedef enum TCStatus { TC_STATUS_PENDING, TC_STATUS_COMPLETED, TC_STATUS_DELETED, - /// Unknown signifies a status in the task DB that was not - /// recognized. + /** + * Unknown signifies a status in the task DB that was not + * recognized. + */ TC_STATUS_UNKNOWN, -}; +} TCStatus; -/// A replica represents an instance of a user's task data, providing an easy interface -/// for querying and modifying that data. -struct TCReplica; +/** + * A replica represents an instance of a user's task data, providing an easy interface + * for querying and modifying that data. + * + * TCReplicas are not threadsafe. + */ +typedef struct TCReplica TCReplica; -/// TCString supports passing strings into and out of the TaskChampion API. -/// -/// Unless specified otherwise, functions in this API take ownership of a TCString when it appears -/// as a function argument, and transfer ownership to the caller when the TCString appears as a -/// return value or output argument. -struct TCString; +/** + * TCString supports passing strings into and out of the TaskChampion API. + * + * Unless specified otherwise, functions in this API take ownership of a TCString when it appears + * as a function argument, and transfer ownership to the caller when the TCString appears as a + * return value or output argument. + */ +typedef struct TCString TCString; -/// A task, as publicly exposed by this library. -/// -/// A task carries no reference to the replica that created it, and can -/// be used until it is freed or converted to a TaskMut. -struct TCTask; +/** + * A task, as publicly exposed by this library. + * + * A task carries no reference to the replica that created it, and can + * be used until it is freed or converted to a TaskMut. + */ +typedef struct TCTask TCTask; -/// TCUuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. -/// Uuids are typically treated as opaque, but the bytes are available in big-endian format. -/// -struct TCUuid { +/** + * TCUuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. + * Uuids are typically treated as opaque, but the bytes are available in big-endian format. + * + */ +typedef struct TCUuid { uint8_t bytes[16]; -}; +} TCUuid; +#ifdef __cplusplus extern "C" { +#endif // __cplusplus extern const size_t TC_UUID_STRING_BYTES; -/// Create a new TCReplica. -/// -/// If path is NULL, then an in-memory replica is created. Otherwise, path is the path to the -/// on-disk storage for this replica. The path argument is no longer referenced after return. -/// -/// Returns NULL on error; see tc_replica_error. -/// -/// TCReplicas are not threadsafe. -TCReplica *tc_replica_new(TCString *path); +/** + * Create a new TCReplica with an in-memory database. The contents of the database will be + * lost when it is freed. + */ +struct TCReplica *tc_replica_new_in_memory(void); -/// Create a new task. The task must not already exist. -/// -/// Returns the task, or NULL on error. -TCTask *tc_replica_new_task(TCReplica *rep, TCStatus status, TCString *description); +/** + * Create a new TCReplica with an on-disk database. On error, a string is written to the + * `error_out` parameter (if it is not NULL) and NULL is returned. + */ +struct TCReplica *tc_replica_new_on_disk(struct TCString *path, struct TCString **error_out); -/// Undo local operations until the most recent UndoPoint. -/// -/// Returns -1 on error, 0 if there are no local operations to undo, and 1 if operations were -/// undone. -int32_t tc_replica_undo(TCReplica *rep); +/** + * Create a new task. The task must not already exist. + * + * Returns the task, or NULL on error. + */ +struct TCTask *tc_replica_new_task(struct TCReplica *rep, + enum TCStatus status, + struct TCString *description); -/// Get the latest error for a replica, or NULL if the last operation succeeded. -/// -/// The returned string is valid until the next replica operation. -const char *tc_replica_error(TCReplica *rep); +/** + * Undo local operations until the most recent UndoPoint. + * + * Returns -1 on error, 0 if there are no local operations to undo, and 1 if operations were + * undone. + */ +int32_t tc_replica_undo(struct TCReplica *rep); -/// Free a TCReplica. -void tc_replica_free(TCReplica *rep); +/** + * Get the latest error for a replica, or NULL if the last operation succeeded. + * Subsequent calls to this function will return NULL. The caller must free the + * returned string. + */ +struct TCString *tc_replica_error(struct TCReplica *rep); -/// Create a new TCString referencing the given C string. The C string must remain valid until -/// after the TCString is freed. It's typically easiest to ensure this by using a static string. -TCString *tc_string_new(const char *cstr); +/** + * Free a TCReplica. + */ +void tc_replica_free(struct TCReplica *rep); -/// Create a new TCString by cloning the content of the given C string. -TCString *tc_string_clone(const char *cstr); +/** + * Create a new TCString referencing the given C string. The C string must remain valid until + * after the TCString is freed. It's typically easiest to ensure this by using a static string. + */ +struct TCString *tc_string_new(const char *cstr); -/// Create a new TCString containing the given string with the given length. This allows creation -/// of strings containing embedded NUL characters. If the given string is not valid UTF-8, this -/// function will return NULL. -TCString *tc_string_clone_with_len(const char *buf, size_t len); +/** + * Create a new TCString by cloning the content of the given C string. + */ +struct TCString *tc_string_clone(const char *cstr); -/// Get the content of the string as a regular C string. The given string must not be NULL. The -/// returned value is NULL if the string contains NUL bytes. The returned string is valid until -/// the TCString is freed or passed to another TC API function. -/// -/// This function does _not_ take ownership of the TCString. -const char *tc_string_content(TCString *tcstring); +/** + * Create a new TCString containing the given string with the given length. This allows creation + * of strings containing embedded NUL characters. If the given string is not valid UTF-8, this + * function will return NULL. + */ +struct TCString *tc_string_clone_with_len(const char *buf, size_t len); -/// Get the content of the string as a pointer and length. The given string must not be NULL. -/// This function can return any string, even one including NUL bytes. The returned string is -/// valid until the TCString is freed or passed to another TC API function. -/// -/// This function does _not_ take ownership of the TCString. -const char *tc_string_content_with_len(TCString *tcstring, size_t *len_out); +/** + * Get the content of the string as a regular C string. The given string must not be NULL. The + * returned value is NULL if the string contains NUL bytes. The returned string is valid until + * the TCString is freed or passed to another TC API function. + * + * This function does _not_ take ownership of the TCString. + */ +const char *tc_string_content(struct TCString *tcstring); -/// Free a TCString. -void tc_string_free(TCString *string); +/** + * Get the content of the string as a pointer and length. The given string must not be NULL. + * This function can return any string, even one including NUL bytes. The returned string is + * valid until the TCString is freed or passed to another TC API function. + * + * This function does _not_ take ownership of the TCString. + */ +const char *tc_string_content_with_len(struct TCString *tcstring, size_t *len_out); -/// Get a task's UUID. -TCUuid tc_task_get_uuid(const TCTask *task); +/** + * Free a TCString. + */ +void tc_string_free(struct TCString *string); -/// Get a task's status. -TCStatus tc_task_get_status(const TCTask *task); +/** + * Get a task's UUID. + */ +struct TCUuid tc_task_get_uuid(const struct TCTask *task); -/// Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it -/// contains embedded NUL characters). -TCString *tc_task_get_description(const TCTask *task); +/** + * Get a task's status. + */ +enum TCStatus tc_task_get_status(const struct TCTask *task); -/// Free a task. -void tc_task_free(TCTask *task); +/** + * Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it + * contains embedded NUL characters). + */ +struct TCString *tc_task_get_description(const struct TCTask *task); -/// Create a new, randomly-generated UUID. -TCUuid tc_uuid_new_v4(); +/** + * Free a task. + */ +void tc_task_free(struct TCTask *task); -/// Create a new UUID with the nil value. -TCUuid tc_uuid_nil(); +/** + * Create a new, randomly-generated UUID. + */ +struct TCUuid tc_uuid_new_v4(void); -/// Write the string representation of a TCUuid into the given buffer, which must be -/// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. -void tc_uuid_to_str(TCUuid uuid, char *out); +/** + * Create a new UUID with the nil value. + */ +struct TCUuid tc_uuid_nil(void); -/// Parse the given value as a UUID. The value must be exactly TC_UUID_STRING_BYTES long. Returns -/// false on failure. -bool tc_uuid_from_str(const char *val, TCUuid *out); +/** + * Write the string representation of a TCUuid into the given buffer, which must be + * at least TC_UUID_STRING_BYTES long. No NUL terminator is added. + */ +void tc_uuid_to_str(struct TCUuid uuid, char *out); +/** + * Parse the given value as a UUID. The value must be exactly TC_UUID_STRING_BYTES long. Returns + * false on failure. + */ +bool tc_uuid_from_str(const char *val, struct TCUuid *out); + +#ifdef __cplusplus } // extern "C" +#endif // __cplusplus From ca0279a73f12bb6c66bb4e1dab3d1b9a04c1d52c Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Tue, 25 Jan 2022 01:32:22 +0000 Subject: [PATCH 12/95] move existing integration tests into new crate --- Cargo.lock | 23 +++++++------------ Cargo.toml | 1 - integration-tests/Cargo.toml | 10 ++++++++ .../tests/cross-sync.rs | 0 .../tests/snapshots.rs | 0 replica-server-tests/Cargo.toml | 21 ----------------- replica-server-tests/src/lib.rs | 1 - 7 files changed, 18 insertions(+), 38 deletions(-) rename {replica-server-tests => integration-tests}/tests/cross-sync.rs (100%) rename {replica-server-tests => integration-tests}/tests/snapshots.rs (100%) delete mode 100644 replica-server-tests/Cargo.toml delete mode 100644 replica-server-tests/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 7924f76e9..b2b905807 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1524,9 +1524,17 @@ dependencies = [ name = "integration-tests" version = "0.4.1" dependencies = [ + "actix-rt", + "actix-web", + "anyhow", "cc", + "env_logger 0.8.4", + "log", + "pretty_assertions", "taskchampion", "taskchampion-lib", + "taskchampion-sync-server", + "tempfile", ] [[package]] @@ -2488,21 +2496,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "replica-server-tests" -version = "0.4.1" -dependencies = [ - "actix-rt", - "actix-web", - "anyhow", - "env_logger 0.8.4", - "log", - "pretty_assertions", - "taskchampion", - "taskchampion-sync-server", - "tempfile", -] - [[package]] name = "resolv-conf" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index 09c0a9677..18399678b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ members = [ "taskchampion", "cli", "sync-server", - "replica-server-tests", "lib", "integration-tests", ] diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index f351e17d3..71f39b6e6 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -8,6 +8,16 @@ build = "build.rs" [dependencies] taskchampion = { path = "../taskchampion" } +taskchampion-sync-server = { path = "../sync-server" } + +[dev-dependencies] +anyhow = "1.0" +actix-web = "^3.3.2" +actix-rt = "^1.1.1" +tempfile = "3" +pretty_assertions = "1" +log = "^0.4.14" +env_logger = "^0.8.3" [build-dependencies] cc = "1.0" diff --git a/replica-server-tests/tests/cross-sync.rs b/integration-tests/tests/cross-sync.rs similarity index 100% rename from replica-server-tests/tests/cross-sync.rs rename to integration-tests/tests/cross-sync.rs diff --git a/replica-server-tests/tests/snapshots.rs b/integration-tests/tests/snapshots.rs similarity index 100% rename from replica-server-tests/tests/snapshots.rs rename to integration-tests/tests/snapshots.rs diff --git a/replica-server-tests/Cargo.toml b/replica-server-tests/Cargo.toml deleted file mode 100644 index efe369b3c..000000000 --- a/replica-server-tests/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "replica-server-tests" -version = "0.4.1" -authors = ["Dustin J. Mitchell "] -edition = "2018" -publish = false - -[dependencies.taskchampion-sync-server] -path = "../sync-server" - -[dependencies.taskchampion] -path = "../taskchampion" - -[dev-dependencies] -anyhow = "1.0" -actix-web = "^3.3.2" -actix-rt = "^1.1.1" -tempfile = "3" -pretty_assertions = "1" -log = "^0.4.14" -env_logger = "^0.8.3" diff --git a/replica-server-tests/src/lib.rs b/replica-server-tests/src/lib.rs deleted file mode 100644 index e783558fc..000000000 --- a/replica-server-tests/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -// test-only crate From 0d68e65354fdb9d83a4d4e6270b68a6aeafb8209 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Tue, 25 Jan 2022 23:29:52 +0000 Subject: [PATCH 13/95] some polish on strings --- .../src/bindings_tests/replica.c | 4 +- integration-tests/src/bindings_tests/string.c | 4 +- integration-tests/src/bindings_tests/task.c | 2 +- lib/src/lib.rs | 1 + lib/src/replica.rs | 20 ++++++-- lib/src/result.rs | 10 ++++ lib/src/string.rs | 37 +++++++++++--- lib/taskchampion.h | 51 +++++++++++++++---- 8 files changed, 100 insertions(+), 29 deletions(-) create mode 100644 lib/src/result.rs diff --git a/integration-tests/src/bindings_tests/replica.c b/integration-tests/src/bindings_tests/replica.c index 71eef0efe..613b15666 100644 --- a/integration-tests/src/bindings_tests/replica.c +++ b/integration-tests/src/bindings_tests/replica.c @@ -13,7 +13,7 @@ static void test_replica_creation(void) { // creating an on-disk replica does not crash static void test_replica_creation_disk(void) { - TCReplica *rep = tc_replica_new_on_disk(tc_string_new("test-db"), NULL); + TCReplica *rep = tc_replica_new_on_disk(tc_string_borrow("test-db"), NULL); TEST_ASSERT_NOT_NULL(rep); TEST_ASSERT_NULL(tc_replica_error(rep)); tc_replica_free(rep); @@ -24,7 +24,7 @@ static void test_replica_undo_empty(void) { TCReplica *rep = tc_replica_new_in_memory(); TEST_ASSERT_NULL(tc_replica_error(rep)); int rv = tc_replica_undo(rep); - TEST_ASSERT_EQUAL(0, rv); + TEST_ASSERT_EQUAL(TC_RESULT_FALSE, rv); TEST_ASSERT_NULL(tc_replica_error(rep)); tc_replica_free(rep); } diff --git a/integration-tests/src/bindings_tests/string.c b/integration-tests/src/bindings_tests/string.c index ad6c62559..9926d79bd 100644 --- a/integration-tests/src/bindings_tests/string.c +++ b/integration-tests/src/bindings_tests/string.c @@ -5,7 +5,7 @@ // creating strings does not crash static void test_string_creation(void) { - TCString *s = tc_string_new("abcdef"); + TCString *s = tc_string_borrow("abcdef"); tc_string_free(s); } @@ -22,7 +22,7 @@ static void test_string_cloning(void) { // borrowed strings echo back their content static void test_string_borrowed_strings_echo(void) { - TCString *s = tc_string_new("abcdef"); + TCString *s = tc_string_borrow("abcdef"); TEST_ASSERT_NOT_NULL(s); TEST_ASSERT_EQUAL_STRING("abcdef", tc_string_content(s)); diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 9f77b8170..b0bbc4117 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -11,7 +11,7 @@ static void test_task_creation(void) { TCTask *task = tc_replica_new_task( rep, TC_STATUS_PENDING, - tc_string_new("my task")); + tc_string_borrow("my task")); TEST_ASSERT_NOT_NULL(task); TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task)); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index a97d0f732..649febbd9 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,4 +1,5 @@ pub mod replica; +pub mod result; pub mod status; pub mod string; pub mod task; diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 08bab398f..7ae4cc286 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -1,4 +1,4 @@ -use crate::{status::TCStatus, string::TCString, task::TCTask}; +use crate::{result::TCResult, status::TCStatus, string::TCString, task::TCTask}; use taskchampion::{Replica, StorageConfig}; /// A replica represents an instance of a user's task data, providing an easy interface @@ -114,11 +114,21 @@ pub extern "C" fn tc_replica_new_task<'a>( /// Undo local operations until the most recent UndoPoint. /// -/// Returns -1 on error, 0 if there are no local operations to undo, and 1 if operations were -/// undone. +/// Returns TC_RESULT_TRUE if an undo occurred, TC_RESULT_FALSE if there are no operations +/// to be undone, or TC_RESULT_ERROR on error. #[no_mangle] -pub extern "C" fn tc_replica_undo<'a>(rep: *mut TCReplica) -> i32 { - wrap(rep, |rep| Ok(if rep.undo()? { 1 } else { 0 }), -1) +pub extern "C" fn tc_replica_undo<'a>(rep: *mut TCReplica) -> TCResult { + wrap( + rep, + |rep| { + Ok(if rep.undo()? { + TCResult::True + } else { + TCResult::False + }) + }, + TCResult::Error, + ) } /// Get the latest error for a replica, or NULL if the last operation succeeded. diff --git a/lib/src/result.rs b/lib/src/result.rs new file mode 100644 index 000000000..e807e4690 --- /dev/null +++ b/lib/src/result.rs @@ -0,0 +1,10 @@ +/// A result combines a boolean success value with +/// an error response. It is equivalent to `Result`. +/// cbindgen:prefix-with-name +/// cbindgen:rename-all=ScreamingSnakeCase +#[repr(C)] +pub enum TCResult { + True, + False, + Error, +} diff --git a/lib/src/string.rs b/lib/src/string.rs index 1816d2a7d..1cb716b62 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -2,11 +2,20 @@ use std::ffi::{CStr, CString, OsStr}; use std::os::unix::ffi::OsStrExt; use std::path::PathBuf; -/// TCString supports passing strings into and out of the TaskChampion API. +// TODO: is utf-8-ness always checked? (no) when? + +/// TCString supports passing strings into and out of the TaskChampion API. A string must contain +/// valid UTF-8, and can contain embedded NUL characters. Strings containing such embedded NULs +/// cannot be represented as a "C string" and must be accessed using `tc_string_content_and_len` +/// and `tc_string_clone_with_len`. In general, these two functions should be used for handling +/// arbitrary data, while more convenient forms may be used where embedded NUL characters are +/// impossible, such as in static strings. /// -/// Unless specified otherwise, functions in this API take ownership of a TCString when it appears -/// as a function argument, and transfer ownership to the caller when the TCString appears as a -/// return value or output argument. +/// Unless specified otherwise, functions in this API take ownership of a TCString when it is given +/// as a function argument, and free the string before returning. Thus the following is valid: +/// +/// When a TCString appears as a return value or output argument, it is the responsibility of the +/// caller to free the string. pub enum TCString<'a> { CString(CString), CStr(&'a CStr), @@ -82,13 +91,24 @@ impl<'a> From<&str> for TCString<'a> { /// Create a new TCString referencing the given C string. The C string must remain valid until /// after the TCString is freed. It's typically easiest to ensure this by using a static string. +/// +/// NOTE: this function does _not_ take responsibility for freeing the C string itself. +/// The underlying string once the TCString has been freed. Among other times, TCStrings are +/// freed when they are passed to API functions (unless documented otherwise). For example: +/// +/// ``` +/// char *url = get_item_url(..); // dynamically allocate C string +/// tc_task_annotate(task, tc_string_borrow(url)); // TCString created, passed, and freed +/// free(url); // string is no longer referenced and can be freed +/// ``` #[no_mangle] -pub extern "C" fn tc_string_new(cstr: *const libc::c_char) -> *mut TCString<'static> { +pub extern "C" fn tc_string_borrow(cstr: *const libc::c_char) -> *mut TCString<'static> { let cstr: &CStr = unsafe { CStr::from_ptr(cstr) }; TCString::CStr(cstr).return_val() } -/// Create a new TCString by cloning the content of the given C string. +/// Create a new TCString by cloning the content of the given C string. The resulting TCString +/// is independent of the given string, which can be freed or overwritten immediately. #[no_mangle] pub extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> *mut TCString<'static> { let cstr: &CStr = unsafe { CStr::from_ptr(cstr) }; @@ -96,8 +116,9 @@ pub extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> *mut TCString<'s } /// Create a new TCString containing the given string with the given length. This allows creation -/// of strings containing embedded NUL characters. If the given string is not valid UTF-8, this -/// function will return NULL. +/// of strings containing embedded NUL characters. As with `tc_string_clone`, the resulting +/// TCString is independent of the passed buffer, which may be reused or freed immediately. If the +/// given string is not valid UTF-8, this function will return NULL. #[no_mangle] pub extern "C" fn tc_string_clone_with_len( buf: *const libc::c_char, diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 02e2284fd..9e3354c14 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -1,6 +1,16 @@ #include #include +/** + * A result combines a boolean success value with + * an error response. It is equivalent to `Result`. + */ +typedef enum TCResult { + TC_RESULT_TRUE, + TC_RESULT_FALSE, + TC_RESULT_ERROR, +} TCResult; + /** * The status of a task, as defined by the task data model. */ @@ -24,11 +34,18 @@ typedef enum TCStatus { typedef struct TCReplica TCReplica; /** - * TCString supports passing strings into and out of the TaskChampion API. + * TCString supports passing strings into and out of the TaskChampion API. A string must contain + * valid UTF-8, and can contain embedded NUL characters. Strings containing such embedded NULs + * cannot be represented as a "C string" and must be accessed using `tc_string_content_and_len` + * and `tc_string_clone_with_len`. In general, these two functions should be used for handling + * arbitrary data, while more convenient forms may be used where embedded NUL characters are + * impossible, such as in static strings. * - * Unless specified otherwise, functions in this API take ownership of a TCString when it appears - * as a function argument, and transfer ownership to the caller when the TCString appears as a - * return value or output argument. + * Unless specified otherwise, functions in this API take ownership of a TCString when it is given + * as a function argument, and free the string before returning. Thus the following is valid: + * + * When a TCString appears as a return value or output argument, it is the responsibility of the + * caller to free the string. */ typedef struct TCString TCString; @@ -79,10 +96,10 @@ struct TCTask *tc_replica_new_task(struct TCReplica *rep, /** * Undo local operations until the most recent UndoPoint. * - * Returns -1 on error, 0 if there are no local operations to undo, and 1 if operations were - * undone. + * Returns TC_RESULT_TRUE if an undo occurred, TC_RESULT_FALSE if there are no operations + * to be undone, or TC_RESULT_ERROR on error. */ -int32_t tc_replica_undo(struct TCReplica *rep); +enum TCResult tc_replica_undo(struct TCReplica *rep); /** * Get the latest error for a replica, or NULL if the last operation succeeded. @@ -99,18 +116,30 @@ void tc_replica_free(struct TCReplica *rep); /** * Create a new TCString referencing the given C string. The C string must remain valid until * after the TCString is freed. It's typically easiest to ensure this by using a static string. + * + * NOTE: this function does _not_ take responsibility for freeing the C string itself. + * The underlying string once the TCString has been freed. Among other times, TCStrings are + * freed when they are passed to API functions (unless documented otherwise). For example: + * + * ``` + * char *url = get_item_url(..); // dynamically allocate C string + * tc_task_annotate(task, tc_string_borrow(url)); // TCString created, passed, and freed + * free(url); // string is no longer referenced and can be freed + * ``` */ -struct TCString *tc_string_new(const char *cstr); +struct TCString *tc_string_borrow(const char *cstr); /** - * Create a new TCString by cloning the content of the given C string. + * Create a new TCString by cloning the content of the given C string. The resulting TCString + * is independent of the given string, which can be freed or overwritten immediately. */ struct TCString *tc_string_clone(const char *cstr); /** * Create a new TCString containing the given string with the given length. This allows creation - * of strings containing embedded NUL characters. If the given string is not valid UTF-8, this - * function will return NULL. + * of strings containing embedded NUL characters. As with `tc_string_clone`, the resulting + * TCString is independent of the passed buffer, which may be reused or freed immediately. If the + * given string is not valid UTF-8, this function will return NULL. */ struct TCString *tc_string_clone_with_len(const char *buf, size_t len); From c5ff2398f79c9abcc7f47eb2d73c456c65e7b557 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 26 Jan 2022 00:35:03 +0000 Subject: [PATCH 14/95] ignore test-db --- integration-tests/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 integration-tests/.gitignore diff --git a/integration-tests/.gitignore b/integration-tests/.gitignore new file mode 100644 index 000000000..e525de89a --- /dev/null +++ b/integration-tests/.gitignore @@ -0,0 +1 @@ +test-db From f8cffb798c963a92fa4e19d59343ab18358db38c Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 26 Jan 2022 00:36:33 +0000 Subject: [PATCH 15/95] fix confusing doc string --- lib/src/string.rs | 7 ++++--- lib/taskchampion.h | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/src/string.rs b/lib/src/string.rs index 1cb716b62..1e3968004 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -92,9 +92,10 @@ impl<'a> From<&str> for TCString<'a> { /// Create a new TCString referencing the given C string. The C string must remain valid until /// after the TCString is freed. It's typically easiest to ensure this by using a static string. /// -/// NOTE: this function does _not_ take responsibility for freeing the C string itself. -/// The underlying string once the TCString has been freed. Among other times, TCStrings are -/// freed when they are passed to API functions (unless documented otherwise). For example: +/// NOTE: this function does _not_ take responsibility for freeing the C string itself. The +/// underlying string can be freed once the TCString referencing it has been freed. +/// +/// For example: /// /// ``` /// char *url = get_item_url(..); // dynamically allocate C string diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 9e3354c14..786ead2c0 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -117,9 +117,10 @@ void tc_replica_free(struct TCReplica *rep); * Create a new TCString referencing the given C string. The C string must remain valid until * after the TCString is freed. It's typically easiest to ensure this by using a static string. * - * NOTE: this function does _not_ take responsibility for freeing the C string itself. - * The underlying string once the TCString has been freed. Among other times, TCStrings are - * freed when they are passed to API functions (unless documented otherwise). For example: + * NOTE: this function does _not_ take responsibility for freeing the C string itself. The + * underlying string can be freed once the TCString referencing it has been freed. + * + * For example: * * ``` * char *url = get_item_url(..); // dynamically allocate C string From dd87f7da1ed1f943a86beb58a296f5dce10c89ce Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 26 Jan 2022 00:56:48 +0000 Subject: [PATCH 16/95] simplify defining suites --- integration-tests/README.md | 6 +-- integration-tests/build.rs | 46 ++++++++++++++------- integration-tests/src/bindings_tests/mod.rs | 6 +-- integration-tests/tests/bindings.rs | 7 +--- 4 files changed, 34 insertions(+), 31 deletions(-) diff --git a/integration-tests/README.md b/integration-tests/README.md index 21ca29c3b..b0a94d050 100644 --- a/integration-tests/README.md +++ b/integration-tests/README.md @@ -26,7 +26,5 @@ Keep the `RUN_TEST`s in the same order as the functions they call. To add a suite, -1. Add a new C file in `integration-tests/src/bindings_tests/`. -1. Add a new `.file(..)` to build that file in `integration-tests/build.rs`. -1. Add a `suite!(..)` to `integration-tests/src/bindings_tests/mod.rs`. -1. Add a `suite!(..)` to `integration-tests/tests/bindings.rs`. +1. Add a new C file in `integration-tests/src/bindings_tests/`, based off of one of hte others. +1. Add a the suite name to `suites` in `integration-tests/build.rs`. diff --git a/integration-tests/build.rs b/integration-tests/build.rs index 78c531024..04cb6b85d 100644 --- a/integration-tests/build.rs +++ b/integration-tests/build.rs @@ -1,36 +1,50 @@ -fn main() { +use std::env; +use std::fs; +use std::path::Path; + +fn build_libtaskchampion(suites: &[&'static str]) { // This crate has taskchampion-lib in its build-dependencies, so // libtaskchampion.so should be built already. Hopefully it's in target/$PROFILE, and hopefully // it's named libtaskchampion.so and not something else - let mut libtaskchampion = std::env::current_dir().unwrap(); + let mut libtaskchampion = env::current_dir().unwrap(); libtaskchampion.pop(); libtaskchampion.push("target"); - libtaskchampion.push(std::env::var("PROFILE").unwrap()); + libtaskchampion.push(env::var("PROFILE").unwrap()); libtaskchampion.push("deps"); libtaskchampion.push("libtaskchampion.so"); - println!("cargo:rerun-if-changed=build.rs"); - let mut build = cc::Build::new(); build.object(libtaskchampion); build.include("../lib"); build.include("src/bindings_tests/unity"); build.file("src/bindings_tests/unity/unity.c"); - let files = &[ - "src/bindings_tests/test.c", - // keep this list in sync with integration-tests/src/bindings_tests/mod.rs and - // integration-tests/tests/bindings.rs - "src/bindings_tests/uuid.c", - "src/bindings_tests/string.c", - "src/bindings_tests/task.c", - "src/bindings_tests/replica.c", - ]; - + let mut files = vec!["src/bindings_tests/test.c".to_string()]; + for suite in suites { + files.push(format!("src/bindings_tests/{}.c", suite)); + } for file in files { - build.file(file); + build.file(&file); println!("cargo:rerun-if-changed={}", file); } build.compile("bindings-tests"); } + +fn make_suite_file(suites: &[&'static str]) { + let out_dir = env::var_os("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("bindings_test_suites.rs"); + let mut content = String::new(); + for suite in suites { + content.push_str(format!("suite!({}_tests);\n", suite).as_ref()); + } + fs::write(&dest_path, content).unwrap(); +} + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + + let suites = &["uuid", "string", "task", "replica"]; + build_libtaskchampion(suites); + make_suite_file(suites); +} diff --git a/integration-tests/src/bindings_tests/mod.rs b/integration-tests/src/bindings_tests/mod.rs index a9cf8119e..4eba5b421 100644 --- a/integration-tests/src/bindings_tests/mod.rs +++ b/integration-tests/src/bindings_tests/mod.rs @@ -13,8 +13,4 @@ macro_rules! suite( }; ); -// keep this list in sync with integration-tests/build.rs and integration-tests/tests/bindings.rs. -suite!(uuid_tests); -suite!(string_tests); -suite!(task_tests); -suite!(replica_tests); +include!(concat!(env!("OUT_DIR"), "/bindings_test_suites.rs")); diff --git a/integration-tests/tests/bindings.rs b/integration-tests/tests/bindings.rs index b4df3a85a..d4574c92f 100644 --- a/integration-tests/tests/bindings.rs +++ b/integration-tests/tests/bindings.rs @@ -7,9 +7,4 @@ macro_rules! suite( }; ); -// keep this list in sync with integration-tests/build.rs and -// integration-tests/src/bindings_tests/mod.rs -suite!(uuid_tests); -suite!(string_tests); -suite!(task_tests); -suite!(replica_tests); +include!(concat!(env!("OUT_DIR"), "/bindings_test_suites.rs")); From 8f703fd63aa562ee96794e71352c515552183416 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 26 Jan 2022 01:29:16 +0000 Subject: [PATCH 17/95] use TCString in UUIDs --- integration-tests/src/bindings_tests/uuid.c | 37 ++++++++++++++++----- lib/src/replica.rs | 16 ++++----- lib/src/uuid.rs | 29 +++++++++++----- lib/taskchampion.h | 10 ++++-- 4 files changed, 63 insertions(+), 29 deletions(-) diff --git a/integration-tests/src/bindings_tests/uuid.c b/integration-tests/src/bindings_tests/uuid.c index 869d0cc17..2e8d0c806 100644 --- a/integration-tests/src/bindings_tests/uuid.c +++ b/integration-tests/src/bindings_tests/uuid.c @@ -8,36 +8,57 @@ static void test_uuid_creation(void) { tc_uuid_nil(); } -// converting UUIDs from string works -static void test_uuid_conversion_to_string(void) { +// converting UUIDs to a buf works +static void test_uuid_to_buf(void) { TEST_ASSERT_EQUAL(TC_UUID_STRING_BYTES, 36); TCUuid u2 = tc_uuid_nil(); char u2str[TC_UUID_STRING_BYTES]; - tc_uuid_to_str(u2, u2str); + tc_uuid_to_buf(u2, u2str); TEST_ASSERT_EQUAL_MEMORY("00000000-0000-0000-0000-000000000000", u2str, TC_UUID_STRING_BYTES); } +// converting UUIDs to a buf works +static void test_uuid_to_str(void) { + TCUuid u = tc_uuid_nil(); + TCString *s = tc_uuid_to_str(u); + TEST_ASSERT_EQUAL_STRING( + "00000000-0000-0000-0000-000000000000", + tc_string_content(s)); + tc_string_free(s); +} + +// converting valid UUIDs from string works +static void test_uuid_valid_from_str(void) { + TCUuid u; + char *ustr = "23cb25e0-5d1a-4932-8131-594ac6d3a843"; + TEST_ASSERT_TRUE(tc_uuid_from_str(tc_string_borrow(ustr), &u)); + TEST_ASSERT_EQUAL(0x23, u.bytes[0]); + TEST_ASSERT_EQUAL(0x43, u.bytes[15]); +} + // converting invalid UUIDs from string fails as expected static void test_uuid_invalid_string_fails(void) { TCUuid u; - char ustr[36] = "not-a-valid-uuid"; - TEST_ASSERT_FALSE(tc_uuid_from_str(ustr, &u)); + char *ustr = "not-a-valid-uuid"; + TEST_ASSERT_FALSE(tc_uuid_from_str(tc_string_borrow(ustr), &u)); } // converting invalid UTF-8 UUIDs from string fails as expected static void test_uuid_bad_utf8(void) { TCUuid u; - char ustr[36] = "\xf0\x28\x8c\xbc"; - TEST_ASSERT_FALSE(tc_uuid_from_str(ustr, &u)); + char *ustr = "\xf0\x28\x8c\xbc"; + TEST_ASSERT_FALSE(tc_uuid_from_str(tc_string_borrow(ustr), &u)); } int uuid_tests(void) { UNITY_BEGIN(); // each test case above should be named here, in order. RUN_TEST(test_uuid_creation); - RUN_TEST(test_uuid_conversion_to_string); + RUN_TEST(test_uuid_valid_from_str); + RUN_TEST(test_uuid_to_buf); + RUN_TEST(test_uuid_to_str); RUN_TEST(test_uuid_invalid_string_fails); RUN_TEST(test_uuid_bad_utf8); return UNITY_END(); diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 7ae4cc286..3d2eafd2f 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -80,13 +80,10 @@ pub extern "C" fn tc_replica_new_on_disk<'a>( })) } -/* - * TODO: - * - tc_replica_all_tasks - * - tc_replica_all_task_uuids - * - tc_replica_working_set - * - tc_replica_get_task - */ +// TODO: tc_replica_all_tasks +// TODO: tc_replica_all_task_uuids +// TODO: tc_replica_working_set +// TODO: tc_replica_get_task /// Create a new task. The task must not already exist. /// @@ -108,9 +105,8 @@ pub extern "C" fn tc_replica_new_task<'a>( ) } -/* - tc_replica_import_task_with_uuid - * - tc_replica_sync - */ +// TODO: tc_replica_import_task_with_uuid +// TODO: tc_replica_sync /// Undo local operations until the most recent UndoPoint. /// diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index 5e3492bea..f25399909 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -1,3 +1,4 @@ +use crate::string::TCString; use libc; use taskchampion::Uuid; @@ -33,31 +34,41 @@ pub extern "C" fn tc_uuid_nil() -> TCUuid { } /// Length, in bytes, of a C string containing a TCUuid. +// TODO: why not a const? #[no_mangle] pub static TC_UUID_STRING_BYTES: usize = ::uuid::adapter::Hyphenated::LENGTH; /// Write the string representation of a TCUuid into the given buffer, which must be /// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. #[no_mangle] -pub extern "C" fn tc_uuid_to_str<'a>(uuid: TCUuid, out: *mut libc::c_char) { - debug_assert!(!out.is_null()); +pub extern "C" fn tc_uuid_to_buf<'a>(uuid: TCUuid, buf: *mut libc::c_char) { + debug_assert!(!buf.is_null()); let buf: &'a mut [u8] = unsafe { - std::slice::from_raw_parts_mut(out as *mut u8, ::uuid::adapter::Hyphenated::LENGTH) + std::slice::from_raw_parts_mut(buf as *mut u8, ::uuid::adapter::Hyphenated::LENGTH) }; let uuid: Uuid = uuid.into(); uuid.to_hyphenated().encode_lower(buf); } +/// Write the string representation of a TCUuid into the given buffer, which must be +/// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. +#[no_mangle] +pub extern "C" fn tc_uuid_to_str(uuid: TCUuid) -> *mut TCString<'static> { + let uuid: Uuid = uuid.into(); + let s = uuid.to_string(); + TCString::from(s).return_val() +} + /// Parse the given value as a UUID. The value must be exactly TC_UUID_STRING_BYTES long. Returns /// false on failure. #[no_mangle] -pub extern "C" fn tc_uuid_from_str<'a>(val: *const libc::c_char, out: *mut TCUuid) -> bool { - debug_assert!(!val.is_null()); - debug_assert!(!out.is_null()); - let slice = unsafe { std::slice::from_raw_parts(val as *const u8, TC_UUID_STRING_BYTES) }; - if let Ok(s) = std::str::from_utf8(slice) { +pub extern "C" fn tc_uuid_from_str<'a>(s: *mut TCString, uuid_out: *mut TCUuid) -> bool { + debug_assert!(!s.is_null()); + debug_assert!(!uuid_out.is_null()); + let s = TCString::from_arg(s); + if let Ok(s) = s.as_str() { if let Ok(u) = Uuid::parse_str(s) { - unsafe { *out = u.into() }; + unsafe { *uuid_out = u.into() }; return true; } } diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 786ead2c0..b09a8f4d5 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -202,13 +202,19 @@ struct TCUuid tc_uuid_nil(void); * Write the string representation of a TCUuid into the given buffer, which must be * at least TC_UUID_STRING_BYTES long. No NUL terminator is added. */ -void tc_uuid_to_str(struct TCUuid uuid, char *out); +void tc_uuid_to_buf(struct TCUuid uuid, char *buf); + +/** + * Write the string representation of a TCUuid into the given buffer, which must be + * at least TC_UUID_STRING_BYTES long. No NUL terminator is added. + */ +struct TCString *tc_uuid_to_str(struct TCUuid uuid); /** * Parse the given value as a UUID. The value must be exactly TC_UUID_STRING_BYTES long. Returns * false on failure. */ -bool tc_uuid_from_str(const char *val, struct TCUuid *out); +bool tc_uuid_from_str(struct TCString *s, struct TCUuid *uuid_out); #ifdef __cplusplus } // extern "C" From 96b59dd5b2bd99cb1fe05463ec17c46492e7c5e6 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 26 Jan 2022 01:38:25 +0000 Subject: [PATCH 18/95] serialize C integration tests --- Cargo.lock | 1 + integration-tests/Cargo.toml | 1 + integration-tests/tests/bindings.rs | 10 ++++++++++ 3 files changed, 12 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index b2b905807..094b83445 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1529,6 +1529,7 @@ dependencies = [ "anyhow", "cc", "env_logger 0.8.4", + "lazy_static", "log", "pretty_assertions", "taskchampion", diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 71f39b6e6..2b4a3bd8a 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -18,6 +18,7 @@ tempfile = "3" pretty_assertions = "1" log = "^0.4.14" env_logger = "^0.8.3" +lazy_static = "1" [build-dependencies] cc = "1.0" diff --git a/integration-tests/tests/bindings.rs b/integration-tests/tests/bindings.rs index d4574c92f..9f14ec47f 100644 --- a/integration-tests/tests/bindings.rs +++ b/integration-tests/tests/bindings.rs @@ -1,7 +1,17 @@ +use lazy_static::lazy_static; +use std::sync::Mutex; + +lazy_static! { + // the C library running the tests is not reentrant, so we use a mutex to ensure that only one + // test runs at a time. + static ref MUTEX: Mutex<()> = Mutex::new(()); +} + macro_rules! suite( { $s:ident } => { #[test] fn $s() { + let _guard = MUTEX.lock().unwrap(); assert_eq!(integration_tests::bindings_tests::$s(), 0); } }; From f3b73ca0e4f120df773a69ccc7d8d6515ac62159 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 26 Jan 2022 01:49:06 +0000 Subject: [PATCH 19/95] add task_import_with_uuid --- integration-tests/src/bindings_tests/task.c | 23 ++++++++++++++++++ lib/src/replica.rs | 26 +++++++++++++++++---- lib/taskchampion.h | 7 ++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index b0bbc4117..bd9e14f4b 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -26,9 +26,32 @@ static void test_task_creation(void) { tc_replica_free(rep); } +// importing a task succeeds and the resulting task looks good +static void test_task_import(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCUuid uuid; + TEST_ASSERT_TRUE(tc_uuid_from_str(tc_string_borrow("23cb25e0-5d1a-4932-8131-594ac6d3a843"), &uuid)); + TCTask *task = tc_replica_import_task_with_uuid(rep, uuid); + TEST_ASSERT_NOT_NULL(task); + + TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task)); + + TCString *desc = tc_task_get_description(task); + TEST_ASSERT_NOT_NULL(desc); + TEST_ASSERT_EQUAL_STRING("", tc_string_content(desc)); // default value + tc_string_free(desc); + + tc_task_free(task); + + tc_replica_free(rep); +} + int task_tests(void) { UNITY_BEGIN(); // each test case above should be named here, in order. RUN_TEST(test_task_creation); + RUN_TEST(test_task_import); return UNITY_END(); } diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 3d2eafd2f..f77612b41 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -1,5 +1,5 @@ -use crate::{result::TCResult, status::TCStatus, string::TCString, task::TCTask}; -use taskchampion::{Replica, StorageConfig}; +use crate::{result::TCResult, status::TCStatus, string::TCString, task::TCTask, uuid::TCUuid}; +use taskchampion::{Replica, StorageConfig, Uuid}; /// A replica represents an instance of a user's task data, providing an easy interface /// for querying and modifying that data. @@ -89,7 +89,7 @@ pub extern "C" fn tc_replica_new_on_disk<'a>( /// /// Returns the task, or NULL on error. #[no_mangle] -pub extern "C" fn tc_replica_new_task<'a>( +pub extern "C" fn tc_replica_new_task( rep: *mut TCReplica, status: TCStatus, description: *mut TCString, @@ -105,7 +105,25 @@ pub extern "C" fn tc_replica_new_task<'a>( ) } -// TODO: tc_replica_import_task_with_uuid +/// Create a new task. The task must not already exist. +/// +/// Returns the task, or NULL on error. +#[no_mangle] +pub extern "C" fn tc_replica_import_task_with_uuid( + rep: *mut TCReplica, + uuid: TCUuid, +) -> *mut TCTask { + wrap( + rep, + |rep| { + let uuid: Uuid = uuid.into(); + let task = rep.import_task_with_uuid(uuid)?; + Ok(TCTask::as_ptr(task)) + }, + std::ptr::null_mut(), + ) +} + // TODO: tc_replica_sync /// Undo local operations until the most recent UndoPoint. diff --git a/lib/taskchampion.h b/lib/taskchampion.h index b09a8f4d5..a82ef49a3 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -93,6 +93,13 @@ struct TCTask *tc_replica_new_task(struct TCReplica *rep, enum TCStatus status, struct TCString *description); +/** + * Create a new task. The task must not already exist. + * + * Returns the task, or NULL on error. + */ +struct TCTask *tc_replica_import_task_with_uuid(struct TCReplica *rep, struct TCUuid uuid); + /** * Undo local operations until the most recent UndoPoint. * From e1c348b96e368df0b566292fc3560e3d4b5e70c6 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 26 Jan 2022 02:15:52 +0000 Subject: [PATCH 20/95] tc_replica_get_task --- .../src/bindings_tests/replica.c | 78 +++++++++++++++++++ integration-tests/src/bindings_tests/task.c | 23 ------ lib/src/replica.rs | 22 +++++- lib/taskchampion.h | 8 ++ 4 files changed, 107 insertions(+), 24 deletions(-) diff --git a/integration-tests/src/bindings_tests/replica.c b/integration-tests/src/bindings_tests/replica.c index 613b15666..756c9acfe 100644 --- a/integration-tests/src/bindings_tests/replica.c +++ b/integration-tests/src/bindings_tests/replica.c @@ -29,11 +29,89 @@ static void test_replica_undo_empty(void) { tc_replica_free(rep); } +// creating a task succeeds and the resulting task looks good +static void test_replica_task_creation(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + TCUuid uuid = tc_task_get_uuid(task); + TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task)); + + TCString *desc = tc_task_get_description(task); + TEST_ASSERT_NOT_NULL(desc); + TEST_ASSERT_EQUAL_STRING("my task", tc_string_content(desc)); + tc_string_free(desc); + + tc_task_free(task); + + // get the task again and verify it + task = tc_replica_get_task(rep, uuid); + TEST_ASSERT_NOT_NULL(task); + TEST_ASSERT_EQUAL_MEMORY(uuid.bytes, tc_task_get_uuid(task).bytes, sizeof(uuid.bytes)); + TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task)); + + tc_task_free(task); + + tc_replica_free(rep); +} + +// importing a task succeeds and the resulting task looks good +static void test_replica_task_import(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCUuid uuid; + TEST_ASSERT_TRUE(tc_uuid_from_str(tc_string_borrow("23cb25e0-5d1a-4932-8131-594ac6d3a843"), &uuid)); + TCTask *task = tc_replica_import_task_with_uuid(rep, uuid); + TEST_ASSERT_NOT_NULL(task); + + TEST_ASSERT_EQUAL_MEMORY(uuid.bytes, tc_task_get_uuid(task).bytes, sizeof(uuid.bytes)); + TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task)); + + TCString *desc = tc_task_get_description(task); + TEST_ASSERT_NOT_NULL(desc); + TEST_ASSERT_EQUAL_STRING("", tc_string_content(desc)); // default value + tc_string_free(desc); + + tc_task_free(task); + + // get the task again and verify it + task = tc_replica_get_task(rep, uuid); + TEST_ASSERT_NOT_NULL(task); + TEST_ASSERT_EQUAL_MEMORY(uuid.bytes, tc_task_get_uuid(task).bytes, sizeof(uuid.bytes)); + TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task)); + + tc_task_free(task); + + tc_replica_free(rep); +} + +// importing a task succeeds and the resulting task looks good +static void test_replica_get_task_not_found(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCUuid uuid; + TEST_ASSERT_TRUE(tc_uuid_from_str(tc_string_borrow("23cb25e0-5d1a-4932-8131-594ac6d3a843"), &uuid)); + TCTask *task = tc_replica_get_task(rep, uuid); + TEST_ASSERT_NULL(task); + TEST_ASSERT_NULL(tc_replica_error(rep)); +} + int replica_tests(void) { UNITY_BEGIN(); // each test case above should be named here, in order. RUN_TEST(test_replica_creation); RUN_TEST(test_replica_creation_disk); RUN_TEST(test_replica_undo_empty); + RUN_TEST(test_replica_task_creation); + RUN_TEST(test_replica_task_import); + RUN_TEST(test_replica_get_task_not_found); return UNITY_END(); } diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index bd9e14f4b..b0bbc4117 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -26,32 +26,9 @@ static void test_task_creation(void) { tc_replica_free(rep); } -// importing a task succeeds and the resulting task looks good -static void test_task_import(void) { - TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); - - TCUuid uuid; - TEST_ASSERT_TRUE(tc_uuid_from_str(tc_string_borrow("23cb25e0-5d1a-4932-8131-594ac6d3a843"), &uuid)); - TCTask *task = tc_replica_import_task_with_uuid(rep, uuid); - TEST_ASSERT_NOT_NULL(task); - - TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task)); - - TCString *desc = tc_task_get_description(task); - TEST_ASSERT_NOT_NULL(desc); - TEST_ASSERT_EQUAL_STRING("", tc_string_content(desc)); // default value - tc_string_free(desc); - - tc_task_free(task); - - tc_replica_free(rep); -} - int task_tests(void) { UNITY_BEGIN(); // each test case above should be named here, in order. RUN_TEST(test_task_creation); - RUN_TEST(test_task_import); return UNITY_END(); } diff --git a/lib/src/replica.rs b/lib/src/replica.rs index f77612b41..6bb89b209 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -27,6 +27,7 @@ where F: FnOnce(&mut Replica) -> anyhow::Result, { let rep: &'a mut TCReplica = rep_ref(rep); + rep.error = None; match f(&mut rep.inner) { Ok(v) => v, Err(e) => { @@ -83,7 +84,26 @@ pub extern "C" fn tc_replica_new_on_disk<'a>( // TODO: tc_replica_all_tasks // TODO: tc_replica_all_task_uuids // TODO: tc_replica_working_set -// TODO: tc_replica_get_task + +/// Get an existing task by its UUID. +/// +/// Returns NULL when the task does not exist, and on error. Consult tc_replica_error +/// to distinguish the two conditions. +#[no_mangle] +pub extern "C" fn tc_replica_get_task(rep: *mut TCReplica, uuid: TCUuid) -> *mut TCTask { + wrap( + rep, + |rep| { + let uuid: Uuid = uuid.into(); + if let Some(task) = rep.get_task(uuid)? { + Ok(TCTask::as_ptr(task)) + } else { + Ok(std::ptr::null_mut()) + } + }, + std::ptr::null_mut(), + ) +} /// Create a new task. The task must not already exist. /// diff --git a/lib/taskchampion.h b/lib/taskchampion.h index a82ef49a3..466e98994 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -84,6 +84,14 @@ struct TCReplica *tc_replica_new_in_memory(void); */ struct TCReplica *tc_replica_new_on_disk(struct TCString *path, struct TCString **error_out); +/** + * Get an existing task by its UUID. + * + * Returns NULL when the task does not exist, and on error. Consult tc_replica_error + * to distinguish the two conditions. + */ +struct TCTask *tc_replica_get_task(struct TCReplica *rep, struct TCUuid uuid); + /** * Create a new task. The task must not already exist. * From b5201a28c34b755be681c6f4072f5a648f00204d Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 26 Jan 2022 14:20:50 +0000 Subject: [PATCH 21/95] build bindings-test shared --- integration-tests/build.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/integration-tests/build.rs b/integration-tests/build.rs index 04cb6b85d..5182784b3 100644 --- a/integration-tests/build.rs +++ b/integration-tests/build.rs @@ -14,6 +14,7 @@ fn build_libtaskchampion(suites: &[&'static str]) { libtaskchampion.push("libtaskchampion.so"); let mut build = cc::Build::new(); + build.shared_flag(true); build.object(libtaskchampion); build.include("../lib"); build.include("src/bindings_tests/unity"); From 633ea5cf470e93622c5f9b1edb30c690944d42e7 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 27 Jan 2022 01:54:00 +0000 Subject: [PATCH 22/95] correctly handle invalid utf-8 --- integration-tests/src/bindings_tests/string.c | 39 +++++++++++- integration-tests/src/bindings_tests/uuid.c | 7 +++ lib/src/string.rs | 62 ++++++++++++------- lib/taskchampion.h | 31 ++++++---- 4 files changed, 104 insertions(+), 35 deletions(-) diff --git a/integration-tests/src/bindings_tests/string.c b/integration-tests/src/bindings_tests/string.c index 9926d79bd..0eebec8f7 100644 --- a/integration-tests/src/bindings_tests/string.c +++ b/integration-tests/src/bindings_tests/string.c @@ -20,6 +20,24 @@ static void test_string_cloning(void) { tc_string_free(s); } +// creating cloned strings with invalid utf-8 does not crash +// ..but content is NULL and content_and_len returns the value +static void test_string_cloning_invalid_utf8(void) { + TCString *s = tc_string_clone("\xf0\x28\x8c\x28"); + TEST_ASSERT_NOT_NULL(s); + + // NOTE: this is not one of the cases where invalid UTF-8 results in NULL, + // but that may change. + + size_t len; + const char *buf = tc_string_content_with_len(s, &len); + TEST_ASSERT_NOT_NULL(buf); + TEST_ASSERT_EQUAL(4, len); + TEST_ASSERT_EQUAL_MEMORY("\xf0\x28\x8c\x28", buf, len); + + tc_string_free(s); +} + // borrowed strings echo back their content static void test_string_borrowed_strings_echo(void) { TCString *s = tc_string_borrow("abcdef"); @@ -54,7 +72,8 @@ static void test_string_cloned_strings_echo(void) { tc_string_free(s); } -// tc_string_content returns NULL for strings containing embedded NULs +// tc_clone_with_len can have NULs, and tc_string_content returns NULL for +// strings containing embedded NULs static void test_string_content_null_for_embedded_nuls(void) { TCString *s = tc_string_clone_with_len("ab\0de", 5); TEST_ASSERT_NOT_NULL(s); @@ -69,13 +88,31 @@ static void test_string_content_null_for_embedded_nuls(void) { tc_string_free(s); } +// tc_string_clone_with_len will accept invalid utf-8, but then tc_string_content +// returns NULL. +static void test_string_clone_with_len_invalid_utf8(void) { + TCString *s = tc_string_clone_with_len("\xf0\x28\x8c\x28", 4); + TEST_ASSERT_NOT_NULL(s); + + TEST_ASSERT_NULL(tc_string_content(s)); + + size_t len; + const char *buf = tc_string_content_with_len(s, &len); + TEST_ASSERT_NOT_NULL(buf); + TEST_ASSERT_EQUAL(4, len); + TEST_ASSERT_EQUAL_MEMORY("\xf0\x28\x8c\x28", buf, len); + tc_string_free(s); +} + int string_tests(void) { UNITY_BEGIN(); // each test case above should be named here, in order. RUN_TEST(test_string_creation); RUN_TEST(test_string_cloning); + RUN_TEST(test_string_cloning_invalid_utf8); RUN_TEST(test_string_borrowed_strings_echo); RUN_TEST(test_string_cloned_strings_echo); RUN_TEST(test_string_content_null_for_embedded_nuls); + RUN_TEST(test_string_clone_with_len_invalid_utf8); return UNITY_END(); } diff --git a/integration-tests/src/bindings_tests/uuid.c b/integration-tests/src/bindings_tests/uuid.c index 2e8d0c806..572a85322 100644 --- a/integration-tests/src/bindings_tests/uuid.c +++ b/integration-tests/src/bindings_tests/uuid.c @@ -52,6 +52,12 @@ static void test_uuid_bad_utf8(void) { TEST_ASSERT_FALSE(tc_uuid_from_str(tc_string_borrow(ustr), &u)); } +// converting a string with embedded NUL fails as expected +static void test_uuid_embedded_nul(void) { + TCUuid u; + TEST_ASSERT_FALSE(tc_uuid_from_str(tc_string_clone_with_len("ab\0de", 5), &u)); +} + int uuid_tests(void) { UNITY_BEGIN(); // each test case above should be named here, in order. @@ -61,5 +67,6 @@ int uuid_tests(void) { RUN_TEST(test_uuid_to_str); RUN_TEST(test_uuid_invalid_string_fails); RUN_TEST(test_uuid_bad_utf8); + RUN_TEST(test_uuid_embedded_nul); return UNITY_END(); } diff --git a/lib/src/string.rs b/lib/src/string.rs index 1e3968004..1614a0c02 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -1,18 +1,20 @@ use std::ffi::{CStr, CString, OsStr}; use std::os::unix::ffi::OsStrExt; use std::path::PathBuf; +use std::str::Utf8Error; -// TODO: is utf-8-ness always checked? (no) when? - -/// TCString supports passing strings into and out of the TaskChampion API. A string must contain -/// valid UTF-8, and can contain embedded NUL characters. Strings containing such embedded NULs -/// cannot be represented as a "C string" and must be accessed using `tc_string_content_and_len` -/// and `tc_string_clone_with_len`. In general, these two functions should be used for handling -/// arbitrary data, while more convenient forms may be used where embedded NUL characters are -/// impossible, such as in static strings. +/// TCString supports passing strings into and out of the TaskChampion API. A string can contain +/// embedded NUL characters. Strings containing such embedded NULs cannot be represented as a "C +/// string" and must be accessed using `tc_string_content_and_len` and `tc_string_clone_with_len`. +/// In general, these two functions should be used for handling arbitrary data, while more +/// convenient forms may be used where embedded NUL characters are impossible, such as in static +/// strings. +/// +/// Rust expects all strings to be UTF-8, and API functions will fail if given a TCString +/// containing invalid UTF-8. /// /// Unless specified otherwise, functions in this API take ownership of a TCString when it is given -/// as a function argument, and free the string before returning. Thus the following is valid: +/// as a function argument, and free the string before returning. /// /// When a TCString appears as a return value or output argument, it is the responsibility of the /// caller to free the string. @@ -21,6 +23,10 @@ pub enum TCString<'a> { CStr(&'a CStr), String(String), + /// This variant denotes an input string that was not valid UTF-8. This allows reporting this + /// error when the string is read, with the constructor remaining infallible. + InvalidUtf8(Utf8Error, Vec), + /// None is the default value for TCString, but this variant is never seen by C code or by Rust /// code outside of this module. None, @@ -52,15 +58,17 @@ impl<'a> TCString<'a> { TCString::CString(cstring) => cstring.as_c_str().to_str(), TCString::CStr(cstr) => cstr.to_str(), TCString::String(string) => Ok(string.as_ref()), + TCString::InvalidUtf8(e, _) => Err(*e), TCString::None => unreachable!(), } } - pub(crate) fn as_bytes(&self) -> &[u8] { + fn as_bytes(&self) -> &[u8] { match self { TCString::CString(cstring) => cstring.as_bytes(), TCString::CStr(cstr) => cstr.to_bytes(), TCString::String(string) => string.as_bytes(), + TCString::InvalidUtf8(_, data) => data.as_ref(), TCString::None => unreachable!(), } } @@ -118,8 +126,7 @@ pub extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> *mut TCString<'s /// Create a new TCString containing the given string with the given length. This allows creation /// of strings containing embedded NUL characters. As with `tc_string_clone`, the resulting -/// TCString is independent of the passed buffer, which may be reused or freed immediately. If the -/// given string is not valid UTF-8, this function will return NULL. +/// TCString is independent of the passed buffer, which may be reused or freed immediately. #[no_mangle] pub extern "C" fn tc_string_clone_with_len( buf: *const libc::c_char, @@ -127,21 +134,30 @@ pub extern "C" fn tc_string_clone_with_len( ) -> *mut TCString<'static> { let slice = unsafe { std::slice::from_raw_parts(buf as *const u8, len) }; let vec = slice.to_vec(); - if let Ok(string) = String::from_utf8(vec) { - TCString::String(string).return_val() - } else { - std::ptr::null_mut() + // try converting to a string, which is the only variant that can contain embedded NULs. If + // the bytes are not valid utf-8, store that information for reporting later. + match String::from_utf8(vec) { + Ok(string) => TCString::String(string), + Err(e) => { + let (e, vec) = (e.utf8_error(), e.into_bytes()); + TCString::InvalidUtf8(e, vec) + } } + .return_val() } /// Get the content of the string as a regular C string. The given string must not be NULL. The -/// returned value is NULL if the string contains NUL bytes. The returned string is valid until -/// the TCString is freed or passed to another TC API function. +/// returned value is NULL if the string contains NUL bytes or (in some cases) invalid UTF-8. The +/// returned C string is valid until the TCString is freed or passed to another TC API function. +/// +/// In general, prefer [`tc_string_content_with_len`] except when it's certain that the string is +/// valid and NUL-free. /// /// This function does _not_ take ownership of the TCString. #[no_mangle] pub extern "C" fn tc_string_content(tcstring: *mut TCString) -> *const libc::c_char { let tcstring = TCString::from_arg_ref(tcstring); + // if we have a String, we need to consume it and turn it into // a CString. if matches!(tcstring, TCString::String(_)) { @@ -153,7 +169,7 @@ pub extern "C" fn tc_string_content(tcstring: *mut TCString) -> *const libc::c_c Err(nul_err) => { // recover the underlying String from the NulError let original_bytes = nul_err.into_vec(); - // SAFETY: original_bytes just came from a String, so must be valid utf8 + // SAFETY: original_bytes came from a String moments ago, so still valid utf8 let string = unsafe { String::from_utf8_unchecked(original_bytes) }; *tcstring = TCString::String(string); @@ -170,13 +186,14 @@ pub extern "C" fn tc_string_content(tcstring: *mut TCString) -> *const libc::c_c TCString::CString(cstring) => cstring.as_ptr(), TCString::String(_) => unreachable!(), // just converted to CString TCString::CStr(cstr) => cstr.as_ptr(), + TCString::InvalidUtf8(_, _) => std::ptr::null(), TCString::None => unreachable!(), } } /// Get the content of the string as a pointer and length. The given string must not be NULL. -/// This function can return any string, even one including NUL bytes. The returned string is -/// valid until the TCString is freed or passed to another TC API function. +/// This function can return any string, even one including NUL bytes or invalid UTF-8. The +/// returned buffer is valid until the TCString is freed or passed to another TC API function. /// /// This function does _not_ take ownership of the TCString. #[no_mangle] @@ -184,11 +201,14 @@ pub extern "C" fn tc_string_content_with_len( tcstring: *mut TCString, len_out: *mut usize, ) -> *const libc::c_char { + debug_assert!(!tcstring.is_null()); + debug_assert!(!len_out.is_null()); let tcstring = TCString::from_arg_ref(tcstring); let bytes = match tcstring { TCString::CString(cstring) => cstring.as_bytes(), TCString::String(string) => string.as_bytes(), TCString::CStr(cstr) => cstr.to_bytes(), + TCString::InvalidUtf8(_, ref v) => v.as_ref(), TCString::None => unreachable!(), }; unsafe { *len_out = bytes.len() }; diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 466e98994..bf746289c 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -34,15 +34,18 @@ typedef enum TCStatus { typedef struct TCReplica TCReplica; /** - * TCString supports passing strings into and out of the TaskChampion API. A string must contain - * valid UTF-8, and can contain embedded NUL characters. Strings containing such embedded NULs - * cannot be represented as a "C string" and must be accessed using `tc_string_content_and_len` - * and `tc_string_clone_with_len`. In general, these two functions should be used for handling - * arbitrary data, while more convenient forms may be used where embedded NUL characters are - * impossible, such as in static strings. + * TCString supports passing strings into and out of the TaskChampion API. A string can contain + * embedded NUL characters. Strings containing such embedded NULs cannot be represented as a "C + * string" and must be accessed using `tc_string_content_and_len` and `tc_string_clone_with_len`. + * In general, these two functions should be used for handling arbitrary data, while more + * convenient forms may be used where embedded NUL characters are impossible, such as in static + * strings. + * + * Rust expects all strings to be UTF-8, and API functions will fail if given a TCString + * containing invalid UTF-8. * * Unless specified otherwise, functions in this API take ownership of a TCString when it is given - * as a function argument, and free the string before returning. Thus the following is valid: + * as a function argument, and free the string before returning. * * When a TCString appears as a return value or output argument, it is the responsibility of the * caller to free the string. @@ -154,15 +157,17 @@ struct TCString *tc_string_clone(const char *cstr); /** * Create a new TCString containing the given string with the given length. This allows creation * of strings containing embedded NUL characters. As with `tc_string_clone`, the resulting - * TCString is independent of the passed buffer, which may be reused or freed immediately. If the - * given string is not valid UTF-8, this function will return NULL. + * TCString is independent of the passed buffer, which may be reused or freed immediately. */ struct TCString *tc_string_clone_with_len(const char *buf, size_t len); /** * Get the content of the string as a regular C string. The given string must not be NULL. The - * returned value is NULL if the string contains NUL bytes. The returned string is valid until - * the TCString is freed or passed to another TC API function. + * returned value is NULL if the string contains NUL bytes or (in some cases) invalid UTF-8. The + * returned C string is valid until the TCString is freed or passed to another TC API function. + * + * In general, prefer [`tc_string_content_with_len`] except when it's certain that the string is + * valid and NUL-free. * * This function does _not_ take ownership of the TCString. */ @@ -170,8 +175,8 @@ const char *tc_string_content(struct TCString *tcstring); /** * Get the content of the string as a pointer and length. The given string must not be NULL. - * This function can return any string, even one including NUL bytes. The returned string is - * valid until the TCString is freed or passed to another TC API function. + * This function can return any string, even one including NUL bytes or invalid UTF-8. The + * returned buffer is valid until the TCString is freed or passed to another TC API function. * * This function does _not_ take ownership of the TCString. */ From 1470bbf7418d59e4c3135354ed79ac4acd2f2850 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 27 Jan 2022 02:22:39 +0000 Subject: [PATCH 23/95] mark unsafe utils as such; add safety comments --- lib/src/replica.rs | 17 ++++++--- lib/src/string.rs | 89 ++++++++++++++++++++++++++++++++++++---------- lib/src/uuid.rs | 8 +++-- lib/taskchampion.h | 29 +++++++++------ 4 files changed, 107 insertions(+), 36 deletions(-) diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 6bb89b209..1d8014424 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -50,14 +50,18 @@ pub extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica { })) } -/// Create a new TCReplica with an on-disk database. On error, a string is written to the -/// `error_out` parameter (if it is not NULL) and NULL is returned. +/// Create a new TCReplica with an on-disk database having the given filename. The filename must +/// not be NULL. On error, a string is written to the `error_out` parameter (if it is not NULL) and +/// NULL is returned. #[no_mangle] pub extern "C" fn tc_replica_new_on_disk<'a>( path: *mut TCString, error_out: *mut *mut TCString, ) -> *mut TCReplica { - let path = TCString::from_arg(path); + // SAFETY: + // - tcstring is not NULL (promised by caller) + // - caller is exclusive owner of tcstring (implicitly promised by caller) + let path = unsafe { TCString::from_arg(path) }; let storage_res = StorageConfig::OnDisk { taskdb_dir: path.to_path_buf(), } @@ -107,6 +111,8 @@ pub extern "C" fn tc_replica_get_task(rep: *mut TCReplica, uuid: TCUuid) -> *mut /// Create a new task. The task must not already exist. /// +/// The description must not be NULL. +/// /// Returns the task, or NULL on error. #[no_mangle] pub extern "C" fn tc_replica_new_task( @@ -114,10 +120,13 @@ pub extern "C" fn tc_replica_new_task( status: TCStatus, description: *mut TCString, ) -> *mut TCTask { + // SAFETY: + // - tcstring is not NULL (promised by caller) + // - caller is exclusive owner of tcstring (implicitly promised by caller) + let description = unsafe { TCString::from_arg(description) }; wrap( rep, |rep| { - let description = TCString::from_arg(description); let task = rep.new_task(status.into(), description.as_str()?.to_string())?; Ok(TCTask::as_ptr(task)) }, diff --git a/lib/src/string.rs b/lib/src/string.rs index 1614a0c02..df0eebddb 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -14,7 +14,8 @@ use std::str::Utf8Error; /// containing invalid UTF-8. /// /// Unless specified otherwise, functions in this API take ownership of a TCString when it is given -/// as a function argument, and free the string before returning. +/// as a function argument, and free the string before returning. Callers must not use or free +/// strings after passing them to such API functions. /// /// When a TCString appears as a return value or output argument, it is the responsibility of the /// caller to free the string. @@ -39,17 +40,31 @@ impl<'a> Default for TCString<'a> { } impl<'a> TCString<'a> { - /// Take a TCString from C as an argument. C callers generally expect TC functions to take - /// ownership of a string, which is what this function does. - pub(crate) fn from_arg(tcstring: *mut TCString<'a>) -> Self { + /// Take a TCString from C as an argument. + /// + /// C callers generally expect TC functions to take ownership of a string, which is what this + /// function does. + /// + /// # Safety + /// + /// The pointer must not be NULL. It is the caller's responsibility to ensure that the + /// lifetime assigned to the reference and the lifetime of the TCString itself do not outlive + /// the lifetime promised by C. + pub(crate) unsafe fn from_arg(tcstring: *mut TCString<'a>) -> Self { debug_assert!(!tcstring.is_null()); - *(unsafe { Box::from_raw(tcstring) }) + *(Box::from_raw(tcstring)) } /// Borrow a TCString from C as an argument. - pub(crate) fn from_arg_ref(tcstring: *mut TCString<'a>) -> &'a mut Self { + /// + /// # Safety + /// + /// The pointer must not be NULL. It is the caller's responsibility to ensure that the + /// lifetime assigned to the reference and the lifetime of the TCString itself do not outlive + /// the lifetime promised by C. + pub(crate) unsafe fn from_arg_ref(tcstring: *mut TCString<'a>) -> &'a mut Self { debug_assert!(!tcstring.is_null()); - unsafe { &mut *tcstring } + &mut *tcstring } /// Get a regular Rust &str for this value. @@ -97,11 +112,12 @@ impl<'a> From<&str> for TCString<'a> { } } -/// Create a new TCString referencing the given C string. The C string must remain valid until -/// after the TCString is freed. It's typically easiest to ensure this by using a static string. +/// Create a new TCString referencing the given C string. The C string must remain valid and +/// unchanged until after the TCString is freed. It's typically easiest to ensure this by using a +/// static string. /// -/// NOTE: this function does _not_ take responsibility for freeing the C string itself. The -/// underlying string can be freed once the TCString referencing it has been freed. +/// NOTE: this function does _not_ take responsibility for freeing the given C string. The +/// given string can be freed once the TCString referencing it has been freed. /// /// For example: /// @@ -112,6 +128,12 @@ impl<'a> From<&str> for TCString<'a> { /// ``` #[no_mangle] pub extern "C" fn tc_string_borrow(cstr: *const libc::c_char) -> *mut TCString<'static> { + debug_assert!(!cstr.is_null()); + // SAFETY: + // - cstr is not NULL (promised by caller, verified by assertion) + // - cstr's lifetime exceeds that of the TCString (promised by caller) + // - cstr contains a valid NUL terminator (promised by caller) + // - cstr's content will not change before it is destroyed (promised by caller) let cstr: &CStr = unsafe { CStr::from_ptr(cstr) }; TCString::CStr(cstr).return_val() } @@ -120,6 +142,12 @@ pub extern "C" fn tc_string_borrow(cstr: *const libc::c_char) -> *mut TCString<' /// is independent of the given string, which can be freed or overwritten immediately. #[no_mangle] pub extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> *mut TCString<'static> { + debug_assert!(!cstr.is_null()); + // SAFETY: + // - cstr is not NULL (promised by caller, verified by assertion) + // - cstr's lifetime exceeds that of this function (by C convention) + // - cstr contains a valid NUL terminator (promised by caller) + // - cstr's content will not change before it is destroyed (by C convention) let cstr: &CStr = unsafe { CStr::from_ptr(cstr) }; TCString::CString(cstr.into()).return_val() } @@ -127,11 +155,21 @@ pub extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> *mut TCString<'s /// Create a new TCString containing the given string with the given length. This allows creation /// of strings containing embedded NUL characters. As with `tc_string_clone`, the resulting /// TCString is independent of the passed buffer, which may be reused or freed immediately. +/// +/// The given length must be less than half the maximum value of usize. #[no_mangle] pub extern "C" fn tc_string_clone_with_len( buf: *const libc::c_char, len: usize, ) -> *mut TCString<'static> { + debug_assert!(!buf.is_null()); + debug_assert!(len < isize::MAX as usize); + // SAFETY: + // - buf is valid for len bytes (by C convention) + // - (no alignment requirements for a byte slice) + // - content of buf will not be mutated during the lifetime of this slice (lifetime + // does not outlive this function call) + // - the length of the buffer is less than isize::MAX (promised by caller) let slice = unsafe { std::slice::from_raw_parts(buf as *const u8, len) }; let vec = slice.to_vec(); // try converting to a string, which is the only variant that can contain embedded NULs. If @@ -156,7 +194,11 @@ pub extern "C" fn tc_string_clone_with_len( /// This function does _not_ take ownership of the TCString. #[no_mangle] pub extern "C" fn tc_string_content(tcstring: *mut TCString) -> *const libc::c_char { - let tcstring = TCString::from_arg_ref(tcstring); + // SAFETY: + // - tcstring is not NULL (promised by caller) + // - lifetime of tcstring outlives the lifetime of this function + // - lifetime of tcstring outlives the lifetime of the returned pointer (promised by caller) + let tcstring = unsafe { TCString::from_arg_ref(tcstring) }; // if we have a String, we need to consume it and turn it into // a CString. @@ -201,9 +243,13 @@ pub extern "C" fn tc_string_content_with_len( tcstring: *mut TCString, len_out: *mut usize, ) -> *const libc::c_char { - debug_assert!(!tcstring.is_null()); + // SAFETY: + // - tcstring is not NULL (promised by caller) + // - lifetime of tcstring outlives the lifetime of this function + // - lifetime of tcstring outlives the lifetime of the returned pointer (promised by caller) + let tcstring = unsafe { TCString::from_arg_ref(tcstring) }; debug_assert!(!len_out.is_null()); - let tcstring = TCString::from_arg_ref(tcstring); + let bytes = match tcstring { TCString::CString(cstring) => cstring.as_bytes(), TCString::String(string) => string.as_bytes(), @@ -211,13 +257,20 @@ pub extern "C" fn tc_string_content_with_len( TCString::InvalidUtf8(_, ref v) => v.as_ref(), TCString::None => unreachable!(), }; + // SAFETY: + // - len_out is not NULL (checked by assertion, promised by caller) + // - len_out points to valid memory (promised by caller) + // - len_out is properly aligned (C convention) unsafe { *len_out = bytes.len() }; bytes.as_ptr() as *const libc::c_char } -/// Free a TCString. +/// Free a TCString. The given string must not be NULL. The string must not be used +/// after this function returns, and must not be freed more than once. #[no_mangle] -pub extern "C" fn tc_string_free(string: *mut TCString) { - debug_assert!(!string.is_null()); - drop(unsafe { Box::from_raw(string) }); +pub extern "C" fn tc_string_free(tcstring: *mut TCString) { + // SAFETY: + // - tcstring is not NULL (promised by caller) + // - caller is exclusive owner of tcstring (promised by caller) + drop(unsafe { TCString::from_arg(tcstring) }); } diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index f25399909..535c03086 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -59,13 +59,15 @@ pub extern "C" fn tc_uuid_to_str(uuid: TCUuid) -> *mut TCString<'static> { TCString::from(s).return_val() } -/// Parse the given value as a UUID. The value must be exactly TC_UUID_STRING_BYTES long. Returns -/// false on failure. +/// Parse the given string as a UUID. The string must not be NULL. Returns false on failure. #[no_mangle] pub extern "C" fn tc_uuid_from_str<'a>(s: *mut TCString, uuid_out: *mut TCUuid) -> bool { debug_assert!(!s.is_null()); debug_assert!(!uuid_out.is_null()); - let s = TCString::from_arg(s); + // SAFETY: + // - tcstring is not NULL (promised by caller) + // - caller is exclusive owner of tcstring (implicitly promised by caller) + let s = unsafe { TCString::from_arg(s) }; if let Ok(s) = s.as_str() { if let Ok(u) = Uuid::parse_str(s) { unsafe { *uuid_out = u.into() }; diff --git a/lib/taskchampion.h b/lib/taskchampion.h index bf746289c..070b366e0 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -45,7 +45,8 @@ typedef struct TCReplica TCReplica; * containing invalid UTF-8. * * Unless specified otherwise, functions in this API take ownership of a TCString when it is given - * as a function argument, and free the string before returning. + * as a function argument, and free the string before returning. Callers must not use or free + * strings after passing them to such API functions. * * When a TCString appears as a return value or output argument, it is the responsibility of the * caller to free the string. @@ -82,8 +83,9 @@ extern const size_t TC_UUID_STRING_BYTES; struct TCReplica *tc_replica_new_in_memory(void); /** - * Create a new TCReplica with an on-disk database. On error, a string is written to the - * `error_out` parameter (if it is not NULL) and NULL is returned. + * Create a new TCReplica with an on-disk database having the given filename. The filename must + * not be NULL. On error, a string is written to the `error_out` parameter (if it is not NULL) and + * NULL is returned. */ struct TCReplica *tc_replica_new_on_disk(struct TCString *path, struct TCString **error_out); @@ -98,6 +100,8 @@ struct TCTask *tc_replica_get_task(struct TCReplica *rep, struct TCUuid uuid); /** * Create a new task. The task must not already exist. * + * The description must not be NULL. + * * Returns the task, or NULL on error. */ struct TCTask *tc_replica_new_task(struct TCReplica *rep, @@ -132,11 +136,12 @@ struct TCString *tc_replica_error(struct TCReplica *rep); void tc_replica_free(struct TCReplica *rep); /** - * Create a new TCString referencing the given C string. The C string must remain valid until - * after the TCString is freed. It's typically easiest to ensure this by using a static string. + * Create a new TCString referencing the given C string. The C string must remain valid and + * unchanged until after the TCString is freed. It's typically easiest to ensure this by using a + * static string. * - * NOTE: this function does _not_ take responsibility for freeing the C string itself. The - * underlying string can be freed once the TCString referencing it has been freed. + * NOTE: this function does _not_ take responsibility for freeing the given C string. The + * given string can be freed once the TCString referencing it has been freed. * * For example: * @@ -158,6 +163,8 @@ struct TCString *tc_string_clone(const char *cstr); * Create a new TCString containing the given string with the given length. This allows creation * of strings containing embedded NUL characters. As with `tc_string_clone`, the resulting * TCString is independent of the passed buffer, which may be reused or freed immediately. + * + * The given length must be less than half the maximum value of usize. */ struct TCString *tc_string_clone_with_len(const char *buf, size_t len); @@ -183,9 +190,10 @@ const char *tc_string_content(struct TCString *tcstring); const char *tc_string_content_with_len(struct TCString *tcstring, size_t *len_out); /** - * Free a TCString. + * Free a TCString. The given string must not be NULL. The string must not be used + * after this function returns, and must not be freed more than once. */ -void tc_string_free(struct TCString *string); +void tc_string_free(struct TCString *tcstring); /** * Get a task's UUID. @@ -231,8 +239,7 @@ void tc_uuid_to_buf(struct TCUuid uuid, char *buf); struct TCString *tc_uuid_to_str(struct TCUuid uuid); /** - * Parse the given value as a UUID. The value must be exactly TC_UUID_STRING_BYTES long. Returns - * false on failure. + * Parse the given string as a UUID. The string must not be NULL. Returns false on failure. */ bool tc_uuid_from_str(struct TCString *s, struct TCUuid *uuid_out); From b3cbec1af3664d1a05a3d41d478fdf3f68d228e4 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Fri, 28 Jan 2022 02:11:13 +0000 Subject: [PATCH 24/95] more unsafe notations --- lib/src/replica.rs | 36 +++++++++++++++++++++++++----------- lib/src/task.rs | 43 ++++++++++++++++++++++++++++++++----------- lib/src/uuid.rs | 9 +++++++++ lib/taskchampion.h | 7 ++++--- 4 files changed, 70 insertions(+), 25 deletions(-) diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 1d8014424..47220ba7f 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -11,10 +11,18 @@ pub struct TCReplica { error: Option>, } -/// Utility function to safely convert *mut TCReplica into &mut TCReplica -fn rep_ref(rep: *mut TCReplica) -> &'static mut TCReplica { - debug_assert!(!rep.is_null()); - unsafe { &mut *rep } +impl TCReplica { + /// Borrow a TCReplica from C as an argument. + /// + /// # Safety + /// + /// The pointer must not be NULL. It is the caller's responsibility to ensure that the + /// lifetime assigned to the reference and the lifetime of the TCReplica itself do not outlive + /// the lifetime promised by C. + pub(crate) unsafe fn from_arg_ref<'a>(tcstring: *mut TCReplica) -> &'a mut Self { + debug_assert!(!tcstring.is_null()); + &mut *tcstring + } } fn err_to_tcstring(e: impl std::string::ToString) -> TCString<'static> { @@ -26,7 +34,10 @@ fn wrap<'a, T, F>(rep: *mut TCReplica, f: F, err_value: T) -> T where F: FnOnce(&mut Replica) -> anyhow::Result, { - let rep: &'a mut TCReplica = rep_ref(rep); + // SAFETY: + // - rep is not null (promised by caller) + // - rep outlives 'a (promised by caller) + let rep: &'a mut TCReplica = unsafe { TCReplica::from_arg_ref(rep) }; rep.error = None; match f(&mut rep.inner) { Ok(v) => v, @@ -100,7 +111,7 @@ pub extern "C" fn tc_replica_get_task(rep: *mut TCReplica, uuid: TCUuid) -> *mut |rep| { let uuid: Uuid = uuid.into(); if let Some(task) = rep.get_task(uuid)? { - Ok(TCTask::as_ptr(task)) + Ok(TCTask::return_val(task)) } else { Ok(std::ptr::null_mut()) } @@ -128,7 +139,7 @@ pub extern "C" fn tc_replica_new_task( rep, |rep| { let task = rep.new_task(status.into(), description.as_str()?.to_string())?; - Ok(TCTask::as_ptr(task)) + Ok(TCTask::return_val(task)) }, std::ptr::null_mut(), ) @@ -147,7 +158,7 @@ pub extern "C" fn tc_replica_import_task_with_uuid( |rep| { let uuid: Uuid = uuid.into(); let task = rep.import_task_with_uuid(uuid)?; - Ok(TCTask::as_ptr(task)) + Ok(TCTask::return_val(task)) }, std::ptr::null_mut(), ) @@ -174,12 +185,15 @@ pub extern "C" fn tc_replica_undo<'a>(rep: *mut TCReplica) -> TCResult { ) } -/// Get the latest error for a replica, or NULL if the last operation succeeded. -/// Subsequent calls to this function will return NULL. The caller must free the +/// Get the latest error for a replica, or NULL if the last operation succeeded. Subsequent calls +/// to this function will return NULL. The rep pointer must not be NULL. The caller must free the /// returned string. #[no_mangle] pub extern "C" fn tc_replica_error<'a>(rep: *mut TCReplica) -> *mut TCString<'static> { - let rep: &'a mut TCReplica = rep_ref(rep); + // SAFETY: + // - rep is not null (promised by caller) + // - rep outlives 'a (promised by caller) + let rep: &'a mut TCReplica = unsafe { TCReplica::from_arg_ref(rep) }; if let Some(tcstring) = rep.error.take() { tcstring.return_val() } else { diff --git a/lib/src/task.rs b/lib/src/task.rs index ba129caa4..21b9d177a 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -10,21 +10,31 @@ pub struct TCTask { } impl TCTask { - pub(crate) fn as_ptr(task: Task) -> *mut TCTask { + /// Borrow a TCTask from C as an argument. + /// + /// # Safety + /// + /// The pointer must not be NULL. It is the caller's responsibility to ensure that the + /// lifetime assigned to the reference and the lifetime of the TCTask itself do not outlive + /// the lifetime promised by C. + pub(crate) unsafe fn from_arg_ref<'a>(tcstring: *const TCTask) -> &'a Self { + debug_assert!(!tcstring.is_null()); + &*tcstring + } + + /// Convert this to a return value for handing off to C. + pub(crate) fn return_val(task: Task) -> *mut TCTask { Box::into_raw(Box::new(TCTask { inner: task })) } } -/// Utility function to safely convert *const TCTask into &Task -fn task_ref(task: *const TCTask) -> &'static Task { - debug_assert!(!task.is_null()); - unsafe { &(*task).inner } -} - /// Get a task's UUID. #[no_mangle] pub extern "C" fn tc_task_get_uuid<'a>(task: *const TCTask) -> TCUuid { - let task: &'a Task = task_ref(task); + // SAFETY: + // - task is not NULL (promised by caller) + // - task's lifetime exceeds this function (promised by caller) + let task: &'a Task = &unsafe { TCTask::from_arg_ref(task) }.inner; let uuid = task.get_uuid(); uuid.into() } @@ -32,7 +42,10 @@ pub extern "C" fn tc_task_get_uuid<'a>(task: *const TCTask) -> TCUuid { /// Get a task's status. #[no_mangle] pub extern "C" fn tc_task_get_status<'a>(task: *const TCTask) -> TCStatus { - let task: &'a Task = task_ref(task); + // SAFETY: + // - task is not NULL (promised by caller) + // - task's lifetime exceeds this function (promised by caller) + let task: &'a Task = &unsafe { TCTask::from_arg_ref(task) }.inner; task.get_status().into() } @@ -45,7 +58,10 @@ pub extern "C" fn tc_task_get_status<'a>(task: *const TCTask) -> TCStatus { /// contains embedded NUL characters). #[no_mangle] pub extern "C" fn tc_task_get_description<'a>(task: *const TCTask) -> *mut TCString<'static> { - let task = task_ref(task); + // SAFETY: + // - task is not NULL (promised by caller) + // - task's lifetime exceeds this function (promised by caller) + let task: &'a Task = &unsafe { TCTask::from_arg_ref(task) }.inner; let descr: TCString = task.get_description().into(); descr.return_val() } @@ -64,9 +80,14 @@ pub extern "C" fn tc_task_get_description<'a>(task: *const TCTask) -> *mut TCStr * get_modified */ -/// Free a task. +/// Free a task. The given task must not be NULL. The task must not be used after this function +/// returns, and must not be freed more than once. #[no_mangle] pub extern "C" fn tc_task_free<'a>(task: *mut TCTask) { debug_assert!(!task.is_null()); + // SAFETY: + // - task is not NULL (promised by caller) + // - task's lifetime exceeds the drop (promised by caller) + // - task does not outlive this function (promised by caller) drop(unsafe { Box::from_raw(task) }); } diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index 535c03086..43fffe97b 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -43,6 +43,12 @@ pub static TC_UUID_STRING_BYTES: usize = ::uuid::adapter::Hyphenated::LENGTH; #[no_mangle] pub extern "C" fn tc_uuid_to_buf<'a>(uuid: TCUuid, buf: *mut libc::c_char) { debug_assert!(!buf.is_null()); + // SAFETY: + // - buf is valid for len bytes (by C convention) + // - (no alignment requirements for a byte slice) + // - content of buf will not be mutated during the lifetime of this slice (lifetime + // does not outlive this function call) + // - the length of the buffer is less than isize::MAX (promised by caller) let buf: &'a mut [u8] = unsafe { std::slice::from_raw_parts_mut(buf as *mut u8, ::uuid::adapter::Hyphenated::LENGTH) }; @@ -70,6 +76,9 @@ pub extern "C" fn tc_uuid_from_str<'a>(s: *mut TCString, uuid_out: *mut TCUuid) let s = unsafe { TCString::from_arg(s) }; if let Ok(s) = s.as_str() { if let Ok(u) = Uuid::parse_str(s) { + // SAFETY: + // - uuid_out is not NULL (promised by caller) + // - alignment is not required unsafe { *uuid_out = u.into() }; return true; } diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 070b366e0..a42060ac6 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -124,8 +124,8 @@ struct TCTask *tc_replica_import_task_with_uuid(struct TCReplica *rep, struct TC enum TCResult tc_replica_undo(struct TCReplica *rep); /** - * Get the latest error for a replica, or NULL if the last operation succeeded. - * Subsequent calls to this function will return NULL. The caller must free the + * Get the latest error for a replica, or NULL if the last operation succeeded. Subsequent calls + * to this function will return NULL. The rep pointer must not be NULL. The caller must free the * returned string. */ struct TCString *tc_replica_error(struct TCReplica *rep); @@ -212,7 +212,8 @@ enum TCStatus tc_task_get_status(const struct TCTask *task); struct TCString *tc_task_get_description(const struct TCTask *task); /** - * Free a task. + * Free a task. The given task must not be NULL. The task must not be used after this function + * returns, and must not be freed more than once. */ void tc_task_free(struct TCTask *task); From 82459e699caf61aa1fe93c549318ad632db99904 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Fri, 28 Jan 2022 03:51:58 +0000 Subject: [PATCH 25/95] use a simple constant --- lib/src/uuid.rs | 7 +++---- lib/taskchampion.h | 8 ++++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index 43fffe97b..9dcd3797b 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -33,10 +33,9 @@ pub extern "C" fn tc_uuid_nil() -> TCUuid { Uuid::nil().into() } -/// Length, in bytes, of a C string containing a TCUuid. -// TODO: why not a const? -#[no_mangle] -pub static TC_UUID_STRING_BYTES: usize = ::uuid::adapter::Hyphenated::LENGTH; +// NOTE: this must be a simple constant so that cbindgen can evaluate it +/// Length, in bytes, of the string representation of a UUID (without NUL terminator) +pub const TC_UUID_STRING_BYTES: usize = 36; /// Write the string representation of a TCUuid into the given buffer, which must be /// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. diff --git a/lib/taskchampion.h b/lib/taskchampion.h index a42060ac6..9c7c030b1 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -1,5 +1,11 @@ #include #include +#define TC_UUID_STRING_BYTES 36 + +/** + * Length, in bytes, of the string representation of a UUID (without NUL terminator) + */ +#define TC_UUID_STRING_BYTES 36 /** * A result combines a boolean success value with @@ -74,8 +80,6 @@ typedef struct TCUuid { extern "C" { #endif // __cplusplus -extern const size_t TC_UUID_STRING_BYTES; - /** * Create a new TCReplica with an in-memory database. The contents of the database will be * lost when it is freed. From 50aceb96964e4025c9253c94b6f84bbf5bd991ee Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sat, 29 Jan 2022 01:00:55 +0000 Subject: [PATCH 26/95] use RefCell for replica, more consistent rust methods --- lib/src/replica.rs | 61 ++++++++++++++++++++++++++-------------------- lib/src/task.rs | 48 +++++++++++++++++++----------------- lib/taskchampion.h | 1 - 3 files changed, 59 insertions(+), 51 deletions(-) diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 47220ba7f..53e5d84c7 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -1,4 +1,5 @@ use crate::{result::TCResult, status::TCStatus, string::TCString, task::TCTask, uuid::TCUuid}; +use std::cell::{RefCell, RefMut}; use taskchampion::{Replica, StorageConfig, Uuid}; /// A replica represents an instance of a user's task data, providing an easy interface @@ -6,8 +7,7 @@ use taskchampion::{Replica, StorageConfig, Uuid}; /// /// TCReplicas are not threadsafe. pub struct TCReplica { - // TODO: make this a RefCell so that it can be take()n when holding a mut ref - inner: Replica, + inner: RefCell, error: Option>, } @@ -19,9 +19,23 @@ impl TCReplica { /// The pointer must not be NULL. It is the caller's responsibility to ensure that the /// lifetime assigned to the reference and the lifetime of the TCReplica itself do not outlive /// the lifetime promised by C. - pub(crate) unsafe fn from_arg_ref<'a>(tcstring: *mut TCReplica) -> &'a mut Self { - debug_assert!(!tcstring.is_null()); - &mut *tcstring + pub(crate) unsafe fn from_arg_ref<'a>(tcreplica: *mut TCReplica) -> &'a mut Self { + debug_assert!(!tcreplica.is_null()); + &mut *tcreplica + } + + /// Convert this to a return value for handing off to C. + pub(crate) fn return_val(self) -> *mut TCReplica { + Box::into_raw(Box::new(self)) + } +} + +impl From for TCReplica { + fn from(rep: Replica) -> TCReplica { + TCReplica { + inner: RefCell::new(rep), + error: None, + } } } @@ -29,17 +43,18 @@ fn err_to_tcstring(e: impl std::string::ToString) -> TCString<'static> { TCString::from(e.to_string()) } -/// Utility function to allow using `?` notation to return an error value. +/// Utility function to allow using `?` notation to return an error value. This makes +/// a mutable borrow, because most Replica methods require a `&mut`. fn wrap<'a, T, F>(rep: *mut TCReplica, f: F, err_value: T) -> T where - F: FnOnce(&mut Replica) -> anyhow::Result, + F: FnOnce(RefMut<'_, Replica>) -> anyhow::Result, { // SAFETY: // - rep is not null (promised by caller) // - rep outlives 'a (promised by caller) let rep: &'a mut TCReplica = unsafe { TCReplica::from_arg_ref(rep) }; rep.error = None; - match f(&mut rep.inner) { + match f(rep.inner.borrow_mut()) { Ok(v) => v, Err(e) => { rep.error = Some(err_to_tcstring(e)); @@ -55,10 +70,7 @@ pub extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica { let storage = StorageConfig::InMemory .into_storage() .expect("in-memory always succeeds"); - Box::into_raw(Box::new(TCReplica { - inner: Replica::new(storage), - error: None, - })) + TCReplica::from(Replica::new(storage)).return_val() } /// Create a new TCReplica with an on-disk database having the given filename. The filename must @@ -90,10 +102,7 @@ pub extern "C" fn tc_replica_new_on_disk<'a>( } }; - Box::into_raw(Box::new(TCReplica { - inner: Replica::new(storage), - error: None, - })) + TCReplica::from(Replica::new(storage)).return_val() } // TODO: tc_replica_all_tasks @@ -108,10 +117,10 @@ pub extern "C" fn tc_replica_new_on_disk<'a>( pub extern "C" fn tc_replica_get_task(rep: *mut TCReplica, uuid: TCUuid) -> *mut TCTask { wrap( rep, - |rep| { + |mut rep| { let uuid: Uuid = uuid.into(); if let Some(task) = rep.get_task(uuid)? { - Ok(TCTask::return_val(task)) + Ok(TCTask::from(task).return_val()) } else { Ok(std::ptr::null_mut()) } @@ -137,9 +146,9 @@ pub extern "C" fn tc_replica_new_task( let description = unsafe { TCString::from_arg(description) }; wrap( rep, - |rep| { + |mut rep| { let task = rep.new_task(status.into(), description.as_str()?.to_string())?; - Ok(TCTask::return_val(task)) + Ok(TCTask::from(task).return_val()) }, std::ptr::null_mut(), ) @@ -155,10 +164,10 @@ pub extern "C" fn tc_replica_import_task_with_uuid( ) -> *mut TCTask { wrap( rep, - |rep| { + |mut rep| { let uuid: Uuid = uuid.into(); let task = rep.import_task_with_uuid(uuid)?; - Ok(TCTask::return_val(task)) + Ok(TCTask::from(task).return_val()) }, std::ptr::null_mut(), ) @@ -174,7 +183,7 @@ pub extern "C" fn tc_replica_import_task_with_uuid( pub extern "C" fn tc_replica_undo<'a>(rep: *mut TCReplica) -> TCResult { wrap( rep, - |rep| { + |mut rep| { Ok(if rep.undo()? { TCResult::True } else { @@ -208,7 +217,5 @@ pub extern "C" fn tc_replica_free(rep: *mut TCReplica) { drop(unsafe { Box::from_raw(rep) }); } -/* - * - tc_replica_rebuild_working_set - * - tc_replica_add_undo_point - */ +// TODO: tc_replica_rebuild_working_set +// TODO: tc_replica_add_undo_point diff --git a/lib/src/task.rs b/lib/src/task.rs index 21b9d177a..ed27acc81 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -17,14 +17,20 @@ impl TCTask { /// The pointer must not be NULL. It is the caller's responsibility to ensure that the /// lifetime assigned to the reference and the lifetime of the TCTask itself do not outlive /// the lifetime promised by C. - pub(crate) unsafe fn from_arg_ref<'a>(tcstring: *const TCTask) -> &'a Self { - debug_assert!(!tcstring.is_null()); - &*tcstring + pub(crate) unsafe fn from_arg_ref<'a>(tctask: *const TCTask) -> &'a Self { + debug_assert!(!tctask.is_null()); + &*tctask } - /// Convert this to a return value for handing off to C. - pub(crate) fn return_val(task: Task) -> *mut TCTask { - Box::into_raw(Box::new(TCTask { inner: task })) + /// Convert a TCTask to a return value for handing off to C. + pub(crate) fn return_val(self) -> *mut TCTask { + Box::into_raw(Box::new(self)) + } +} + +impl From for TCTask { + fn from(task: Task) -> TCTask { + TCTask { inner: task } } } @@ -49,10 +55,8 @@ pub extern "C" fn tc_task_get_status<'a>(task: *const TCTask) -> TCStatus { task.get_status().into() } -/* TODO - * into_mut - * get_taskmap - */ +// TODO: into_mut +// TODO: get_taskmap /// Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it /// contains embedded NUL characters). @@ -66,19 +70,17 @@ pub extern "C" fn tc_task_get_description<'a>(task: *const TCTask) -> *mut TCStr descr.return_val() } -/* TODO - * get_wait - * is_waiting - * is_active - * has_tag - * get_tags - * get_annotations - * get_uda - * get_udas - * get_legacy_uda - * get_legacy_udas - * get_modified - */ +// TODO: :get_wait +// TODO: :is_waiting +// TODO: :is_active +// TODO: :has_tag +// TODO: :get_tags +// TODO: :get_annotations +// TODO: :get_uda +// TODO: :get_udas +// TODO: :get_legacy_uda +// TODO: :get_legacy_udas +// TODO: :get_modified /// Free a task. The given task must not be NULL. The task must not be used after this function /// returns, and must not be freed more than once. diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 9c7c030b1..67250584c 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -1,6 +1,5 @@ #include #include -#define TC_UUID_STRING_BYTES 36 /** * Length, in bytes, of the string representation of a UUID (without NUL terminator) From 452ae2074f3634fe19727923f6ac6884e50c8061 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sat, 29 Jan 2022 03:08:45 +0000 Subject: [PATCH 27/95] implement task mutability --- integration-tests/src/bindings_tests/task.c | 59 ++++++ lib/src/replica.rs | 52 ++++- lib/src/task.rs | 220 +++++++++++++++++--- lib/src/taskmut.rs | 67 ++++++ lib/taskchampion.h | 52 ++++- 5 files changed, 411 insertions(+), 39 deletions(-) create mode 100644 lib/src/taskmut.rs diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index b0bbc4117..b6f323583 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -26,9 +26,68 @@ static void test_task_creation(void) { tc_replica_free(rep); } +// updating status on a task works +static void test_task_get_set_status(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task)); + + tc_task_to_mut(task, rep); + TEST_ASSERT_TRUE(tc_task_set_status(task, TC_STATUS_DELETED)); + TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task)); // while mut + tc_task_to_immut(task, rep); + TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task)); // while immut + + tc_task_free(task); + + tc_replica_free(rep); +} + +// updating description on a task works +static void test_task_get_set_description(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + TCString *desc; + + tc_task_to_mut(task, rep); + tc_task_set_description(task, tc_string_borrow("updated")); + + TEST_ASSERT_TRUE(desc = tc_task_get_description(task)); + TEST_ASSERT_NOT_NULL(desc); + TEST_ASSERT_EQUAL_STRING("updated", tc_string_content(desc)); + tc_string_free(desc); + + tc_task_to_immut(task, rep); + + desc = tc_task_get_description(task); + TEST_ASSERT_NOT_NULL(desc); + TEST_ASSERT_EQUAL_STRING("updated", tc_string_content(desc)); + tc_string_free(desc); + + tc_task_free(task); + + tc_replica_free(rep); +} + int task_tests(void) { UNITY_BEGIN(); // each test case above should be named here, in order. RUN_TEST(test_task_creation); + RUN_TEST(test_task_get_set_status); + RUN_TEST(test_task_get_set_description); return UNITY_END(); } diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 53e5d84c7..d9c28eb0b 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -1,5 +1,4 @@ use crate::{result::TCResult, status::TCStatus, string::TCString, task::TCTask, uuid::TCUuid}; -use std::cell::{RefCell, RefMut}; use taskchampion::{Replica, StorageConfig, Uuid}; /// A replica represents an instance of a user's task data, providing an easy interface @@ -7,7 +6,13 @@ use taskchampion::{Replica, StorageConfig, Uuid}; /// /// TCReplicas are not threadsafe. pub struct TCReplica { - inner: RefCell, + /// The wrapped Replica + inner: Replica, + + /// If true, this replica has an outstanding &mut (for a TaskMut) + mut_borrowed: bool, + + /// The error from the most recent operation, if any error: Option>, } @@ -24,16 +29,36 @@ impl TCReplica { &mut *tcreplica } + // TODO: from_arg_owned, use in drop + /// Convert this to a return value for handing off to C. pub(crate) fn return_val(self) -> *mut TCReplica { Box::into_raw(Box::new(self)) } + + /// Mutably borrow the inner Replica + pub(crate) fn borrow_mut(&mut self) -> &mut Replica { + if self.mut_borrowed { + panic!("replica is already borrowed"); + } + self.mut_borrowed = true; + &mut self.inner + } + + /// Release the borrow made by [`borrow_mut`] + pub(crate) fn release_borrow(&mut self) { + if !self.mut_borrowed { + panic!("replica is not borrowed"); + } + self.mut_borrowed = false; + } } impl From for TCReplica { fn from(rep: Replica) -> TCReplica { TCReplica { - inner: RefCell::new(rep), + inner: rep, + mut_borrowed: false, error: None, } } @@ -47,14 +72,17 @@ fn err_to_tcstring(e: impl std::string::ToString) -> TCString<'static> { /// a mutable borrow, because most Replica methods require a `&mut`. fn wrap<'a, T, F>(rep: *mut TCReplica, f: F, err_value: T) -> T where - F: FnOnce(RefMut<'_, Replica>) -> anyhow::Result, + F: FnOnce(&mut Replica) -> anyhow::Result, { // SAFETY: // - rep is not null (promised by caller) // - rep outlives 'a (promised by caller) let rep: &'a mut TCReplica = unsafe { TCReplica::from_arg_ref(rep) }; + if rep.mut_borrowed { + panic!("replica is borrowed and cannot be used"); + } rep.error = None; - match f(rep.inner.borrow_mut()) { + match f(&mut rep.inner) { Ok(v) => v, Err(e) => { rep.error = Some(err_to_tcstring(e)); @@ -117,7 +145,7 @@ pub extern "C" fn tc_replica_new_on_disk<'a>( pub extern "C" fn tc_replica_get_task(rep: *mut TCReplica, uuid: TCUuid) -> *mut TCTask { wrap( rep, - |mut rep| { + |rep| { let uuid: Uuid = uuid.into(); if let Some(task) = rep.get_task(uuid)? { Ok(TCTask::from(task).return_val()) @@ -146,7 +174,7 @@ pub extern "C" fn tc_replica_new_task( let description = unsafe { TCString::from_arg(description) }; wrap( rep, - |mut rep| { + |rep| { let task = rep.new_task(status.into(), description.as_str()?.to_string())?; Ok(TCTask::from(task).return_val()) }, @@ -164,7 +192,7 @@ pub extern "C" fn tc_replica_import_task_with_uuid( ) -> *mut TCTask { wrap( rep, - |mut rep| { + |rep| { let uuid: Uuid = uuid.into(); let task = rep.import_task_with_uuid(uuid)?; Ok(TCTask::from(task).return_val()) @@ -183,7 +211,7 @@ pub extern "C" fn tc_replica_import_task_with_uuid( pub extern "C" fn tc_replica_undo<'a>(rep: *mut TCReplica) -> TCResult { wrap( rep, - |mut rep| { + |rep| { Ok(if rep.undo()? { TCResult::True } else { @@ -213,7 +241,11 @@ pub extern "C" fn tc_replica_error<'a>(rep: *mut TCReplica) -> *mut TCString<'st /// Free a TCReplica. #[no_mangle] pub extern "C" fn tc_replica_free(rep: *mut TCReplica) { - debug_assert!(!rep.is_null()); + let replica: &mut TCReplica = unsafe { TCReplica::from_arg_ref(rep) }; + if replica.mut_borrowed { + panic!("replica is borrowed and cannot be freed"); + } + drop(replica); drop(unsafe { Box::from_raw(rep) }); } diff --git a/lib/src/task.rs b/lib/src/task.rs index ed27acc81..ac65b22eb 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -1,12 +1,21 @@ -use crate::{status::TCStatus, string::TCString, uuid::TCUuid}; -use taskchampion::Task; +use crate::{replica::TCReplica, status::TCStatus, string::TCString, uuid::TCUuid}; +use std::ops::Deref; +use taskchampion::{Task, TaskMut}; /// A task, as publicly exposed by this library. /// +/// A task begins in "immutable" mode. It must be converted to "mutable" mode +/// to make any changes, and doing so requires exclusive access to the replica +/// until the task is freed or converted back to immutable mode. +/// /// A task carries no reference to the replica that created it, and can /// be used until it is freed or converted to a TaskMut. -pub struct TCTask { - inner: Task, +pub enum TCTask { + Immutable(Task), + Mutable(TaskMut<'static>), + + /// A transitional state for a TCTask as it goes from mutable to immutable. + Invalid, } impl TCTask { @@ -22,55 +31,155 @@ impl TCTask { &*tctask } + /// Borrow a TCTask from C as an argument, allowing mutation. + /// + /// # Safety + /// + /// The pointer must not be NULL. It is the caller's responsibility to ensure that the + /// lifetime assigned to the reference and the lifetime of the TCTask itself do not outlive + /// the lifetime promised by C. + pub(crate) unsafe fn from_arg_ref_mut<'a>(tctask: *mut TCTask) -> &'a mut Self { + debug_assert!(!tctask.is_null()); + &mut *tctask + } + + // TODO: from_arg_owned, use in drop + /// Convert a TCTask to a return value for handing off to C. pub(crate) fn return_val(self) -> *mut TCTask { Box::into_raw(Box::new(self)) } + + /// Make an immutable TCTask into a mutable TCTask. Does nothing if the task + /// is already mutable. + fn to_mut(&mut self, tcreplica: &'static mut TCReplica) { + *self = match std::mem::replace(self, TCTask::Invalid) { + TCTask::Immutable(task) => { + let rep_ref = tcreplica.borrow_mut(); + TCTask::Mutable(task.into_mut(rep_ref)) + } + TCTask::Mutable(task) => TCTask::Mutable(task), + TCTask::Invalid => unreachable!(), + } + } + + /// Make an mutable TCTask into a immutable TCTask. Does nothing if the task + /// is already immutable. + fn to_immut(&mut self, tcreplica: &mut TCReplica) { + *self = match std::mem::replace(self, TCTask::Invalid) { + TCTask::Immutable(task) => TCTask::Immutable(task), + TCTask::Mutable(task) => { + tcreplica.release_borrow(); + TCTask::Immutable(task.into_immut()) + } + TCTask::Invalid => unreachable!(), + } + } } impl From for TCTask { fn from(task: Task) -> TCTask { - TCTask { inner: task } + TCTask::Immutable(task) } } +/// Utility function to get a shared reference to the underlying Task. +fn wrap<'a, T, F>(task: *const TCTask, f: F) -> T +where + F: FnOnce(&Task) -> T, +{ + // SAFETY: + // - task is not null (promised by caller) + // - task outlives 'a (promised by caller) + let tctask: &'a TCTask = unsafe { TCTask::from_arg_ref(task) }; + let task: &'a Task = match tctask { + TCTask::Immutable(t) => t, + TCTask::Mutable(t) => t.deref(), + TCTask::Invalid => unreachable!(), + }; + f(task) +} + +/// Utility function to get a mutable reference to the underlying Task. The +/// TCTask must be mutable. +fn wrap_mut<'a, T, F>(task: *mut TCTask, f: F) -> T +where + F: FnOnce(&mut TaskMut) -> anyhow::Result, +{ + // SAFETY: + // - task is not null (promised by caller) + // - task outlives 'a (promised by caller) + let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; + let task: &'a mut TaskMut = match tctask { + TCTask::Immutable(_) => panic!("Task is immutable"), + TCTask::Mutable(ref mut t) => t, + TCTask::Invalid => unreachable!(), + }; + // TODO: add TCTask error handling, like replica + f(task).unwrap() +} + +/// Convert an immutable task into a mutable task. +/// +/// The task is modified in-place, and becomes mutable. +/// +/// The replica _cannot be used at all_ until this task is made immutable again. This implies that +/// it is not allowed for more than one task associated with a replica to be mutable at any time. +/// +/// Typical mutation of tasks is bracketed with `tc_task_to_mut` and `tc_task_to_immut`: +/// +/// ```c +/// tc_task_to_mut(task, rep); +/// success = tc_task_done(task); +/// tc_task_to_immut(task, rep); +/// if (!success) { ... } +/// ``` +#[no_mangle] +pub extern "C" fn tc_task_to_mut<'a>(task: *mut TCTask, rep: *mut TCReplica) { + let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; + let tcreplica: &'static mut TCReplica = unsafe { TCReplica::from_arg_ref(rep) }; + tctask.to_mut(tcreplica); +} + +/// Convert a mutable task into an immutable task. +/// +/// The task is modified in-place, and becomes immutable. +/// +/// The replica may be used freely after this call. +#[no_mangle] +pub extern "C" fn tc_task_to_immut<'a>(task: *mut TCTask, rep: *mut TCReplica) { + let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; + let tcreplica: &'static mut TCReplica = unsafe { TCReplica::from_arg_ref(rep) }; + tctask.to_immut(tcreplica); +} + /// Get a task's UUID. #[no_mangle] -pub extern "C" fn tc_task_get_uuid<'a>(task: *const TCTask) -> TCUuid { - // SAFETY: - // - task is not NULL (promised by caller) - // - task's lifetime exceeds this function (promised by caller) - let task: &'a Task = &unsafe { TCTask::from_arg_ref(task) }.inner; - let uuid = task.get_uuid(); - uuid.into() +pub extern "C" fn tc_task_get_uuid(task: *const TCTask) -> TCUuid { + wrap(task, |task| task.get_uuid().into()) } /// Get a task's status. #[no_mangle] pub extern "C" fn tc_task_get_status<'a>(task: *const TCTask) -> TCStatus { - // SAFETY: - // - task is not NULL (promised by caller) - // - task's lifetime exceeds this function (promised by caller) - let task: &'a Task = &unsafe { TCTask::from_arg_ref(task) }.inner; - task.get_status().into() + wrap(task, |task| task.get_status().into()) } -// TODO: into_mut // TODO: get_taskmap /// Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it /// contains embedded NUL characters). #[no_mangle] pub extern "C" fn tc_task_get_description<'a>(task: *const TCTask) -> *mut TCString<'static> { - // SAFETY: - // - task is not NULL (promised by caller) - // - task's lifetime exceeds this function (promised by caller) - let task: &'a Task = &unsafe { TCTask::from_arg_ref(task) }.inner; - let descr: TCString = task.get_description().into(); - descr.return_val() + wrap(task, |task| { + let descr: TCString = task.get_description().into(); + descr.return_val() + }) } +// TODO: :get_entry // TODO: :get_wait +// TODO: :get_modified // TODO: :is_waiting // TODO: :is_active // TODO: :has_tag @@ -82,11 +191,68 @@ pub extern "C" fn tc_task_get_description<'a>(task: *const TCTask) -> *mut TCStr // TODO: :get_legacy_udas // TODO: :get_modified -/// Free a task. The given task must not be NULL. The task must not be used after this function -/// returns, and must not be freed more than once. +/// Set a mutable task's status. +/// +/// Returns false on error. +#[no_mangle] +pub extern "C" fn tc_task_set_status<'a>(task: *mut TCTask, status: TCStatus) -> bool { + wrap_mut(task, |task| { + task.set_status(status.into())?; + Ok(true) + }) +} + +/// Set a mutable task's description. +/// +/// Returns false on error. +#[no_mangle] +pub extern "C" fn tc_task_set_description<'a>( + task: *mut TCTask, + description: *mut TCString, +) -> bool { + // SAFETY: + // - tcstring is not NULL (promised by caller) + // - caller is exclusive owner of tcstring (implicitly promised by caller) + let description = unsafe { TCString::from_arg(description) }; + wrap_mut(task, |task| { + task.set_description(description.as_str()?.to_string())?; + Ok(true) + }) +} + +// TODO: tc_task_set_description +// TODO: tc_task_set_entry +// TODO: tc_task_set_wait +// TODO: tc_task_set_modified +// TODO: tc_task_start +// TODO: tc_task_stop +// TODO: tc_task_done +// TODO: tc_task_delete +// TODO: tc_task_add_tag +// TODO: tc_task_remove_tag +// TODO: tc_task_add_annotation +// TODO: tc_task_remove_annotation +// TODO: tc_task_set_uda +// TODO: tc_task_remove_uda +// TODO: tc_task_set_legacy_uda +// TODO: tc_task_remove_legacy_uda + +/// Free a task. The given task must not be NULL and must be immutable. The task must not be used +/// after this function returns, and must not be freed more than once. +/// +/// The restriction that the task must be immutable may be lifted (TODO) #[no_mangle] pub extern "C" fn tc_task_free<'a>(task: *mut TCTask) { - debug_assert!(!task.is_null()); + // convert the task to immutable before freeing + let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; + if !matches!(tctask, TCTask::Immutable(_)) { + // this limit is in place because we require the caller to supply a pointer + // to the replica to make a task immutable, and no such pointer is available + // here. + panic!("Task must be immutable when freed"); + } + drop(tctask); + // SAFETY: // - task is not NULL (promised by caller) // - task's lifetime exceeds the drop (promised by caller) diff --git a/lib/src/taskmut.rs b/lib/src/taskmut.rs new file mode 100644 index 000000000..311647e54 --- /dev/null +++ b/lib/src/taskmut.rs @@ -0,0 +1,67 @@ +use crate::{status::TCStatus, string::TCString, uuid::TCUuid}; +use taskchampion::{TaskMut, Replica}; +use std::cell::RefMut; + +/// A mutable task. +/// +/// A mutable task carries an exclusive reference to the replica, +/// meaning that no other replica operations can be carried out while +/// the mutable task still exists. +pub struct TCTaskMut { + inner: TaskMut<'static>, + replica: RefMut, +} + +impl TCTaskMut { + /// Borrow a TCTaskMut from C as an argument. + /// + /// # Safety + /// + /// The pointer must not be NULL. It is the caller's responsibility to ensure that the + /// lifetime assigned to the reference and the lifetime of the TCTaskMut itself do not outlive + /// the lifetime promised by C. + pub(crate) unsafe fn from_arg_ref<'a>(tctask: *mut TCTaskMut) -> &'a mut Self { + debug_assert!(!tctask.is_null()); + &mut *tctask + } + + /// Convert this to a return value for handing off to C. + pub(crate) fn return_val(self) -> *mut TCTaskMut { + Box::into_raw(Box::new(self)) + } + + /// Create a new TCTaskMut, given a RefMut to the replica. + pub(crate) fn from_immut(task: Task, rep: RefMut) -> Self { + // SAFETY: + // This ref will be embedded in the TaskMut, and we will hang onto + // the RefMut for that duration, guaranteeing no other mutable borrows. + let rep_ref: &'static mut = unsafe { rep.deref_mut() } + let task_mut = task.into_mut(rep_ref); + TCTaskMut { + inner: task_mut, + replica: rep, + } + } +} + +impl From for TCTaskMut { + fn from(rep: Replica) -> TCReplica { + TCReplica { + inner: RefCell::new(rep), + error: None, + } + } +} + + +/// Free a task. The given task must not be NULL. The task must not be used after this function +/// returns, and must not be freed more than once. +#[no_mangle] +pub extern "C" fn tc_task_mut_free(task: *mut TCTaskMut) { + debug_assert!(!task.is_null()); + // SAFETY: + // - task is not NULL (promised by caller) + // - task's lifetime exceeds the drop (promised by caller) + // - task does not outlive this function (promised by caller) + drop(unsafe { Box::from_raw(task) }); +} diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 67250584c..f632938d5 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -61,6 +61,10 @@ typedef struct TCString TCString; /** * A task, as publicly exposed by this library. * + * A task begins in "immutable" mode. It must be converted to "mutable" mode + * to make any changes, and doing so requires exclusive access to the replica + * until the task is freed or converted back to immutable mode. + * * A task carries no reference to the replica that created it, and can * be used until it is freed or converted to a TaskMut. */ @@ -198,6 +202,34 @@ const char *tc_string_content_with_len(struct TCString *tcstring, size_t *len_ou */ void tc_string_free(struct TCString *tcstring); +/** + * Convert an immutable task into a mutable task. + * + * The task is modified in-place, and becomes mutable. + * + * The replica _cannot be used at all_ until this task is made immutable again. This implies that + * it is not allowed for more than one task associated with a replica to be mutable at any time. + * + * Typical mutation of tasks is bracketed with `tc_task_to_mut` and `tc_task_to_immut`: + * + * ```c + * tc_task_to_mut(task, rep); + * success = tc_task_done(task); + * tc_task_to_immut(task, rep); + * if (!success) { ... } + * ``` + */ +void tc_task_to_mut(struct TCTask *task, struct TCReplica *rep); + +/** + * Convert a mutable task into an immutable task. + * + * The task is modified in-place, and becomes immutable. + * + * The replica may be used freely after this call. + */ +void tc_task_to_immut(struct TCTask *task, struct TCReplica *rep); + /** * Get a task's UUID. */ @@ -215,8 +247,24 @@ enum TCStatus tc_task_get_status(const struct TCTask *task); struct TCString *tc_task_get_description(const struct TCTask *task); /** - * Free a task. The given task must not be NULL. The task must not be used after this function - * returns, and must not be freed more than once. + * Set a mutable task's status. + * + * Returns false on error. + */ +bool tc_task_set_status(struct TCTask *task, enum TCStatus status); + +/** + * Set a mutable task's description. + * + * Returns false on error. + */ +bool tc_task_set_description(struct TCTask *task, struct TCString *description); + +/** + * Free a task. The given task must not be NULL and must be immutable. The task must not be used + * after this function returns, and must not be freed more than once. + * + * The restriction that the task must be immutable may be lifted (TODO) */ void tc_task_free(struct TCTask *task); From 364ca57736fa3e7051c61eefcb003a3236c8ab04 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 30 Jan 2022 23:42:52 +0000 Subject: [PATCH 28/95] Slightly more ergonomic task mutation --- integration-tests/src/bindings_tests/task.c | 4 +- lib/src/task.rs | 73 ++++++++++++++------- lib/src/taskmut.rs | 67 ------------------- lib/taskchampion.h | 15 +++-- 4 files changed, 61 insertions(+), 98 deletions(-) delete mode 100644 lib/src/taskmut.rs diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index b6f323583..b4173a1eb 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -42,7 +42,7 @@ static void test_task_get_set_status(void) { tc_task_to_mut(task, rep); TEST_ASSERT_TRUE(tc_task_set_status(task, TC_STATUS_DELETED)); TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task)); // while mut - tc_task_to_immut(task, rep); + tc_task_to_immut(task); TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task)); // while immut tc_task_free(task); @@ -71,7 +71,7 @@ static void test_task_get_set_description(void) { TEST_ASSERT_EQUAL_STRING("updated", tc_string_content(desc)); tc_string_free(desc); - tc_task_to_immut(task, rep); + tc_task_to_immut(task); desc = tc_task_get_description(task); TEST_ASSERT_NOT_NULL(desc); diff --git a/lib/src/task.rs b/lib/src/task.rs index ac65b22eb..f929416d9 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -11,10 +11,16 @@ use taskchampion::{Task, TaskMut}; /// A task carries no reference to the replica that created it, and can /// be used until it is freed or converted to a TaskMut. pub enum TCTask { + /// A regular, immutable task Immutable(Task), - Mutable(TaskMut<'static>), - /// A transitional state for a TCTask as it goes from mutable to immutable. + /// A mutable task, together with the replica to which it holds an exclusive + /// reference. + Mutable(TaskMut<'static>, *mut TCReplica), + + /// A transitional state for a TCTask as it goes from mutable to immutable and back. A task + /// can only be in this state outside of [`to_mut`] and [`to_immut`] if a panic occurs during + /// one of those methods. Invalid, } @@ -52,24 +58,38 @@ impl TCTask { /// Make an immutable TCTask into a mutable TCTask. Does nothing if the task /// is already mutable. - fn to_mut(&mut self, tcreplica: &'static mut TCReplica) { + /// + /// # Safety + /// + /// The tcreplica pointer must not be NULL, and the replica it points to must not + /// be freed before TCTask.to_immut completes. + unsafe fn to_mut(&mut self, tcreplica: *mut TCReplica) { *self = match std::mem::replace(self, TCTask::Invalid) { TCTask::Immutable(task) => { - let rep_ref = tcreplica.borrow_mut(); - TCTask::Mutable(task.into_mut(rep_ref)) + // SAFETY: + // - tcreplica is not null (promised by caller) + // - tcreplica outlives the pointer in this variant (promised by caller) + let tcreplica_ref: &mut TCReplica = TCReplica::from_arg_ref(tcreplica); + let rep_ref = tcreplica_ref.borrow_mut(); + TCTask::Mutable(task.into_mut(rep_ref), tcreplica) } - TCTask::Mutable(task) => TCTask::Mutable(task), + TCTask::Mutable(task, tcreplica) => TCTask::Mutable(task, tcreplica), TCTask::Invalid => unreachable!(), } } /// Make an mutable TCTask into a immutable TCTask. Does nothing if the task /// is already immutable. - fn to_immut(&mut self, tcreplica: &mut TCReplica) { + fn to_immut(&mut self) { *self = match std::mem::replace(self, TCTask::Invalid) { TCTask::Immutable(task) => TCTask::Immutable(task), - TCTask::Mutable(task) => { - tcreplica.release_borrow(); + TCTask::Mutable(task, tcreplica) => { + // SAFETY: + // - tcreplica is not null (promised by caller of to_mut, which created this + // variant) + // - tcreplica is still alive (promised by caller of to_mut) + let tcreplica_ref: &mut TCReplica = unsafe { TCReplica::from_arg_ref(tcreplica) }; + tcreplica_ref.release_borrow(); TCTask::Immutable(task.into_immut()) } TCTask::Invalid => unreachable!(), @@ -94,7 +114,7 @@ where let tctask: &'a TCTask = unsafe { TCTask::from_arg_ref(task) }; let task: &'a Task = match tctask { TCTask::Immutable(t) => t, - TCTask::Mutable(t) => t.deref(), + TCTask::Mutable(t, _) => t.deref(), TCTask::Invalid => unreachable!(), }; f(task) @@ -112,7 +132,7 @@ where let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; let task: &'a mut TaskMut = match tctask { TCTask::Immutable(_) => panic!("Task is immutable"), - TCTask::Mutable(ref mut t) => t, + TCTask::Mutable(ref mut t, _) => t, TCTask::Invalid => unreachable!(), }; // TODO: add TCTask error handling, like replica @@ -121,10 +141,11 @@ where /// Convert an immutable task into a mutable task. /// -/// The task is modified in-place, and becomes mutable. +/// The task must not be NULL. It is modified in-place, and becomes mutable. /// -/// The replica _cannot be used at all_ until this task is made immutable again. This implies that -/// it is not allowed for more than one task associated with a replica to be mutable at any time. +/// The replica must not be NULL. After this function returns, the replica _cannot be used at all_ +/// until this task is made immutable again. This implies that it is not allowed for more than one +/// task associated with a replica to be mutable at any time. /// /// Typical mutation of tasks is bracketed with `tc_task_to_mut` and `tc_task_to_immut`: /// @@ -135,22 +156,30 @@ where /// if (!success) { ... } /// ``` #[no_mangle] -pub extern "C" fn tc_task_to_mut<'a>(task: *mut TCTask, rep: *mut TCReplica) { +pub extern "C" fn tc_task_to_mut<'a>(task: *mut TCTask, tcreplica: *mut TCReplica) { + // SAFETY: + // - task is not null (promised by caller) + // - task outlives 'a (promised by caller) let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; - let tcreplica: &'static mut TCReplica = unsafe { TCReplica::from_arg_ref(rep) }; - tctask.to_mut(tcreplica); + // SAFETY: + // - tcreplica is not NULL (promised by caller) + // - tcreplica lives until later call to to_immut via tc_task_to_immut (promised by caller, + // who cannot call tc_replica_free during this time) + unsafe { tctask.to_mut(tcreplica) }; } /// Convert a mutable task into an immutable task. /// -/// The task is modified in-place, and becomes immutable. +/// The task must not be NULL. It is modified in-place, and becomes immutable. /// -/// The replica may be used freely after this call. +/// The replica passed to `tc_task_to_mut` may be used freely after this call. #[no_mangle] -pub extern "C" fn tc_task_to_immut<'a>(task: *mut TCTask, rep: *mut TCReplica) { +pub extern "C" fn tc_task_to_immut<'a>(task: *mut TCTask) { + // SAFETY: + // - task is not null (promised by caller) + // - task outlives 'a (promised by caller) let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; - let tcreplica: &'static mut TCReplica = unsafe { TCReplica::from_arg_ref(rep) }; - tctask.to_immut(tcreplica); + tctask.to_immut(); } /// Get a task's UUID. diff --git a/lib/src/taskmut.rs b/lib/src/taskmut.rs deleted file mode 100644 index 311647e54..000000000 --- a/lib/src/taskmut.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::{status::TCStatus, string::TCString, uuid::TCUuid}; -use taskchampion::{TaskMut, Replica}; -use std::cell::RefMut; - -/// A mutable task. -/// -/// A mutable task carries an exclusive reference to the replica, -/// meaning that no other replica operations can be carried out while -/// the mutable task still exists. -pub struct TCTaskMut { - inner: TaskMut<'static>, - replica: RefMut, -} - -impl TCTaskMut { - /// Borrow a TCTaskMut from C as an argument. - /// - /// # Safety - /// - /// The pointer must not be NULL. It is the caller's responsibility to ensure that the - /// lifetime assigned to the reference and the lifetime of the TCTaskMut itself do not outlive - /// the lifetime promised by C. - pub(crate) unsafe fn from_arg_ref<'a>(tctask: *mut TCTaskMut) -> &'a mut Self { - debug_assert!(!tctask.is_null()); - &mut *tctask - } - - /// Convert this to a return value for handing off to C. - pub(crate) fn return_val(self) -> *mut TCTaskMut { - Box::into_raw(Box::new(self)) - } - - /// Create a new TCTaskMut, given a RefMut to the replica. - pub(crate) fn from_immut(task: Task, rep: RefMut) -> Self { - // SAFETY: - // This ref will be embedded in the TaskMut, and we will hang onto - // the RefMut for that duration, guaranteeing no other mutable borrows. - let rep_ref: &'static mut = unsafe { rep.deref_mut() } - let task_mut = task.into_mut(rep_ref); - TCTaskMut { - inner: task_mut, - replica: rep, - } - } -} - -impl From for TCTaskMut { - fn from(rep: Replica) -> TCReplica { - TCReplica { - inner: RefCell::new(rep), - error: None, - } - } -} - - -/// Free a task. The given task must not be NULL. The task must not be used after this function -/// returns, and must not be freed more than once. -#[no_mangle] -pub extern "C" fn tc_task_mut_free(task: *mut TCTaskMut) { - debug_assert!(!task.is_null()); - // SAFETY: - // - task is not NULL (promised by caller) - // - task's lifetime exceeds the drop (promised by caller) - // - task does not outlive this function (promised by caller) - drop(unsafe { Box::from_raw(task) }); -} diff --git a/lib/taskchampion.h b/lib/taskchampion.h index f632938d5..18aa00faf 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -205,10 +205,11 @@ void tc_string_free(struct TCString *tcstring); /** * Convert an immutable task into a mutable task. * - * The task is modified in-place, and becomes mutable. + * The task must not be NULL. It is modified in-place, and becomes mutable. * - * The replica _cannot be used at all_ until this task is made immutable again. This implies that - * it is not allowed for more than one task associated with a replica to be mutable at any time. + * The replica must not be NULL. After this function returns, the replica _cannot be used at all_ + * until this task is made immutable again. This implies that it is not allowed for more than one + * task associated with a replica to be mutable at any time. * * Typical mutation of tasks is bracketed with `tc_task_to_mut` and `tc_task_to_immut`: * @@ -219,16 +220,16 @@ void tc_string_free(struct TCString *tcstring); * if (!success) { ... } * ``` */ -void tc_task_to_mut(struct TCTask *task, struct TCReplica *rep); +void tc_task_to_mut(struct TCTask *task, struct TCReplica *tcreplica); /** * Convert a mutable task into an immutable task. * - * The task is modified in-place, and becomes immutable. + * The task must not be NULL. It is modified in-place, and becomes immutable. * - * The replica may be used freely after this call. + * The replica passed to `tc_task_to_mut` may be used freely after this call. */ -void tc_task_to_immut(struct TCTask *task, struct TCReplica *rep); +void tc_task_to_immut(struct TCTask *task); /** * Get a task's UUID. From d24319179c64455e61f2973f1746e947548fc566 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 30 Jan 2022 23:53:12 +0000 Subject: [PATCH 29/95] TCFoo::from_arg to take from a pointer --- lib/src/replica.rs | 19 +++++++++++++++---- lib/src/task.rs | 22 +++++++++++++--------- lib/taskchampion.h | 3 ++- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/lib/src/replica.rs b/lib/src/replica.rs index d9c28eb0b..a5643f045 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -29,7 +29,15 @@ impl TCReplica { &mut *tcreplica } - // TODO: from_arg_owned, use in drop + /// Take a TCReplica from C as an argument. + /// + /// # Safety + /// + /// The pointer must not be NULL. The pointer becomes invalid before this function returns. + pub(crate) unsafe fn from_arg(tcreplica: *mut TCReplica) -> Self { + debug_assert!(!tcreplica.is_null()); + *Box::from_raw(tcreplica) + } /// Convert this to a return value for handing off to C. pub(crate) fn return_val(self) -> *mut TCReplica { @@ -238,15 +246,18 @@ pub extern "C" fn tc_replica_error<'a>(rep: *mut TCReplica) -> *mut TCString<'st } } -/// Free a TCReplica. +/// Free a replica. The replica may not be used after this function returns and must not be freed +/// more than once. #[no_mangle] pub extern "C" fn tc_replica_free(rep: *mut TCReplica) { - let replica: &mut TCReplica = unsafe { TCReplica::from_arg_ref(rep) }; + // SAFETY: + // - rep is not NULL + // - caller will not use the TCReplica after this + let replica = unsafe { TCReplica::from_arg(rep) }; if replica.mut_borrowed { panic!("replica is borrowed and cannot be freed"); } drop(replica); - drop(unsafe { Box::from_raw(rep) }); } // TODO: tc_replica_rebuild_working_set diff --git a/lib/src/task.rs b/lib/src/task.rs index f929416d9..558843b17 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -49,7 +49,15 @@ impl TCTask { &mut *tctask } - // TODO: from_arg_owned, use in drop + /// Take a TCTask from C as an argument. + /// + /// # Safety + /// + /// The pointer must not be NULL. The pointer becomes invalid before this function returns. + pub(crate) unsafe fn from_arg<'a>(tctask: *mut TCTask) -> Self { + debug_assert!(!tctask.is_null()); + *Box::from_raw(tctask) + } /// Convert a TCTask to a return value for handing off to C. pub(crate) fn return_val(self) -> *mut TCTask { @@ -272,8 +280,10 @@ pub extern "C" fn tc_task_set_description<'a>( /// The restriction that the task must be immutable may be lifted (TODO) #[no_mangle] pub extern "C" fn tc_task_free<'a>(task: *mut TCTask) { - // convert the task to immutable before freeing - let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; + // SAFETY: + // - rep is not NULL + // - caller will not use the TCTask after this + let tctask = unsafe { TCTask::from_arg(task) }; if !matches!(tctask, TCTask::Immutable(_)) { // this limit is in place because we require the caller to supply a pointer // to the replica to make a task immutable, and no such pointer is available @@ -281,10 +291,4 @@ pub extern "C" fn tc_task_free<'a>(task: *mut TCTask) { panic!("Task must be immutable when freed"); } drop(tctask); - - // SAFETY: - // - task is not NULL (promised by caller) - // - task's lifetime exceeds the drop (promised by caller) - // - task does not outlive this function (promised by caller) - drop(unsafe { Box::from_raw(task) }); } diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 18aa00faf..cda313171 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -138,7 +138,8 @@ enum TCResult tc_replica_undo(struct TCReplica *rep); struct TCString *tc_replica_error(struct TCReplica *rep); /** - * Free a TCReplica. + * Free a replica. The replica may not be used after this function returns and must not be freed + * more than once. */ void tc_replica_free(struct TCReplica *rep); From 8bd9605b252bdc728659f79bbcc779deef0e4f3e Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Mon, 31 Jan 2022 00:04:58 +0000 Subject: [PATCH 30/95] support starting and stopping tasks --- integration-tests/src/bindings_tests/task.c | 55 ++++++++++++++ lib/src/replica.rs | 4 +- lib/src/task.rs | 83 ++++++++++++++------- lib/taskchampion.h | 27 ++++++- 4 files changed, 136 insertions(+), 33 deletions(-) diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index b4173a1eb..b75320df3 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -26,6 +26,34 @@ static void test_task_creation(void) { tc_replica_free(rep); } +// freeing a mutable task works, marking it immutable +static void test_task_free_mutable_task(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task)); + TCUuid uuid = tc_task_get_uuid(task); + + tc_task_to_mut(task, rep); + TEST_ASSERT_TRUE(tc_task_set_status(task, TC_STATUS_DELETED)); + TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task)); + + tc_task_free(task); // implicitly converts to immut + + task = tc_replica_get_task(rep, uuid); + TEST_ASSERT_NOT_NULL(task); + TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task)); + tc_task_free(task); + + tc_replica_free(rep); +} + // updating status on a task works static void test_task_get_set_status(void) { TCReplica *rep = tc_replica_new_in_memory(); @@ -83,11 +111,38 @@ static void test_task_get_set_description(void) { tc_replica_free(rep); } +// starting and stopping a task works, as seen by tc_task_is_active +static void test_task_start_stop_is_active(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + TEST_ASSERT_FALSE(tc_task_is_active(task)); + + tc_task_to_mut(task, rep); + + TEST_ASSERT_FALSE(tc_task_is_active(task)); + tc_task_start(task); + TEST_ASSERT_TRUE(tc_task_is_active(task)); + tc_task_stop(task); + TEST_ASSERT_FALSE(tc_task_is_active(task)); + + tc_task_free(task); + tc_replica_free(rep); +} + int task_tests(void) { UNITY_BEGIN(); // each test case above should be named here, in order. RUN_TEST(test_task_creation); + RUN_TEST(test_task_free_mutable_task); RUN_TEST(test_task_get_set_status); RUN_TEST(test_task_get_set_description); + RUN_TEST(test_task_start_stop_is_active); return UNITY_END(); } diff --git a/lib/src/replica.rs b/lib/src/replica.rs index a5643f045..3dd1f0df9 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -251,8 +251,8 @@ pub extern "C" fn tc_replica_error<'a>(rep: *mut TCReplica) -> *mut TCString<'st #[no_mangle] pub extern "C" fn tc_replica_free(rep: *mut TCReplica) { // SAFETY: - // - rep is not NULL - // - caller will not use the TCReplica after this + // - rep is not NULL (promised by caller) + // - caller will not use the TCReplica after this (promised by caller) let replica = unsafe { TCReplica::from_arg(rep) }; if replica.mut_borrowed { panic!("replica is borrowed and cannot be freed"); diff --git a/lib/src/task.rs b/lib/src/task.rs index 558843b17..14aa82746 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -10,6 +10,8 @@ use taskchampion::{Task, TaskMut}; /// /// A task carries no reference to the replica that created it, and can /// be used until it is freed or converted to a TaskMut. +/// +/// All `tc_task_..` functions taking a task as an argument require that it not be NULL. pub enum TCTask { /// A regular, immutable task Immutable(Task), @@ -202,7 +204,7 @@ pub extern "C" fn tc_task_get_status<'a>(task: *const TCTask) -> TCStatus { wrap(task, |task| task.get_status().into()) } -// TODO: get_taskmap +// TODO: tc_task_get_taskmap (?? then we have to wrap a map..) /// Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it /// contains embedded NUL characters). @@ -214,19 +216,25 @@ pub extern "C" fn tc_task_get_description<'a>(task: *const TCTask) -> *mut TCStr }) } -// TODO: :get_entry -// TODO: :get_wait -// TODO: :get_modified -// TODO: :is_waiting -// TODO: :is_active -// TODO: :has_tag -// TODO: :get_tags -// TODO: :get_annotations -// TODO: :get_uda -// TODO: :get_udas -// TODO: :get_legacy_uda -// TODO: :get_legacy_udas -// TODO: :get_modified +// TODO: tc_task_get_entry +// TODO: tc_task_get_wait +// TODO: tc_task_get_modified +// TODO: tc_task_is_waiting + +/// Check if a task is active (started and not stopped). +#[no_mangle] +pub extern "C" fn tc_task_is_active<'a>(task: *const TCTask) -> bool { + wrap(task, |task| task.is_active()) +} + +// TODO: tc_task_has_tag +// TODO: tc_task_get_tags +// TODO: tc_task_get_annotations +// TODO: tc_task_get_uda +// TODO: tc_task_get_udas +// TODO: tc_task_get_legacy_uda +// TODO: tc_task_get_legacy_udas +// TODO: tc_task_get_modified /// Set a mutable task's status. /// @@ -261,8 +269,29 @@ pub extern "C" fn tc_task_set_description<'a>( // TODO: tc_task_set_entry // TODO: tc_task_set_wait // TODO: tc_task_set_modified -// TODO: tc_task_start -// TODO: tc_task_stop + +/// Start a task. +/// +/// TODO: error +#[no_mangle] +pub extern "C" fn tc_task_start<'a>(task: *mut TCTask) { + wrap_mut(task, |task| { + task.start()?; + Ok(()) + }) +} + +/// Stop a task. +/// +/// TODO: error +#[no_mangle] +pub extern "C" fn tc_task_stop<'a>(task: *mut TCTask) { + wrap_mut(task, |task| { + task.stop()?; + Ok(()) + }) +} + // TODO: tc_task_done // TODO: tc_task_delete // TODO: tc_task_add_tag @@ -274,21 +303,19 @@ pub extern "C" fn tc_task_set_description<'a>( // TODO: tc_task_set_legacy_uda // TODO: tc_task_remove_legacy_uda -/// Free a task. The given task must not be NULL and must be immutable. The task must not be used -/// after this function returns, and must not be freed more than once. +/// Free a task. The given task must not be NULL. The task must not be used after this function +/// returns, and must not be freed more than once. /// -/// The restriction that the task must be immutable may be lifted (TODO) +/// If the task is currently mutable, it will first be made immutable. #[no_mangle] pub extern "C" fn tc_task_free<'a>(task: *mut TCTask) { // SAFETY: - // - rep is not NULL - // - caller will not use the TCTask after this - let tctask = unsafe { TCTask::from_arg(task) }; - if !matches!(tctask, TCTask::Immutable(_)) { - // this limit is in place because we require the caller to supply a pointer - // to the replica to make a task immutable, and no such pointer is available - // here. - panic!("Task must be immutable when freed"); - } + // - rep is not NULL (promised by caller) + // - caller will not use the TCTask after this (promised by caller) + let mut tctask = unsafe { TCTask::from_arg(task) }; + + // convert to immut if it was mutable + tctask.to_immut(); + drop(tctask); } diff --git a/lib/taskchampion.h b/lib/taskchampion.h index cda313171..65de06667 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -67,6 +67,8 @@ typedef struct TCString TCString; * * A task carries no reference to the replica that created it, and can * be used until it is freed or converted to a TaskMut. + * + * All `tc_task_..` functions taking a task as an argument require that it not be NULL. */ typedef struct TCTask TCTask; @@ -248,6 +250,11 @@ enum TCStatus tc_task_get_status(const struct TCTask *task); */ struct TCString *tc_task_get_description(const struct TCTask *task); +/** + * Check if a task is active (started and not stopped). + */ +bool tc_task_is_active(const struct TCTask *task); + /** * Set a mutable task's status. * @@ -263,10 +270,24 @@ bool tc_task_set_status(struct TCTask *task, enum TCStatus status); bool tc_task_set_description(struct TCTask *task, struct TCString *description); /** - * Free a task. The given task must not be NULL and must be immutable. The task must not be used - * after this function returns, and must not be freed more than once. + * Start a task. * - * The restriction that the task must be immutable may be lifted (TODO) + * TODO: error + */ +void tc_task_start(struct TCTask *task); + +/** + * Stop a task. + * + * TODO: error + */ +void tc_task_stop(struct TCTask *task); + +/** + * Free a task. The given task must not be NULL. The task must not be used after this function + * returns, and must not be freed more than once. + * + * If the task is currently mutable, it will first be made immutable. */ void tc_task_free(struct TCTask *task); From ef0bb2ced46cb6b2c831380a0497d3d3a80ecb67 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Mon, 31 Jan 2022 19:34:21 +0000 Subject: [PATCH 31/95] allow task setters to return error values --- integration-tests/src/bindings_tests/task.c | 10 +-- lib/src/result.rs | 7 +- lib/src/task.rs | 85 +++++++++++++-------- lib/taskchampion.h | 22 +++--- 4 files changed, 75 insertions(+), 49 deletions(-) diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index b75320df3..0a80bfaaf 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -41,7 +41,7 @@ static void test_task_free_mutable_task(void) { TCUuid uuid = tc_task_get_uuid(task); tc_task_to_mut(task, rep); - TEST_ASSERT_TRUE(tc_task_set_status(task, TC_STATUS_DELETED)); + TEST_ASSERT_EQUAL(TC_RESULT_TRUE, tc_task_set_status(task, TC_STATUS_DELETED)); TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task)); tc_task_free(task); // implicitly converts to immut @@ -68,7 +68,7 @@ static void test_task_get_set_status(void) { TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task)); tc_task_to_mut(task, rep); - TEST_ASSERT_TRUE(tc_task_set_status(task, TC_STATUS_DELETED)); + TEST_ASSERT_EQUAL(TC_RESULT_TRUE, tc_task_set_status(task, TC_STATUS_DELETED)); TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task)); // while mut tc_task_to_immut(task); TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task)); // while immut @@ -92,7 +92,7 @@ static void test_task_get_set_description(void) { TCString *desc; tc_task_to_mut(task, rep); - tc_task_set_description(task, tc_string_borrow("updated")); + TEST_ASSERT_EQUAL(TC_RESULT_TRUE, tc_task_set_description(task, tc_string_borrow("updated"))); TEST_ASSERT_TRUE(desc = tc_task_get_description(task)); TEST_ASSERT_NOT_NULL(desc); @@ -127,9 +127,9 @@ static void test_task_start_stop_is_active(void) { tc_task_to_mut(task, rep); TEST_ASSERT_FALSE(tc_task_is_active(task)); - tc_task_start(task); + TEST_ASSERT_EQUAL(TC_RESULT_TRUE, tc_task_start(task)); TEST_ASSERT_TRUE(tc_task_is_active(task)); - tc_task_stop(task); + TEST_ASSERT_EQUAL(TC_RESULT_TRUE, tc_task_stop(task)); TEST_ASSERT_FALSE(tc_task_is_active(task)); tc_task_free(task); diff --git a/lib/src/result.rs b/lib/src/result.rs index e807e4690..12f5e3e6a 100644 --- a/lib/src/result.rs +++ b/lib/src/result.rs @@ -1,10 +1,11 @@ +// TODO: make true = 1, false = 0, error = -1 /// A result combines a boolean success value with /// an error response. It is equivalent to `Result`. /// cbindgen:prefix-with-name /// cbindgen:rename-all=ScreamingSnakeCase #[repr(C)] pub enum TCResult { - True, - False, - Error, + Error = -1, + False = 0, + True = 1, } diff --git a/lib/src/task.rs b/lib/src/task.rs index 14aa82746..531f33173 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -1,4 +1,6 @@ -use crate::{replica::TCReplica, status::TCStatus, string::TCString, uuid::TCUuid}; +use crate::{ + replica::TCReplica, result::TCResult, status::TCStatus, string::TCString, uuid::TCUuid, +}; use std::ops::Deref; use taskchampion::{Task, TaskMut}; @@ -113,7 +115,8 @@ impl From for TCTask { } } -/// Utility function to get a shared reference to the underlying Task. +/// Utility function to get a shared reference to the underlying Task. All Task getters +/// are error-free, so this does not handle errors. fn wrap<'a, T, F>(task: *const TCTask, f: F) -> T where F: FnOnce(&Task) -> T, @@ -131,8 +134,9 @@ where } /// Utility function to get a mutable reference to the underlying Task. The -/// TCTask must be mutable. -fn wrap_mut<'a, T, F>(task: *mut TCTask, f: F) -> T +/// TCTask must be mutable. The inner function may use `?` syntax to return an +/// error, which will be represented with the `err_value` returned to C. +fn wrap_mut<'a, T, F>(task: *mut TCTask, f: F, err_value: T) -> T where F: FnOnce(&mut TaskMut) -> anyhow::Result, { @@ -145,8 +149,13 @@ where TCTask::Mutable(ref mut t, _) => t, TCTask::Invalid => unreachable!(), }; - // TODO: add TCTask error handling, like replica - f(task).unwrap() + match f(task) { + Ok(rv) => rv, + Err(e) => { + // TODO: add TCTask error handling, like replica + err_value + } + } } /// Convert an immutable task into a mutable task. @@ -238,31 +247,39 @@ pub extern "C" fn tc_task_is_active<'a>(task: *const TCTask) -> bool { /// Set a mutable task's status. /// -/// Returns false on error. +/// Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. #[no_mangle] -pub extern "C" fn tc_task_set_status<'a>(task: *mut TCTask, status: TCStatus) -> bool { - wrap_mut(task, |task| { - task.set_status(status.into())?; - Ok(true) - }) +pub extern "C" fn tc_task_set_status<'a>(task: *mut TCTask, status: TCStatus) -> TCResult { + wrap_mut( + task, + |task| { + task.set_status(status.into())?; + Ok(TCResult::True) + }, + TCResult::Error, + ) } /// Set a mutable task's description. /// -/// Returns false on error. +/// Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. #[no_mangle] pub extern "C" fn tc_task_set_description<'a>( task: *mut TCTask, description: *mut TCString, -) -> bool { +) -> TCResult { // SAFETY: // - tcstring is not NULL (promised by caller) // - caller is exclusive owner of tcstring (implicitly promised by caller) let description = unsafe { TCString::from_arg(description) }; - wrap_mut(task, |task| { - task.set_description(description.as_str()?.to_string())?; - Ok(true) - }) + wrap_mut( + task, + |task| { + task.set_description(description.as_str()?.to_string())?; + Ok(TCResult::True) + }, + TCResult::Error, + ) } // TODO: tc_task_set_description @@ -272,24 +289,32 @@ pub extern "C" fn tc_task_set_description<'a>( /// Start a task. /// -/// TODO: error +/// Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. #[no_mangle] -pub extern "C" fn tc_task_start<'a>(task: *mut TCTask) { - wrap_mut(task, |task| { - task.start()?; - Ok(()) - }) +pub extern "C" fn tc_task_start<'a>(task: *mut TCTask) -> TCResult { + wrap_mut( + task, + |task| { + task.start()?; + Ok(TCResult::True) + }, + TCResult::Error, + ) } /// Stop a task. /// -/// TODO: error +/// Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. #[no_mangle] -pub extern "C" fn tc_task_stop<'a>(task: *mut TCTask) { - wrap_mut(task, |task| { - task.stop()?; - Ok(()) - }) +pub extern "C" fn tc_task_stop<'a>(task: *mut TCTask) -> TCResult { + wrap_mut( + task, + |task| { + task.stop()?; + Ok(TCResult::True) + }, + TCResult::Error, + ) } // TODO: tc_task_done diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 65de06667..8330a1dbe 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -11,9 +11,9 @@ * an error response. It is equivalent to `Result`. */ typedef enum TCResult { - TC_RESULT_TRUE, - TC_RESULT_FALSE, - TC_RESULT_ERROR, + TC_RESULT_ERROR = -1, + TC_RESULT_FALSE = 0, + TC_RESULT_TRUE = 1, } TCResult; /** @@ -258,30 +258,30 @@ bool tc_task_is_active(const struct TCTask *task); /** * Set a mutable task's status. * - * Returns false on error. + * Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. */ -bool tc_task_set_status(struct TCTask *task, enum TCStatus status); +enum TCResult tc_task_set_status(struct TCTask *task, enum TCStatus status); /** * Set a mutable task's description. * - * Returns false on error. + * Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. */ -bool tc_task_set_description(struct TCTask *task, struct TCString *description); +enum TCResult tc_task_set_description(struct TCTask *task, struct TCString *description); /** * Start a task. * - * TODO: error + * Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. */ -void tc_task_start(struct TCTask *task); +enum TCResult tc_task_start(struct TCTask *task); /** * Stop a task. * - * TODO: error + * Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. */ -void tc_task_stop(struct TCTask *task); +enum TCResult tc_task_stop(struct TCTask *task); /** * Free a task. The given task must not be NULL. The task must not be used after this function From ce45c1004c678dff7c03619956248216e87f007d Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Mon, 31 Jan 2022 19:44:00 +0000 Subject: [PATCH 32/95] add tc_task_add_tag and check errors --- integration-tests/src/bindings_tests/task.c | 30 +++++++++++++++++++++ lib/src/task.rs | 27 ++++++++++++++++--- lib/taskchampion.h | 7 +++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 0a80bfaaf..bceb657b0 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -136,6 +136,35 @@ static void test_task_start_stop_is_active(void) { tc_replica_free(rep); } +// adding tags to a task works, and invalid tags are rejected +static void task_task_add_tag(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + tc_task_to_mut(task, rep); + + TEST_ASSERT_EQUAL(TC_RESULT_TRUE, tc_task_add_tag(task, tc_string_borrow("next"))); + + // invalid - synthetic tag + TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_task_add_tag(task, tc_string_borrow("PENDING"))); + // invald - not a valid tag string + TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_task_add_tag(task, tc_string_borrow("my tag"))); + // invald - not utf-8 + TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_task_add_tag(task, tc_string_borrow("\xf0\x28\x8c\x28"))); + + // TODO: check error messages + // TODO: test getting the tag + + tc_task_free(task); + tc_replica_free(rep); +} + int task_tests(void) { UNITY_BEGIN(); // each test case above should be named here, in order. @@ -144,5 +173,6 @@ int task_tests(void) { RUN_TEST(test_task_get_set_status); RUN_TEST(test_task_get_set_description); RUN_TEST(test_task_start_stop_is_active); + RUN_TEST(task_task_add_tag); return UNITY_END(); } diff --git a/lib/src/task.rs b/lib/src/task.rs index 531f33173..365bd88af 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -2,7 +2,8 @@ use crate::{ replica::TCReplica, result::TCResult, status::TCStatus, string::TCString, uuid::TCUuid, }; use std::ops::Deref; -use taskchampion::{Task, TaskMut}; +use std::str::FromStr; +use taskchampion::{Tag, Task, TaskMut}; /// A task, as publicly exposed by this library. /// @@ -282,7 +283,6 @@ pub extern "C" fn tc_task_set_description<'a>( ) } -// TODO: tc_task_set_description // TODO: tc_task_set_entry // TODO: tc_task_set_wait // TODO: tc_task_set_modified @@ -319,7 +319,28 @@ pub extern "C" fn tc_task_stop<'a>(task: *mut TCTask) -> TCResult { // TODO: tc_task_done // TODO: tc_task_delete -// TODO: tc_task_add_tag + +/// Add a tag to a mutable task. +/// +/// Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. +#[no_mangle] +pub extern "C" fn tc_task_add_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> TCResult { + // SAFETY: + // - tcstring is not NULL (promised by caller) + // - caller is exclusive owner of tcstring (implicitly promised by caller) + let tcstring = unsafe { TCString::from_arg(tag) }; + wrap_mut( + task, + |task| { + let tagstr = tcstring.as_str()?; + let tag = Tag::from_str(tagstr)?; + task.add_tag(&tag)?; + Ok(TCResult::True) + }, + TCResult::Error, + ) +} + // TODO: tc_task_remove_tag // TODO: tc_task_add_annotation // TODO: tc_task_remove_annotation diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 8330a1dbe..d2853484e 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -283,6 +283,13 @@ enum TCResult tc_task_start(struct TCTask *task); */ enum TCResult tc_task_stop(struct TCTask *task); +/** + * Add a tag to a mutable task. + * + * Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. + */ +enum TCResult tc_task_add_tag(struct TCTask *task, struct TCString *tag); + /** * Free a task. The given task must not be NULL. The task must not be used after this function * returns, and must not be freed more than once. From 2dc93580859ffac2d7488e3078861fa8c1958507 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Mon, 31 Jan 2022 19:57:05 +0000 Subject: [PATCH 33/95] add warn(unsafe_op_in_unsafe_fn) --- lib/src/lib.rs | 1 + lib/src/replica.rs | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 649febbd9..e1c3564ef 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,3 +1,4 @@ +#[warn(unsafe_op_in_unsafe_fn)] pub mod replica; pub mod result; pub mod status; diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 3dd1f0df9..7ffb0bf15 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -26,17 +26,20 @@ impl TCReplica { /// the lifetime promised by C. pub(crate) unsafe fn from_arg_ref<'a>(tcreplica: *mut TCReplica) -> &'a mut Self { debug_assert!(!tcreplica.is_null()); - &mut *tcreplica + // SAFETY: see doc comment + unsafe { &mut *tcreplica } } /// Take a TCReplica from C as an argument. /// /// # Safety /// - /// The pointer must not be NULL. The pointer becomes invalid before this function returns. + /// The pointer must not be NULL and must point to a valid replica. The pointer becomes + /// invalid before this function returns and must not be used afterward. pub(crate) unsafe fn from_arg(tcreplica: *mut TCReplica) -> Self { debug_assert!(!tcreplica.is_null()); - *Box::from_raw(tcreplica) + // SAFETY: see doc comment + unsafe { *Box::from_raw(tcreplica) } } /// Convert this to a return value for handing off to C. From b675cef99cd95015584728817924c1eba3020e2a Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Tue, 1 Feb 2022 00:35:02 +0000 Subject: [PATCH 34/95] add error handling for tasks --- integration-tests/src/bindings_tests/task.c | 13 ++- lib/src/lib.rs | 3 + lib/src/replica.rs | 5 +- lib/src/task.rs | 104 ++++++++++++-------- lib/src/util.rs | 5 + lib/taskchampion.h | 15 ++- 6 files changed, 93 insertions(+), 52 deletions(-) create mode 100644 lib/src/util.rs diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index bceb657b0..94024bc76 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -150,15 +150,26 @@ static void task_task_add_tag(void) { tc_task_to_mut(task, rep); TEST_ASSERT_EQUAL(TC_RESULT_TRUE, tc_task_add_tag(task, tc_string_borrow("next"))); + TEST_ASSERT_NULL(tc_task_error(task)); // invalid - synthetic tag TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_task_add_tag(task, tc_string_borrow("PENDING"))); + TCString *err = tc_task_error(task); + TEST_ASSERT_NOT_NULL(err); + tc_string_free(err); + // invald - not a valid tag string TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_task_add_tag(task, tc_string_borrow("my tag"))); + err = tc_task_error(task); + TEST_ASSERT_NOT_NULL(err); + tc_string_free(err); + // invald - not utf-8 TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_task_add_tag(task, tc_string_borrow("\xf0\x28\x8c\x28"))); + err = tc_task_error(task); + TEST_ASSERT_NOT_NULL(err); + tc_string_free(err); - // TODO: check error messages // TODO: test getting the tag tc_task_free(task); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index e1c3564ef..c011ed36e 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,3 +1,6 @@ +mod util; + +// TODO: #![..] #[warn(unsafe_op_in_unsafe_fn)] pub mod replica; pub mod result; diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 7ffb0bf15..7ec619495 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -1,3 +1,4 @@ +use crate::util::err_to_tcstring; use crate::{result::TCResult, status::TCStatus, string::TCString, task::TCTask, uuid::TCUuid}; use taskchampion::{Replica, StorageConfig, Uuid}; @@ -75,10 +76,6 @@ impl From for TCReplica { } } -fn err_to_tcstring(e: impl std::string::ToString) -> TCString<'static> { - TCString::from(e.to_string()) -} - /// Utility function to allow using `?` notation to return an error value. This makes /// a mutable borrow, because most Replica methods require a `&mut`. fn wrap<'a, T, F>(rep: *mut TCReplica, f: F, err_value: T) -> T diff --git a/lib/src/task.rs b/lib/src/task.rs index 365bd88af..870fc0f91 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -1,3 +1,4 @@ +use crate::util::err_to_tcstring; use crate::{ replica::TCReplica, result::TCResult, status::TCStatus, string::TCString, uuid::TCUuid, }; @@ -15,7 +16,15 @@ use taskchampion::{Tag, Task, TaskMut}; /// be used until it is freed or converted to a TaskMut. /// /// All `tc_task_..` functions taking a task as an argument require that it not be NULL. -pub enum TCTask { +pub struct TCTask { + /// The wrapped Task or TaskMut + inner: Inner, + + /// The error from the most recent operation, if any + error: Option>, +} + +enum Inner { /// A regular, immutable task Immutable(Task), @@ -37,19 +46,7 @@ impl TCTask { /// The pointer must not be NULL. It is the caller's responsibility to ensure that the /// lifetime assigned to the reference and the lifetime of the TCTask itself do not outlive /// the lifetime promised by C. - pub(crate) unsafe fn from_arg_ref<'a>(tctask: *const TCTask) -> &'a Self { - debug_assert!(!tctask.is_null()); - &*tctask - } - - /// Borrow a TCTask from C as an argument, allowing mutation. - /// - /// # Safety - /// - /// The pointer must not be NULL. It is the caller's responsibility to ensure that the - /// lifetime assigned to the reference and the lifetime of the TCTask itself do not outlive - /// the lifetime promised by C. - pub(crate) unsafe fn from_arg_ref_mut<'a>(tctask: *mut TCTask) -> &'a mut Self { + pub(crate) unsafe fn from_arg_ref<'a>(tctask: *mut TCTask) -> &'a mut Self { debug_assert!(!tctask.is_null()); &mut *tctask } @@ -77,60 +74,64 @@ impl TCTask { /// The tcreplica pointer must not be NULL, and the replica it points to must not /// be freed before TCTask.to_immut completes. unsafe fn to_mut(&mut self, tcreplica: *mut TCReplica) { - *self = match std::mem::replace(self, TCTask::Invalid) { - TCTask::Immutable(task) => { + self.inner = match std::mem::replace(&mut self.inner, Inner::Invalid) { + Inner::Immutable(task) => { // SAFETY: // - tcreplica is not null (promised by caller) // - tcreplica outlives the pointer in this variant (promised by caller) let tcreplica_ref: &mut TCReplica = TCReplica::from_arg_ref(tcreplica); let rep_ref = tcreplica_ref.borrow_mut(); - TCTask::Mutable(task.into_mut(rep_ref), tcreplica) + Inner::Mutable(task.into_mut(rep_ref), tcreplica) } - TCTask::Mutable(task, tcreplica) => TCTask::Mutable(task, tcreplica), - TCTask::Invalid => unreachable!(), + Inner::Mutable(task, tcreplica) => Inner::Mutable(task, tcreplica), + Inner::Invalid => unreachable!(), } } /// Make an mutable TCTask into a immutable TCTask. Does nothing if the task /// is already immutable. fn to_immut(&mut self) { - *self = match std::mem::replace(self, TCTask::Invalid) { - TCTask::Immutable(task) => TCTask::Immutable(task), - TCTask::Mutable(task, tcreplica) => { + self.inner = match std::mem::replace(&mut self.inner, Inner::Invalid) { + Inner::Immutable(task) => Inner::Immutable(task), + Inner::Mutable(task, tcreplica) => { // SAFETY: // - tcreplica is not null (promised by caller of to_mut, which created this // variant) // - tcreplica is still alive (promised by caller of to_mut) let tcreplica_ref: &mut TCReplica = unsafe { TCReplica::from_arg_ref(tcreplica) }; tcreplica_ref.release_borrow(); - TCTask::Immutable(task.into_immut()) + Inner::Immutable(task.into_immut()) } - TCTask::Invalid => unreachable!(), + Inner::Invalid => unreachable!(), } } } impl From for TCTask { fn from(task: Task) -> TCTask { - TCTask::Immutable(task) + TCTask { + inner: Inner::Immutable(task), + error: None, + } } } /// Utility function to get a shared reference to the underlying Task. All Task getters /// are error-free, so this does not handle errors. -fn wrap<'a, T, F>(task: *const TCTask, f: F) -> T +fn wrap<'a, T, F>(task: *mut TCTask, f: F) -> T where F: FnOnce(&Task) -> T, { // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) - let tctask: &'a TCTask = unsafe { TCTask::from_arg_ref(task) }; - let task: &'a Task = match tctask { - TCTask::Immutable(t) => t, - TCTask::Mutable(t, _) => t.deref(), - TCTask::Invalid => unreachable!(), + let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref(task) }; + let task: &'a Task = match &tctask.inner { + Inner::Immutable(t) => t, + Inner::Mutable(t, _) => t.deref(), + Inner::Invalid => unreachable!(), }; + tctask.error = None; f(task) } @@ -144,16 +145,17 @@ where // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) - let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; - let task: &'a mut TaskMut = match tctask { - TCTask::Immutable(_) => panic!("Task is immutable"), - TCTask::Mutable(ref mut t, _) => t, - TCTask::Invalid => unreachable!(), + let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref(task) }; + let task: &'a mut TaskMut = match tctask.inner { + Inner::Immutable(_) => panic!("Task is immutable"), + Inner::Mutable(ref mut t, _) => t, + Inner::Invalid => unreachable!(), }; + tctask.error = None; match f(task) { Ok(rv) => rv, Err(e) => { - // TODO: add TCTask error handling, like replica + tctask.error = Some(err_to_tcstring(e)); err_value } } @@ -180,7 +182,7 @@ pub extern "C" fn tc_task_to_mut<'a>(task: *mut TCTask, tcreplica: *mut TCReplic // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) - let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; + let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref(task) }; // SAFETY: // - tcreplica is not NULL (promised by caller) // - tcreplica lives until later call to to_immut via tc_task_to_immut (promised by caller, @@ -198,19 +200,19 @@ pub extern "C" fn tc_task_to_immut<'a>(task: *mut TCTask) { // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) - let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; + let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref(task) }; tctask.to_immut(); } /// Get a task's UUID. #[no_mangle] -pub extern "C" fn tc_task_get_uuid(task: *const TCTask) -> TCUuid { +pub extern "C" fn tc_task_get_uuid(task: *mut TCTask) -> TCUuid { wrap(task, |task| task.get_uuid().into()) } /// Get a task's status. #[no_mangle] -pub extern "C" fn tc_task_get_status<'a>(task: *const TCTask) -> TCStatus { +pub extern "C" fn tc_task_get_status<'a>(task: *mut TCTask) -> TCStatus { wrap(task, |task| task.get_status().into()) } @@ -219,7 +221,7 @@ pub extern "C" fn tc_task_get_status<'a>(task: *const TCTask) -> TCStatus { /// Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it /// contains embedded NUL characters). #[no_mangle] -pub extern "C" fn tc_task_get_description<'a>(task: *const TCTask) -> *mut TCString<'static> { +pub extern "C" fn tc_task_get_description<'a>(task: *mut TCTask) -> *mut TCString<'static> { wrap(task, |task| { let descr: TCString = task.get_description().into(); descr.return_val() @@ -233,7 +235,7 @@ pub extern "C" fn tc_task_get_description<'a>(task: *const TCTask) -> *mut TCStr /// Check if a task is active (started and not stopped). #[no_mangle] -pub extern "C" fn tc_task_is_active<'a>(task: *const TCTask) -> bool { +pub extern "C" fn tc_task_is_active<'a>(task: *mut TCTask) -> bool { wrap(task, |task| task.is_active()) } @@ -349,6 +351,22 @@ pub extern "C" fn tc_task_add_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> // TODO: tc_task_set_legacy_uda // TODO: tc_task_remove_legacy_uda +/// Get the latest error for a task, or NULL if the last operation succeeded. Subsequent calls +/// to this function will return NULL. The task pointer must not be NULL. The caller must free the +/// returned string. +#[no_mangle] +pub extern "C" fn tc_task_error<'a>(task: *mut TCTask) -> *mut TCString<'static> { + // SAFETY: + // - task is not null (promised by caller) + // - task outlives 'a (promised by caller) + let task: &'a mut TCTask = unsafe { TCTask::from_arg_ref(task) }; + if let Some(tcstring) = task.error.take() { + tcstring.return_val() + } else { + std::ptr::null_mut() + } +} + /// Free a task. The given task must not be NULL. The task must not be used after this function /// returns, and must not be freed more than once. /// diff --git a/lib/src/util.rs b/lib/src/util.rs new file mode 100644 index 000000000..0709d2640 --- /dev/null +++ b/lib/src/util.rs @@ -0,0 +1,5 @@ +use crate::string::TCString; + +pub(crate) fn err_to_tcstring(e: impl std::string::ToString) -> TCString<'static> { + TCString::from(e.to_string()) +} diff --git a/lib/taskchampion.h b/lib/taskchampion.h index d2853484e..33a710068 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -237,23 +237,23 @@ void tc_task_to_immut(struct TCTask *task); /** * Get a task's UUID. */ -struct TCUuid tc_task_get_uuid(const struct TCTask *task); +struct TCUuid tc_task_get_uuid(struct TCTask *task); /** * Get a task's status. */ -enum TCStatus tc_task_get_status(const struct TCTask *task); +enum TCStatus tc_task_get_status(struct TCTask *task); /** * Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it * contains embedded NUL characters). */ -struct TCString *tc_task_get_description(const struct TCTask *task); +struct TCString *tc_task_get_description(struct TCTask *task); /** * Check if a task is active (started and not stopped). */ -bool tc_task_is_active(const struct TCTask *task); +bool tc_task_is_active(struct TCTask *task); /** * Set a mutable task's status. @@ -290,6 +290,13 @@ enum TCResult tc_task_stop(struct TCTask *task); */ enum TCResult tc_task_add_tag(struct TCTask *task, struct TCString *tag); +/** + * Get the latest error for a task, or NULL if the last operation succeeded. Subsequent calls + * to this function will return NULL. The task pointer must not be NULL. The caller must free the + * returned string. + */ +struct TCString *tc_task_error(struct TCTask *task); + /** * Free a task. The given task must not be NULL. The task must not be used after this function * returns, and must not be freed more than once. From 22a6857c1bbc041e5b491ca82341bb6f84358d6d Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Tue, 1 Feb 2022 00:46:04 +0000 Subject: [PATCH 35/95] simplify TCResult to just two values --- .../src/bindings_tests/replica.c | 17 +++++- integration-tests/src/bindings_tests/task.c | 12 ++--- lib/src/replica.rs | 22 +++++--- lib/src/result.rs | 10 ++-- lib/src/task.rs | 25 ++++----- lib/taskchampion.h | 52 ++++++++++--------- 6 files changed, 77 insertions(+), 61 deletions(-) diff --git a/integration-tests/src/bindings_tests/replica.c b/integration-tests/src/bindings_tests/replica.c index 756c9acfe..bf3cdaa43 100644 --- a/integration-tests/src/bindings_tests/replica.c +++ b/integration-tests/src/bindings_tests/replica.c @@ -23,8 +23,20 @@ static void test_replica_creation_disk(void) { static void test_replica_undo_empty(void) { TCReplica *rep = tc_replica_new_in_memory(); TEST_ASSERT_NULL(tc_replica_error(rep)); - int rv = tc_replica_undo(rep); - TEST_ASSERT_EQUAL(TC_RESULT_FALSE, rv); + int undone; + int rv = tc_replica_undo(rep, &undone); + TEST_ASSERT_EQUAL(TC_RESULT_OK, rv); + TEST_ASSERT_EQUAL(0, undone); + TEST_ASSERT_NULL(tc_replica_error(rep)); + tc_replica_free(rep); +} + +// When tc_replica_undo is passed NULL for undone_out, it still succeeds +static void test_replica_undo_empty_null_undone_out(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + int rv = tc_replica_undo(rep, NULL); + TEST_ASSERT_EQUAL(TC_RESULT_OK, rv); TEST_ASSERT_NULL(tc_replica_error(rep)); tc_replica_free(rep); } @@ -110,6 +122,7 @@ int replica_tests(void) { RUN_TEST(test_replica_creation); RUN_TEST(test_replica_creation_disk); RUN_TEST(test_replica_undo_empty); + RUN_TEST(test_replica_undo_empty_null_undone_out); RUN_TEST(test_replica_task_creation); RUN_TEST(test_replica_task_import); RUN_TEST(test_replica_get_task_not_found); diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 94024bc76..9ca537760 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -41,7 +41,7 @@ static void test_task_free_mutable_task(void) { TCUuid uuid = tc_task_get_uuid(task); tc_task_to_mut(task, rep); - TEST_ASSERT_EQUAL(TC_RESULT_TRUE, tc_task_set_status(task, TC_STATUS_DELETED)); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_status(task, TC_STATUS_DELETED)); TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task)); tc_task_free(task); // implicitly converts to immut @@ -68,7 +68,7 @@ static void test_task_get_set_status(void) { TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task)); tc_task_to_mut(task, rep); - TEST_ASSERT_EQUAL(TC_RESULT_TRUE, tc_task_set_status(task, TC_STATUS_DELETED)); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_status(task, TC_STATUS_DELETED)); TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task)); // while mut tc_task_to_immut(task); TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task)); // while immut @@ -92,7 +92,7 @@ static void test_task_get_set_description(void) { TCString *desc; tc_task_to_mut(task, rep); - TEST_ASSERT_EQUAL(TC_RESULT_TRUE, tc_task_set_description(task, tc_string_borrow("updated"))); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_description(task, tc_string_borrow("updated"))); TEST_ASSERT_TRUE(desc = tc_task_get_description(task)); TEST_ASSERT_NOT_NULL(desc); @@ -127,9 +127,9 @@ static void test_task_start_stop_is_active(void) { tc_task_to_mut(task, rep); TEST_ASSERT_FALSE(tc_task_is_active(task)); - TEST_ASSERT_EQUAL(TC_RESULT_TRUE, tc_task_start(task)); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_start(task)); TEST_ASSERT_TRUE(tc_task_is_active(task)); - TEST_ASSERT_EQUAL(TC_RESULT_TRUE, tc_task_stop(task)); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_stop(task)); TEST_ASSERT_FALSE(tc_task_is_active(task)); tc_task_free(task); @@ -149,7 +149,7 @@ static void task_task_add_tag(void) { tc_task_to_mut(task, rep); - TEST_ASSERT_EQUAL(TC_RESULT_TRUE, tc_task_add_tag(task, tc_string_borrow("next"))); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_tag(task, tc_string_borrow("next"))); TEST_ASSERT_NULL(tc_task_error(task)); // invalid - synthetic tag diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 7ec619495..0d2a5af4b 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -6,6 +6,9 @@ use taskchampion::{Replica, StorageConfig, Uuid}; /// for querying and modifying that data. /// /// TCReplicas are not threadsafe. +/// +/// When a `tc_replica_..` function that returns a TCResult returns TC_RESULT_ERROR, then +/// `tc_replica_error` will return the error message. pub struct TCReplica { /// The wrapped Replica inner: Replica, @@ -213,18 +216,21 @@ pub extern "C" fn tc_replica_import_task_with_uuid( /// Undo local operations until the most recent UndoPoint. /// -/// Returns TC_RESULT_TRUE if an undo occurred, TC_RESULT_FALSE if there are no operations -/// to be undone, or TC_RESULT_ERROR on error. +/// If undone_out is not NULL, then on success it is set to 1 if operations were undone, or 0 if +/// there are no operations that can be done. #[no_mangle] -pub extern "C" fn tc_replica_undo<'a>(rep: *mut TCReplica) -> TCResult { +pub extern "C" fn tc_replica_undo<'a>(rep: *mut TCReplica, undone_out: *mut i32) -> TCResult { wrap( rep, |rep| { - Ok(if rep.undo()? { - TCResult::True - } else { - TCResult::False - }) + let undone = if rep.undo()? { 1 } else { 0 }; + if !undone_out.is_null() { + // SAFETY: + // - undone_out is not NULL (just checked) + // - undone_out is properly aligned (implicitly promised by caller) + unsafe { *undone_out = undone }; + } + Ok(TCResult::Ok) }, TCResult::Error, ) diff --git a/lib/src/result.rs b/lib/src/result.rs index 12f5e3e6a..a7d53ea8d 100644 --- a/lib/src/result.rs +++ b/lib/src/result.rs @@ -1,11 +1,9 @@ -// TODO: make true = 1, false = 0, error = -1 -/// A result combines a boolean success value with -/// an error response. It is equivalent to `Result`. +/// A result from a TC operation. Typically if this value is TC_RESULT_ERROR, +/// the associated object's `tc_.._error` method will return an error message. /// cbindgen:prefix-with-name /// cbindgen:rename-all=ScreamingSnakeCase -#[repr(C)] +#[repr(i32)] pub enum TCResult { Error = -1, - False = 0, - True = 1, + Ok = 0, } diff --git a/lib/src/task.rs b/lib/src/task.rs index 870fc0f91..ab79079a3 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -16,6 +16,11 @@ use taskchampion::{Tag, Task, TaskMut}; /// be used until it is freed or converted to a TaskMut. /// /// All `tc_task_..` functions taking a task as an argument require that it not be NULL. +/// +/// When a `tc_task_..` function that returns a TCResult returns TC_RESULT_ERROR, then +/// `tc_task_error` will return the error message. +/// +/// TCTasks are not threadsafe. pub struct TCTask { /// The wrapped Task or TaskMut inner: Inner, @@ -249,23 +254,19 @@ pub extern "C" fn tc_task_is_active<'a>(task: *mut TCTask) -> bool { // TODO: tc_task_get_modified /// Set a mutable task's status. -/// -/// Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. #[no_mangle] pub extern "C" fn tc_task_set_status<'a>(task: *mut TCTask, status: TCStatus) -> TCResult { wrap_mut( task, |task| { task.set_status(status.into())?; - Ok(TCResult::True) + Ok(TCResult::Ok) }, TCResult::Error, ) } /// Set a mutable task's description. -/// -/// Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. #[no_mangle] pub extern "C" fn tc_task_set_description<'a>( task: *mut TCTask, @@ -279,7 +280,7 @@ pub extern "C" fn tc_task_set_description<'a>( task, |task| { task.set_description(description.as_str()?.to_string())?; - Ok(TCResult::True) + Ok(TCResult::Ok) }, TCResult::Error, ) @@ -290,30 +291,26 @@ pub extern "C" fn tc_task_set_description<'a>( // TODO: tc_task_set_modified /// Start a task. -/// -/// Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. #[no_mangle] pub extern "C" fn tc_task_start<'a>(task: *mut TCTask) -> TCResult { wrap_mut( task, |task| { task.start()?; - Ok(TCResult::True) + Ok(TCResult::Ok) }, TCResult::Error, ) } /// Stop a task. -/// -/// Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. #[no_mangle] pub extern "C" fn tc_task_stop<'a>(task: *mut TCTask) -> TCResult { wrap_mut( task, |task| { task.stop()?; - Ok(TCResult::True) + Ok(TCResult::Ok) }, TCResult::Error, ) @@ -323,8 +320,6 @@ pub extern "C" fn tc_task_stop<'a>(task: *mut TCTask) -> TCResult { // TODO: tc_task_delete /// Add a tag to a mutable task. -/// -/// Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. #[no_mangle] pub extern "C" fn tc_task_add_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> TCResult { // SAFETY: @@ -337,7 +332,7 @@ pub extern "C" fn tc_task_add_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> let tagstr = tcstring.as_str()?; let tag = Tag::from_str(tagstr)?; task.add_tag(&tag)?; - Ok(TCResult::True) + Ok(TCResult::Ok) }, TCResult::Error, ) diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 33a710068..e2c3cef79 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -7,14 +7,20 @@ #define TC_UUID_STRING_BYTES 36 /** - * A result combines a boolean success value with - * an error response. It is equivalent to `Result`. + * A result from a TC operation. Typically if this value is TC_RESULT_ERROR, + * the associated object's `tc_.._error` method will return an error message. */ -typedef enum TCResult { +enum TCResult +#ifdef __cplusplus + : int32_t +#endif // __cplusplus + { TC_RESULT_ERROR = -1, - TC_RESULT_FALSE = 0, - TC_RESULT_TRUE = 1, -} TCResult; + TC_RESULT_OK = 0, +}; +#ifndef __cplusplus +typedef int32_t TCResult; +#endif // __cplusplus /** * The status of a task, as defined by the task data model. @@ -35,6 +41,9 @@ typedef enum TCStatus { * for querying and modifying that data. * * TCReplicas are not threadsafe. + * + * When a `tc_replica_..` function that returns a TCResult returns TC_RESULT_ERROR, then + * `tc_replica_error` will return the error message. */ typedef struct TCReplica TCReplica; @@ -69,6 +78,11 @@ typedef struct TCString TCString; * be used until it is freed or converted to a TaskMut. * * All `tc_task_..` functions taking a task as an argument require that it not be NULL. + * + * When a `tc_task_..` function that returns a TCResult returns TC_RESULT_ERROR, then + * `tc_task_error` will return the error message. + * + * TCTasks are not threadsafe. */ typedef struct TCTask TCTask; @@ -127,10 +141,10 @@ struct TCTask *tc_replica_import_task_with_uuid(struct TCReplica *rep, struct TC /** * Undo local operations until the most recent UndoPoint. * - * Returns TC_RESULT_TRUE if an undo occurred, TC_RESULT_FALSE if there are no operations - * to be undone, or TC_RESULT_ERROR on error. + * If undone_out is not NULL, then on success it is set to 1 if operations were undone, or 0 if + * there are no operations that can be done. */ -enum TCResult tc_replica_undo(struct TCReplica *rep); +TCResult tc_replica_undo(struct TCReplica *rep, int32_t *undone_out); /** * Get the latest error for a replica, or NULL if the last operation succeeded. Subsequent calls @@ -257,38 +271,28 @@ bool tc_task_is_active(struct TCTask *task); /** * Set a mutable task's status. - * - * Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. */ -enum TCResult tc_task_set_status(struct TCTask *task, enum TCStatus status); +TCResult tc_task_set_status(struct TCTask *task, enum TCStatus status); /** * Set a mutable task's description. - * - * Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. */ -enum TCResult tc_task_set_description(struct TCTask *task, struct TCString *description); +TCResult tc_task_set_description(struct TCTask *task, struct TCString *description); /** * Start a task. - * - * Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. */ -enum TCResult tc_task_start(struct TCTask *task); +TCResult tc_task_start(struct TCTask *task); /** * Stop a task. - * - * Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. */ -enum TCResult tc_task_stop(struct TCTask *task); +TCResult tc_task_stop(struct TCTask *task); /** * Add a tag to a mutable task. - * - * Returns TC_RESULT_TRUE on success and TC_RESULT_ERROR on failure. */ -enum TCResult tc_task_add_tag(struct TCTask *task, struct TCString *tag); +TCResult tc_task_add_tag(struct TCTask *task, struct TCString *tag); /** * Get the latest error for a task, or NULL if the last operation succeeded. Subsequent calls From 03ffb6ce83194aec3535b0a108db0fbb77430f84 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Tue, 1 Feb 2022 00:48:49 +0000 Subject: [PATCH 36/95] limit unsafe regions --- lib/src/lib.rs | 3 +-- lib/src/string.rs | 6 ++++-- lib/src/task.rs | 8 +++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/src/lib.rs b/lib/src/lib.rs index c011ed36e..7706f8d5c 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,7 +1,6 @@ +#![warn(unsafe_op_in_unsafe_fn)] mod util; -// TODO: #![..] -#[warn(unsafe_op_in_unsafe_fn)] pub mod replica; pub mod result; pub mod status; diff --git a/lib/src/string.rs b/lib/src/string.rs index df0eebddb..163c2caf8 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -52,7 +52,8 @@ impl<'a> TCString<'a> { /// the lifetime promised by C. pub(crate) unsafe fn from_arg(tcstring: *mut TCString<'a>) -> Self { debug_assert!(!tcstring.is_null()); - *(Box::from_raw(tcstring)) + // SAFETY: see docstring + unsafe { *(Box::from_raw(tcstring)) } } /// Borrow a TCString from C as an argument. @@ -64,7 +65,8 @@ impl<'a> TCString<'a> { /// the lifetime promised by C. pub(crate) unsafe fn from_arg_ref(tcstring: *mut TCString<'a>) -> &'a mut Self { debug_assert!(!tcstring.is_null()); - &mut *tcstring + // SAFETY: see docstring + unsafe { &mut *tcstring } } /// Get a regular Rust &str for this value. diff --git a/lib/src/task.rs b/lib/src/task.rs index ab79079a3..841ae83f5 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -53,7 +53,8 @@ impl TCTask { /// the lifetime promised by C. pub(crate) unsafe fn from_arg_ref<'a>(tctask: *mut TCTask) -> &'a mut Self { debug_assert!(!tctask.is_null()); - &mut *tctask + // SAFETY: see docstring + unsafe { &mut *tctask } } /// Take a TCTask from C as an argument. @@ -63,7 +64,8 @@ impl TCTask { /// The pointer must not be NULL. The pointer becomes invalid before this function returns. pub(crate) unsafe fn from_arg<'a>(tctask: *mut TCTask) -> Self { debug_assert!(!tctask.is_null()); - *Box::from_raw(tctask) + // SAFETY: see docstring + unsafe { *Box::from_raw(tctask) } } /// Convert a TCTask to a return value for handing off to C. @@ -84,7 +86,7 @@ impl TCTask { // SAFETY: // - tcreplica is not null (promised by caller) // - tcreplica outlives the pointer in this variant (promised by caller) - let tcreplica_ref: &mut TCReplica = TCReplica::from_arg_ref(tcreplica); + let tcreplica_ref: &mut TCReplica = unsafe { TCReplica::from_arg_ref(tcreplica) }; let rep_ref = tcreplica_ref.borrow_mut(); Inner::Mutable(task.into_mut(rep_ref), tcreplica) } From f2b3e5fd0a6a8fe4497fba03ced1626f9861bf07 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Tue, 1 Feb 2022 01:02:49 +0000 Subject: [PATCH 37/95] tc_task_has_tag --- integration-tests/src/bindings_tests/task.c | 6 +++- lib/src/task.rs | 31 +++++++++++++++++++-- lib/taskchampion.h | 6 ++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 9ca537760..7fcb9a683 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -149,9 +149,13 @@ static void task_task_add_tag(void) { tc_task_to_mut(task, rep); + TEST_ASSERT_FALSE(tc_task_has_tag(task, tc_string_borrow("next"))); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_tag(task, tc_string_borrow("next"))); TEST_ASSERT_NULL(tc_task_error(task)); + TEST_ASSERT_TRUE(tc_task_has_tag(task, tc_string_borrow("next"))); + // invalid - synthetic tag TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_task_add_tag(task, tc_string_borrow("PENDING"))); TCString *err = tc_task_error(task); @@ -170,7 +174,7 @@ static void task_task_add_tag(void) { TEST_ASSERT_NOT_NULL(err); tc_string_free(err); - // TODO: test getting the tag + TEST_ASSERT_TRUE(tc_task_has_tag(task, tc_string_borrow("next"))); tc_task_free(task); tc_replica_free(rep); diff --git a/lib/src/task.rs b/lib/src/task.rs index 841ae83f5..3558ccf20 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -2,6 +2,7 @@ use crate::util::err_to_tcstring; use crate::{ replica::TCReplica, result::TCResult, status::TCStatus, string::TCString, uuid::TCUuid, }; +use std::convert::TryFrom; use std::ops::Deref; use std::str::FromStr; use taskchampion::{Tag, Task, TaskMut}; @@ -168,6 +169,15 @@ where } } +impl TryFrom> for Tag { + type Error = anyhow::Error; + + fn try_from(tcstring: TCString) -> Result { + let tagstr = tcstring.as_str()?; + Tag::from_str(tagstr) + } +} + /// Convert an immutable task into a mutable task. /// /// The task must not be NULL. It is modified in-place, and becomes mutable. @@ -246,7 +256,23 @@ pub extern "C" fn tc_task_is_active<'a>(task: *mut TCTask) -> bool { wrap(task, |task| task.is_active()) } -// TODO: tc_task_has_tag +/// Check if a task has the given tag. If the tag is invalid, this function will simply return +/// false with no error from `tc_task_error`. The given tag must not be NULL. +#[no_mangle] +pub extern "C" fn tc_task_has_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> bool { + // SAFETY: + // - tcstring is not NULL (promised by caller) + // - caller is exclusive owner of tcstring (implicitly promised by caller) + let tcstring = unsafe { TCString::from_arg(tag) }; + wrap(task, |task| { + if let Ok(tag) = Tag::try_from(tcstring) { + task.has_tag(&tag) + } else { + false + } + }) +} + // TODO: tc_task_get_tags // TODO: tc_task_get_annotations // TODO: tc_task_get_uda @@ -331,8 +357,7 @@ pub extern "C" fn tc_task_add_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> wrap_mut( task, |task| { - let tagstr = tcstring.as_str()?; - let tag = Tag::from_str(tagstr)?; + let tag = Tag::try_from(tcstring)?; task.add_tag(&tag)?; Ok(TCResult::Ok) }, diff --git a/lib/taskchampion.h b/lib/taskchampion.h index e2c3cef79..a0b9e46d7 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -269,6 +269,12 @@ struct TCString *tc_task_get_description(struct TCTask *task); */ bool tc_task_is_active(struct TCTask *task); +/** + * Check if a task has the given tag. If the tag is invalid, this function will simply return + * false with no error from `tc_task_error`. The given tag must not be NULL. + */ +bool tc_task_has_tag(struct TCTask *task, struct TCString *tag); + /** * Set a mutable task's status. */ From e5625e1597bc57e4713d542b825da6429ccf2d1b Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Tue, 1 Feb 2022 02:45:28 +0000 Subject: [PATCH 38/95] entry and wait time support --- Cargo.lock | 1 + integration-tests/src/bindings_tests/task.c | 63 ++++++++++++++++++ lib/Cargo.toml | 3 +- lib/build.rs | 2 +- lib/src/task.rs | 74 +++++++++++++++++++-- lib/taskchampion.h | 33 ++++++++- taskchampion/src/replica.rs | 2 +- taskchampion/src/task/task.rs | 26 +++++++- 8 files changed, 190 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 094b83445..ad2920754 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3036,6 +3036,7 @@ version = "0.1.0" dependencies = [ "anyhow", "cbindgen", + "chrono", "libc", "taskchampion", "uuid", diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 7fcb9a683..8b264e41f 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -111,6 +111,67 @@ static void test_task_get_set_description(void) { tc_replica_free(rep); } +// updating entry on a task works +static void test_task_get_set_entry(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + // creation of a task sets entry to current time + TEST_ASSERT_NOT_EQUAL(0, tc_task_get_entry(task)); + + tc_task_to_mut(task, rep); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_entry(task, 1643679997)); + TEST_ASSERT_EQUAL(1643679997, tc_task_get_entry(task)); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_entry(task, 0)); + TEST_ASSERT_EQUAL(0, tc_task_get_entry(task)); + + tc_task_free(task); + + tc_replica_free(rep); +} + +// updating wait on a task works +static void test_task_get_set_wait_and_is_waiting(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + // wait is not set on creation + TEST_ASSERT_EQUAL(0, tc_task_get_wait(task)); + TEST_ASSERT_FALSE(tc_task_is_waiting(task)); + + tc_task_to_mut(task, rep); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_wait(task, 3643679997)); // 2085 + TEST_ASSERT_EQUAL(3643679997, tc_task_get_wait(task)); + TEST_ASSERT_TRUE(tc_task_is_waiting(task)); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_wait(task, 643679997)); // THE PAST! + TEST_ASSERT_EQUAL(643679997, tc_task_get_wait(task)); + TEST_ASSERT_FALSE(tc_task_is_waiting(task)); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_wait(task, 0)); + TEST_ASSERT_EQUAL(0, tc_task_get_wait(task)); + TEST_ASSERT_FALSE(tc_task_is_waiting(task)); + + tc_task_free(task); + + tc_replica_free(rep); +} + // starting and stopping a task works, as seen by tc_task_is_active static void test_task_start_stop_is_active(void) { TCReplica *rep = tc_replica_new_in_memory(); @@ -187,6 +248,8 @@ int task_tests(void) { RUN_TEST(test_task_free_mutable_task); RUN_TEST(test_task_get_set_status); RUN_TEST(test_task_get_set_description); + RUN_TEST(test_task_get_set_entry); + RUN_TEST(test_task_get_set_wait_and_is_waiting); RUN_TEST(test_task_start_stop_is_active); RUN_TEST(task_task_add_tag); return UNITY_END(); diff --git a/lib/Cargo.toml b/lib/Cargo.toml index b8545df90..66b8c6051 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -10,8 +10,9 @@ crate-type = ["cdylib"] [dependencies] libc = "0.2.113" +chrono = "^0.4.10" taskchampion = { path = "../taskchampion" } -uuid = { version = "^0.8.2", features = ["serde", "v4"] } +uuid = { version = "^0.8.2", features = ["v4"] } anyhow = "1.0" [build-dependencies] diff --git a/lib/build.rs b/lib/build.rs index 462b707a4..f8f75d3b5 100644 --- a/lib/build.rs +++ b/lib/build.rs @@ -10,7 +10,7 @@ fn main() { .with_config(Config { language: Language::C, cpp_compat: true, - sys_includes: vec!["stdbool.h".into(), "stdint.h".into()], + sys_includes: vec!["stdbool.h".into(), "stdint.h".into(), "time.h".into()], usize_is_size_t: true, no_includes: true, enumeration: EnumConfig { diff --git a/lib/src/task.rs b/lib/src/task.rs index 3558ccf20..03318d1f9 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -2,6 +2,7 @@ use crate::util::err_to_tcstring; use crate::{ replica::TCReplica, result::TCResult, status::TCStatus, string::TCString, uuid::TCUuid, }; +use chrono::{DateTime, TimeZone, Utc}; use std::convert::TryFrom; use std::ops::Deref; use std::str::FromStr; @@ -13,8 +14,9 @@ use taskchampion::{Tag, Task, TaskMut}; /// to make any changes, and doing so requires exclusive access to the replica /// until the task is freed or converted back to immutable mode. /// -/// A task carries no reference to the replica that created it, and can -/// be used until it is freed or converted to a TaskMut. +/// An immutable task carries no reference to the replica that created it, and can be used until it +/// is freed or converted to a TaskMut. A mutable task carries a reference to the replica and +/// must be freed or made immutable before the replica is freed. /// /// All `tc_task_..` functions taking a task as an argument require that it not be NULL. /// @@ -178,6 +180,22 @@ impl TryFrom> for Tag { } } +/// Convert a DateTime to a libc::time_t, or zero if not set. +fn to_time_t(timestamp: Option>) -> libc::time_t { + timestamp + .map(|ts| ts.timestamp() as libc::time_t) + .unwrap_or(0 as libc::time_t) +} + +/// Convert a libc::time_t to Option>, treating time zero as None +fn to_datetime(time: libc::time_t) -> Option> { + if time == 0 { + None + } else { + Some(Utc.timestamp(time as i64, 0)) + } +} + /// Convert an immutable task into a mutable task. /// /// The task must not be NULL. It is modified in-place, and becomes mutable. @@ -245,14 +263,29 @@ pub extern "C" fn tc_task_get_description<'a>(task: *mut TCTask) -> *mut TCStrin }) } -// TODO: tc_task_get_entry -// TODO: tc_task_get_wait +/// Get the entry timestamp for a task (when it was created), or 0 if not set. +#[no_mangle] +pub extern "C" fn tc_task_get_entry<'a>(task: *mut TCTask) -> libc::time_t { + wrap(task, |task| to_time_t(task.get_entry())) +} + +/// Get the wait timestamp for a task, or 0 if not set. +#[no_mangle] +pub extern "C" fn tc_task_get_wait<'a>(task: *mut TCTask) -> libc::time_t { + wrap(task, |task| to_time_t(task.get_wait())) +} + // TODO: tc_task_get_modified -// TODO: tc_task_is_waiting + +/// Check if a task is waiting. +#[no_mangle] +pub extern "C" fn tc_task_is_waiting(task: *mut TCTask) -> bool { + wrap(task, |task| task.is_waiting()) +} /// Check if a task is active (started and not stopped). #[no_mangle] -pub extern "C" fn tc_task_is_active<'a>(task: *mut TCTask) -> bool { +pub extern "C" fn tc_task_is_active(task: *mut TCTask) -> bool { wrap(task, |task| task.is_active()) } @@ -314,7 +347,34 @@ pub extern "C" fn tc_task_set_description<'a>( ) } -// TODO: tc_task_set_entry +/// Set a mutable task's entry (creation time). Pass entry=0 to unset +/// the entry field. +#[no_mangle] +pub extern "C" fn tc_task_set_entry<'a>(task: *mut TCTask, entry: libc::time_t) -> TCResult { + wrap_mut( + task, + |task| { + task.set_entry(to_datetime(entry))?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + +/// Set a mutable task's wait (creation time). Pass wait=0 to unset the +/// wait field. +#[no_mangle] +pub extern "C" fn tc_task_set_wait<'a>(task: *mut TCTask, wait: libc::time_t) -> TCResult { + wrap_mut( + task, + |task| { + task.set_wait(to_datetime(wait))?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + // TODO: tc_task_set_wait // TODO: tc_task_set_modified diff --git a/lib/taskchampion.h b/lib/taskchampion.h index a0b9e46d7..f102d03aa 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -1,5 +1,6 @@ #include #include +#include /** * Length, in bytes, of the string representation of a UUID (without NUL terminator) @@ -74,8 +75,9 @@ typedef struct TCString TCString; * to make any changes, and doing so requires exclusive access to the replica * until the task is freed or converted back to immutable mode. * - * A task carries no reference to the replica that created it, and can - * be used until it is freed or converted to a TaskMut. + * An immutable task carries no reference to the replica that created it, and can be used until it + * is freed or converted to a TaskMut. A mutable task carries a reference to the replica and + * must be freed or made immutable before the replica is freed. * * All `tc_task_..` functions taking a task as an argument require that it not be NULL. * @@ -264,6 +266,21 @@ enum TCStatus tc_task_get_status(struct TCTask *task); */ struct TCString *tc_task_get_description(struct TCTask *task); +/** + * Get the entry timestamp for a task (when it was created), or 0 if not set. + */ +time_t tc_task_get_entry(struct TCTask *task); + +/** + * Get the wait timestamp for a task, or 0 if not set. + */ +time_t tc_task_get_wait(struct TCTask *task); + +/** + * Check if a task is waiting. + */ +bool tc_task_is_waiting(struct TCTask *task); + /** * Check if a task is active (started and not stopped). */ @@ -285,6 +302,18 @@ TCResult tc_task_set_status(struct TCTask *task, enum TCStatus status); */ TCResult tc_task_set_description(struct TCTask *task, struct TCString *description); +/** + * Set a mutable task's entry (creation time). Pass entry=0 to unset + * the entry field. + */ +TCResult tc_task_set_entry(struct TCTask *task, time_t entry); + +/** + * Set a mutable task's wait (creation time). Pass wait=0 to unset the + * wait field. + */ +TCResult tc_task_set_wait(struct TCTask *task, time_t wait); + /** * Start a task. */ diff --git a/taskchampion/src/replica.rs b/taskchampion/src/replica.rs index b85cb84c7..2dd1e4887 100644 --- a/taskchampion/src/replica.rs +++ b/taskchampion/src/replica.rs @@ -110,7 +110,7 @@ impl Replica { let mut task = Task::new(uuid, taskmap).into_mut(self); task.set_description(description)?; task.set_status(status)?; - task.set_entry(Utc::now())?; + task.set_entry(Some(Utc::now()))?; trace!("task {} created", uuid); Ok(task.into_immut()) } diff --git a/taskchampion/src/task/task.rs b/taskchampion/src/task/task.rs index 82bafb7d7..d5517f5b4 100644 --- a/taskchampion/src/task/task.rs +++ b/taskchampion/src/task/task.rs @@ -120,6 +120,10 @@ impl Task { .unwrap_or("") } + pub fn get_entry(&self) -> Option> { + self.get_timestamp(Prop::Entry.as_ref()) + } + pub fn get_priority(&self) -> Priority { self.taskmap .get(Prop::Status.as_ref()) @@ -299,8 +303,8 @@ impl<'r> TaskMut<'r> { self.set_string(Prop::Description.as_ref(), Some(description)) } - pub(crate) fn set_entry(&mut self, entry: DateTime) -> anyhow::Result<()> { - self.set_timestamp(Prop::Entry.as_ref(), Some(entry)) + pub fn set_entry(&mut self, entry: Option>) -> anyhow::Result<()> { + self.set_timestamp(Prop::Entry.as_ref(), entry) } pub fn set_wait(&mut self, wait: Option>) -> anyhow::Result<()> { @@ -526,6 +530,24 @@ mod test { assert!(!task.is_active()); } + #[test] + fn test_entry_not_set() { + let task = Task::new(Uuid::new_v4(), TaskMap::new()); + assert_eq!(task.get_entry(), None); + } + + #[test] + fn test_entry_set() { + let ts = Utc.ymd(1980, 1, 1).and_hms(0, 0, 0); + let task = Task::new( + Uuid::new_v4(), + vec![(String::from("entry"), format!("{}", ts.timestamp()))] + .drain(..) + .collect(), + ); + assert_eq!(task.get_entry(), Some(ts)); + } + #[test] fn test_wait_not_set() { let task = Task::new(Uuid::new_v4(), TaskMap::new()); From 8b160c7ee82fe3e376fae15ff0c64db77fcb7372 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Tue, 1 Feb 2022 03:01:09 +0000 Subject: [PATCH 39/95] more task functions --- integration-tests/src/bindings_tests/task.c | 52 +++++++++++++++++ lib/src/task.rs | 63 ++++++++++++++++----- lib/taskchampion.h | 23 +++++++- 3 files changed, 123 insertions(+), 15 deletions(-) diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 8b264e41f..74dcfcda7 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -172,6 +172,33 @@ static void test_task_get_set_wait_and_is_waiting(void) { tc_replica_free(rep); } +// updating modified on a task works +static void test_task_get_set_modified(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + // creation of a task sets modified to current time + TEST_ASSERT_NOT_EQUAL(0, tc_task_get_modified(task)); + + tc_task_to_mut(task, rep); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_modified(task, 1643679997)); + TEST_ASSERT_EQUAL(1643679997, tc_task_get_modified(task)); + + // zero is not allowed + TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_task_set_modified(task, 0)); + + tc_task_free(task); + + tc_replica_free(rep); +} + // starting and stopping a task works, as seen by tc_task_is_active static void test_task_start_stop_is_active(void) { TCReplica *rep = tc_replica_new_in_memory(); @@ -197,6 +224,29 @@ static void test_task_start_stop_is_active(void) { tc_replica_free(rep); } +// tc_task_done and delete work and set the status +static void test_task_done_and_delete(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + tc_task_to_mut(task, rep); + + TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task)); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_done(task)); + TEST_ASSERT_EQUAL(TC_STATUS_COMPLETED, tc_task_get_status(task)); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_delete(task)); + TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task)); + + tc_task_free(task); + tc_replica_free(rep); +} + // adding tags to a task works, and invalid tags are rejected static void task_task_add_tag(void) { TCReplica *rep = tc_replica_new_in_memory(); @@ -249,8 +299,10 @@ int task_tests(void) { RUN_TEST(test_task_get_set_status); RUN_TEST(test_task_get_set_description); RUN_TEST(test_task_get_set_entry); + RUN_TEST(test_task_get_set_modified); RUN_TEST(test_task_get_set_wait_and_is_waiting); RUN_TEST(test_task_start_stop_is_active); + RUN_TEST(test_task_done_and_delete); RUN_TEST(task_task_add_tag); return UNITY_END(); } diff --git a/lib/src/task.rs b/lib/src/task.rs index 03318d1f9..4caa9a0c7 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -275,7 +275,11 @@ pub extern "C" fn tc_task_get_wait<'a>(task: *mut TCTask) -> libc::time_t { wrap(task, |task| to_time_t(task.get_wait())) } -// TODO: tc_task_get_modified +/// Get the modified timestamp for a task, or 0 if not set. +#[no_mangle] +pub extern "C" fn tc_task_get_modified<'a>(task: *mut TCTask) -> libc::time_t { + wrap(task, |task| to_time_t(task.get_modified())) +} /// Check if a task is waiting. #[no_mangle] @@ -312,7 +316,6 @@ pub extern "C" fn tc_task_has_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> // TODO: tc_task_get_udas // TODO: tc_task_get_legacy_uda // TODO: tc_task_get_legacy_udas -// TODO: tc_task_get_modified /// Set a mutable task's status. #[no_mangle] @@ -350,7 +353,7 @@ pub extern "C" fn tc_task_set_description<'a>( /// Set a mutable task's entry (creation time). Pass entry=0 to unset /// the entry field. #[no_mangle] -pub extern "C" fn tc_task_set_entry<'a>(task: *mut TCTask, entry: libc::time_t) -> TCResult { +pub extern "C" fn tc_task_set_entry(task: *mut TCTask, entry: libc::time_t) -> TCResult { wrap_mut( task, |task| { @@ -361,10 +364,9 @@ pub extern "C" fn tc_task_set_entry<'a>(task: *mut TCTask, entry: libc::time_t) ) } -/// Set a mutable task's wait (creation time). Pass wait=0 to unset the -/// wait field. +/// Set a mutable task's wait timestamp. Pass wait=0 to unset the wait field. #[no_mangle] -pub extern "C" fn tc_task_set_wait<'a>(task: *mut TCTask, wait: libc::time_t) -> TCResult { +pub extern "C" fn tc_task_set_wait(task: *mut TCTask, wait: libc::time_t) -> TCResult { wrap_mut( task, |task| { @@ -375,12 +377,24 @@ pub extern "C" fn tc_task_set_wait<'a>(task: *mut TCTask, wait: libc::time_t) -> ) } -// TODO: tc_task_set_wait -// TODO: tc_task_set_modified +/// Set a mutable task's modified timestamp. The value cannot be zero. +#[no_mangle] +pub extern "C" fn tc_task_set_modified(task: *mut TCTask, modified: libc::time_t) -> TCResult { + wrap_mut( + task, + |task| { + task.set_modified( + to_datetime(modified).ok_or_else(|| anyhow::anyhow!("modified cannot be zero"))?, + )?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} /// Start a task. #[no_mangle] -pub extern "C" fn tc_task_start<'a>(task: *mut TCTask) -> TCResult { +pub extern "C" fn tc_task_start(task: *mut TCTask) -> TCResult { wrap_mut( task, |task| { @@ -393,7 +407,7 @@ pub extern "C" fn tc_task_start<'a>(task: *mut TCTask) -> TCResult { /// Stop a task. #[no_mangle] -pub extern "C" fn tc_task_stop<'a>(task: *mut TCTask) -> TCResult { +pub extern "C" fn tc_task_stop(task: *mut TCTask) -> TCResult { wrap_mut( task, |task| { @@ -404,12 +418,35 @@ pub extern "C" fn tc_task_stop<'a>(task: *mut TCTask) -> TCResult { ) } -// TODO: tc_task_done -// TODO: tc_task_delete +/// Mark a task as done. +#[no_mangle] +pub extern "C" fn tc_task_done(task: *mut TCTask) -> TCResult { + wrap_mut( + task, + |task| { + task.done()?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + +/// Mark a task as deleted. +#[no_mangle] +pub extern "C" fn tc_task_delete(task: *mut TCTask) -> TCResult { + wrap_mut( + task, + |task| { + task.delete()?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} /// Add a tag to a mutable task. #[no_mangle] -pub extern "C" fn tc_task_add_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> TCResult { +pub extern "C" fn tc_task_add_tag(task: *mut TCTask, tag: *mut TCString) -> TCResult { // SAFETY: // - tcstring is not NULL (promised by caller) // - caller is exclusive owner of tcstring (implicitly promised by caller) diff --git a/lib/taskchampion.h b/lib/taskchampion.h index f102d03aa..4a3a33b96 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -276,6 +276,11 @@ time_t tc_task_get_entry(struct TCTask *task); */ time_t tc_task_get_wait(struct TCTask *task); +/** + * Get the modified timestamp for a task, or 0 if not set. + */ +time_t tc_task_get_modified(struct TCTask *task); + /** * Check if a task is waiting. */ @@ -309,11 +314,15 @@ TCResult tc_task_set_description(struct TCTask *task, struct TCString *descripti TCResult tc_task_set_entry(struct TCTask *task, time_t entry); /** - * Set a mutable task's wait (creation time). Pass wait=0 to unset the - * wait field. + * Set a mutable task's wait timestamp. Pass wait=0 to unset the wait field. */ TCResult tc_task_set_wait(struct TCTask *task, time_t wait); +/** + * Set a mutable task's modified timestamp. The value cannot be zero. + */ +TCResult tc_task_set_modified(struct TCTask *task, time_t modified); + /** * Start a task. */ @@ -324,6 +333,16 @@ TCResult tc_task_start(struct TCTask *task); */ TCResult tc_task_stop(struct TCTask *task); +/** + * Mark a task as done. + */ +TCResult tc_task_done(struct TCTask *task); + +/** + * Mark a task as deleted. + */ +TCResult tc_task_delete(struct TCTask *task); + /** * Add a tag to a mutable task. */ From 3dd2ae501194c248e943347717ca4167da4d3a57 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 2 Feb 2022 03:28:54 +0000 Subject: [PATCH 40/95] implement TCTags as an array --- integration-tests/src/bindings_tests/task.c | 49 ++++++++++- lib/src/arrays.rs | 98 +++++++++++++++++++++ lib/src/lib.rs | 1 + lib/src/string.rs | 4 +- lib/src/task.rs | 42 ++++++++- lib/taskchampion.h | 40 +++++++++ 6 files changed, 226 insertions(+), 8 deletions(-) create mode 100644 lib/src/arrays.rs diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 74dcfcda7..2ef89204c 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -247,8 +247,8 @@ static void test_task_done_and_delete(void) { tc_replica_free(rep); } -// adding tags to a task works, and invalid tags are rejected -static void task_task_add_tag(void) { +// adding and removing tags to a task works, and invalid tags are rejected +static void test_task_add_remove_has_tag(void) { TCReplica *rep = tc_replica_new_in_memory(); TEST_ASSERT_NULL(tc_replica_error(rep)); @@ -287,6 +287,48 @@ static void task_task_add_tag(void) { TEST_ASSERT_TRUE(tc_task_has_tag(task, tc_string_borrow("next"))); + // remove the tag + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_remove_tag(task, tc_string_borrow("next"))); + TEST_ASSERT_NULL(tc_task_error(task)); + + TEST_ASSERT_FALSE(tc_task_has_tag(task, tc_string_borrow("next"))); + + tc_task_free(task); + tc_replica_free(rep); +} + +// get_tags returns the list of tags +static void test_task_get_tags(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + tc_task_to_mut(task, rep); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_tag(task, tc_string_borrow("next"))); + + TCTags tags = tc_task_get_tags(task); + + int found_pending = false, found_next = false; + for (size_t i = 0; i < tags.num_tags; i++) { + if (strcmp("PENDING", tc_string_content(tags.tags[i])) == 0) { + found_pending = true; + } + if (strcmp("next", tc_string_content(tags.tags[i])) == 0) { + found_next = true; + } + } + TEST_ASSERT_TRUE(found_pending); + TEST_ASSERT_TRUE(found_next); + + tc_tags_free(&tags); + TEST_ASSERT_NULL(tags.tags); + tc_task_free(task); tc_replica_free(rep); } @@ -303,6 +345,7 @@ int task_tests(void) { RUN_TEST(test_task_get_set_wait_and_is_waiting); RUN_TEST(test_task_start_stop_is_active); RUN_TEST(test_task_done_and_delete); - RUN_TEST(task_task_add_tag); + RUN_TEST(test_task_add_remove_has_tag); + RUN_TEST(test_task_get_tags); return UNITY_END(); } diff --git a/lib/src/arrays.rs b/lib/src/arrays.rs new file mode 100644 index 000000000..e94ead43b --- /dev/null +++ b/lib/src/arrays.rs @@ -0,0 +1,98 @@ +use crate::string::TCString; +use std::ptr::NonNull; + +// TODO: generalize to TCStrings? + +/// TCTags represents a list of tags associated with a task. +/// +/// The content of this struct must be treated as read-only. +/// +/// The lifetime of a TCTags instance is independent of the task, and it +/// will remain valid even if the task is freed. +#[repr(C)] +pub struct TCTags { + // TODO: can we use NonNull here? + /// strings representing each tag. these remain owned by the + /// TCTags instance and will be freed by tc_tags_free. + tags: *const NonNull>, + /// number of tags in tags + num_tags: libc::size_t, + /// total size of tags (internal use only) + _capacity: libc::size_t, +} + +impl Default for TCTags { + fn default() -> Self { + Self { + tags: std::ptr::null_mut(), + num_tags: 0, + _capacity: 0, + } + } +} + +impl TCTags { + /// Create a Vec of TCStrings into a TCTags instance. + pub(crate) fn new(tags: Vec>>) -> Self { + // emulate Vec::into_raw_parts(): + // - disable dropping the Vec with ManuallyDrop + // - extract ptr, len, and capacity using those methods + let mut tags = std::mem::ManuallyDrop::new(tags); + Self { + tags: tags.as_mut_ptr(), + num_tags: tags.len(), + _capacity: tags.capacity(), + } + } + + /// Convert a TCTags to a Vec<_>. + /// + /// # Safety + /// + /// Tags must be _exactly_ as created by [`new`] + unsafe fn into_vec(self) -> Vec>> { + // SAFETY: + // + // * tags.tags needs to have been previously allocated via Vec<*mut TCString> + // * TCString needs to have the same size and alignment as what ptr was allocated with. + // * length needs to be less than or equal to capacity. + // * capacity needs to be the capacity that the pointer was allocated with. + // * vec elements are not NULL + // + // All of this is true for a value returned from `new`, which the caller promised + // not to change. + unsafe { Vec::from_raw_parts(self.tags as *mut _, self.num_tags, self._capacity) } + } +} + +/// Free a TCTags instance. The given pointer must not be NULL. The instance must not be used +/// after this call. +#[no_mangle] +pub extern "C" fn tc_tags_free<'a>(tctags: *mut TCTags) { + debug_assert!(!tctags.is_null()); + // SAFETY: + // - tctags is not NULL + // - tctags is valid (caller promises it has not been changed) + // - caller will not use the TCTags after this (promised by caller) + let tctags: &'a mut TCTags = unsafe { &mut *tctags }; + + debug_assert!(!tctags.tags.is_null()); + + // replace the caller's TCTags with one containing a NULL pointer + let tctags: TCTags = std::mem::take(tctags); + + // convert to a regular Vec + // SAFETY: + // - tctags is exactly as returned from TCTags::new (promised by caller) + let mut vec: Vec<_> = unsafe { tctags.into_vec() }; + + // drop each contained string + for tcstring in vec.drain(..) { + // SAFETY: + // - the pointer is not NULL (as created by TCString::new) + // - C does not expect the string's lifetime to exceed this function + drop(unsafe { TCString::from_arg(tcstring.as_ptr()) }); + } + + drop(vec); +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 7706f8d5c..e72f4bf9b 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,6 +1,7 @@ #![warn(unsafe_op_in_unsafe_fn)] mod util; +pub mod arrays; pub mod replica; pub mod result; pub mod status; diff --git a/lib/src/string.rs b/lib/src/string.rs index 163c2caf8..e711966a3 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -108,8 +108,8 @@ impl<'a> From for TCString<'a> { } } -impl<'a> From<&str> for TCString<'a> { - fn from(string: &str) -> TCString<'a> { +impl<'a> From<&str> for TCString<'static> { + fn from(string: &str) -> TCString<'static> { TCString::String(string.to_string()) } } diff --git a/lib/src/task.rs b/lib/src/task.rs index 4caa9a0c7..6c305792d 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -1,13 +1,17 @@ use crate::util::err_to_tcstring; use crate::{ - replica::TCReplica, result::TCResult, status::TCStatus, string::TCString, uuid::TCUuid, + arrays::TCTags, replica::TCReplica, result::TCResult, status::TCStatus, string::TCString, + uuid::TCUuid, }; use chrono::{DateTime, TimeZone, Utc}; use std::convert::TryFrom; use std::ops::Deref; +use std::ptr::NonNull; use std::str::FromStr; use taskchampion::{Tag, Task, TaskMut}; +// TODO: use NonNull more + /// A task, as publicly exposed by this library. /// /// A task begins in "immutable" mode. It must be converted to "mutable" mode @@ -310,7 +314,22 @@ pub extern "C" fn tc_task_has_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> }) } -// TODO: tc_task_get_tags +/// Get the tags for the task. The task must not be NULL. +#[no_mangle] +pub extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCTags { + wrap(task, |task| { + let tcstrings: Vec>> = task + .get_tags() + .map(|t| { + let t_ptr = TCString::from(t.as_ref()).return_val(); + // SAFETY: t_ptr was just created and is not NULL + unsafe { NonNull::new_unchecked(t_ptr) } + }) + .collect(); + TCTags::new(tcstrings) + }) +} + // TODO: tc_task_get_annotations // TODO: tc_task_get_uda // TODO: tc_task_get_udas @@ -462,7 +481,24 @@ pub extern "C" fn tc_task_add_tag(task: *mut TCTask, tag: *mut TCString) -> TCRe ) } -// TODO: tc_task_remove_tag +/// Remove a tag from a mutable task. +#[no_mangle] +pub extern "C" fn tc_task_remove_tag(task: *mut TCTask, tag: *mut TCString) -> TCResult { + // SAFETY: + // - tcstring is not NULL (promised by caller) + // - caller is exclusive owner of tcstring (implicitly promised by caller) + let tcstring = unsafe { TCString::from_arg(tag) }; + wrap_mut( + task, + |task| { + let tag = Tag::try_from(tcstring)?; + task.remove_tag(&tag)?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + // TODO: tc_task_add_annotation // TODO: tc_task_remove_annotation // TODO: tc_task_set_uda diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 4a3a33b96..854119bcc 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -88,6 +88,30 @@ typedef struct TCString TCString; */ typedef struct TCTask TCTask; +/** + * TCTags represents a list of tags associated with a task. + * + * The content of this struct must be treated as read-only. + * + * The lifetime of a TCTags instance is independent of the task, and it + * will remain valid even if the task is freed. + */ +typedef struct TCTags { + /** + * strings representing each tag. these remain owned by the + * TCTags instance and will be freed by tc_tags_free. + */ + struct TCString *const *tags; + /** + * number of tags in tags + */ + size_t num_tags; + /** + * total size of tags (internal use only; do not change) + */ + size_t _capacity; +} TCTags; + /** * TCUuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. * Uuids are typically treated as opaque, but the bytes are available in big-endian format. @@ -101,6 +125,12 @@ typedef struct TCUuid { extern "C" { #endif // __cplusplus +/** + * Free a TCTags instance. The given pointer must not be NULL. The instance must not be used + * after this call. + */ +void tc_tags_free(struct TCTags *tctags); + /** * Create a new TCReplica with an in-memory database. The contents of the database will be * lost when it is freed. @@ -297,6 +327,11 @@ bool tc_task_is_active(struct TCTask *task); */ bool tc_task_has_tag(struct TCTask *task, struct TCString *tag); +/** + * Get the tags for the task. The task must not be NULL. + */ +struct TCTags tc_task_get_tags(struct TCTask *task); + /** * Set a mutable task's status. */ @@ -348,6 +383,11 @@ TCResult tc_task_delete(struct TCTask *task); */ TCResult tc_task_add_tag(struct TCTask *task, struct TCString *tag); +/** + * Remove a tag from a mutable task. + */ +TCResult tc_task_remove_tag(struct TCTask *task, struct TCString *tag); + /** * Get the latest error for a task, or NULL if the last operation succeeded. Subsequent calls * to this function will return NULL. The task pointer must not be NULL. The caller must free the From a46a9d587a9cfa5dc30e5e7fbc436800b4161029 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sat, 5 Feb 2022 00:23:53 +0000 Subject: [PATCH 41/95] fix typo --- integration-tests/README.md | 2 +- lib/taskchampion.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/README.md b/integration-tests/README.md index b0a94d050..cbec3515a 100644 --- a/integration-tests/README.md +++ b/integration-tests/README.md @@ -26,5 +26,5 @@ Keep the `RUN_TEST`s in the same order as the functions they call. To add a suite, -1. Add a new C file in `integration-tests/src/bindings_tests/`, based off of one of hte others. +1. Add a new C file in `integration-tests/src/bindings_tests/`, based off of one of the others. 1. Add a the suite name to `suites` in `integration-tests/build.rs`. diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 854119bcc..c001538a6 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -107,7 +107,7 @@ typedef struct TCTags { */ size_t num_tags; /** - * total size of tags (internal use only; do not change) + * total size of tags (internal use only) */ size_t _capacity; } TCTags; From 23ba6a57b3916c25a7ebef99b58fceed8de01f94 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 6 Feb 2022 04:02:53 +0000 Subject: [PATCH 42/95] switch to PassByValue and PassByPointer traits --- lib/src/arrays.rs | 7 ++- lib/src/lib.rs | 2 + lib/src/replica.rs | 104 ++++++++++++++++++--------------------------- lib/src/string.rs | 98 +++++++++++++++++++----------------------- lib/src/task.rs | 44 +++++++++---------- lib/src/uuid.rs | 45 +++++++++++--------- lib/taskchampion.h | 79 ++++++++++++++++++++++------------ 7 files changed, 187 insertions(+), 192 deletions(-) diff --git a/lib/src/arrays.rs b/lib/src/arrays.rs index e94ead43b..3c266cb3c 100644 --- a/lib/src/arrays.rs +++ b/lib/src/arrays.rs @@ -1,4 +1,5 @@ use crate::string::TCString; +use crate::traits::*; use std::ptr::NonNull; // TODO: generalize to TCStrings? @@ -88,10 +89,8 @@ pub extern "C" fn tc_tags_free<'a>(tctags: *mut TCTags) { // drop each contained string for tcstring in vec.drain(..) { - // SAFETY: - // - the pointer is not NULL (as created by TCString::new) - // - C does not expect the string's lifetime to exceed this function - drop(unsafe { TCString::from_arg(tcstring.as_ptr()) }); + // SAFETY: see TCString docstring + drop(unsafe { TCString::take_from_arg(tcstring.as_ptr()) }); } drop(vec); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index e72f4bf9b..18cba4ac9 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,4 +1,6 @@ #![warn(unsafe_op_in_unsafe_fn)] + +mod traits; mod util; pub mod arrays; diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 0d2a5af4b..7512d1f91 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -1,3 +1,4 @@ +use crate::traits::*; use crate::util::err_to_tcstring; use crate::{result::TCResult, status::TCStatus, string::TCString, task::TCTask, uuid::TCUuid}; use taskchampion::{Replica, StorageConfig, Uuid}; @@ -5,10 +6,25 @@ use taskchampion::{Replica, StorageConfig, Uuid}; /// A replica represents an instance of a user's task data, providing an easy interface /// for querying and modifying that data. /// -/// TCReplicas are not threadsafe. +/// # Error Handling /// /// When a `tc_replica_..` function that returns a TCResult returns TC_RESULT_ERROR, then /// `tc_replica_error` will return the error message. +/// +/// # Safety +/// +/// The `*TCReplica` returned from `tc_replica_new…` functions is owned by the caller and +/// must later be freed to avoid a memory leak. +/// +/// Any function taking a `*TCReplica` requires: +/// - the pointer must not be NUL; +/// - the pointer must be one previously returned from a tc_… function; +/// - the memory referenced by the pointer must never be modified by C code; and +/// - except for `tc_replica_free`, ownership of a `*TCReplica` remains with the caller. +/// +/// Once passed to `tc_replica_free`, a `*TCReplica` becmes invalid and must not be used again. +/// +/// TCReplicas are not threadsafe. pub struct TCReplica { /// The wrapped Replica inner: Replica, @@ -20,37 +36,9 @@ pub struct TCReplica { error: Option>, } +impl PassByPointer for TCReplica {} + impl TCReplica { - /// Borrow a TCReplica from C as an argument. - /// - /// # Safety - /// - /// The pointer must not be NULL. It is the caller's responsibility to ensure that the - /// lifetime assigned to the reference and the lifetime of the TCReplica itself do not outlive - /// the lifetime promised by C. - pub(crate) unsafe fn from_arg_ref<'a>(tcreplica: *mut TCReplica) -> &'a mut Self { - debug_assert!(!tcreplica.is_null()); - // SAFETY: see doc comment - unsafe { &mut *tcreplica } - } - - /// Take a TCReplica from C as an argument. - /// - /// # Safety - /// - /// The pointer must not be NULL and must point to a valid replica. The pointer becomes - /// invalid before this function returns and must not be used afterward. - pub(crate) unsafe fn from_arg(tcreplica: *mut TCReplica) -> Self { - debug_assert!(!tcreplica.is_null()); - // SAFETY: see doc comment - unsafe { *Box::from_raw(tcreplica) } - } - - /// Convert this to a return value for handing off to C. - pub(crate) fn return_val(self) -> *mut TCReplica { - Box::into_raw(Box::new(self)) - } - /// Mutably borrow the inner Replica pub(crate) fn borrow_mut(&mut self) -> &mut Replica { if self.mut_borrowed { @@ -85,10 +73,8 @@ fn wrap<'a, T, F>(rep: *mut TCReplica, f: F, err_value: T) -> T where F: FnOnce(&mut Replica) -> anyhow::Result, { - // SAFETY: - // - rep is not null (promised by caller) - // - rep outlives 'a (promised by caller) - let rep: &'a mut TCReplica = unsafe { TCReplica::from_arg_ref(rep) }; + // SAFETY: see type docstring + let rep: &'a mut TCReplica = unsafe { TCReplica::from_arg_ref_mut(rep) }; if rep.mut_borrowed { panic!("replica is borrowed and cannot be used"); } @@ -109,21 +95,19 @@ pub extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica { let storage = StorageConfig::InMemory .into_storage() .expect("in-memory always succeeds"); - TCReplica::from(Replica::new(storage)).return_val() + // SAFETY: see type docstring + unsafe { TCReplica::from(Replica::new(storage)).return_val() } } -/// Create a new TCReplica with an on-disk database having the given filename. The filename must -/// not be NULL. On error, a string is written to the `error_out` parameter (if it is not NULL) and -/// NULL is returned. +/// Create a new TCReplica with an on-disk database having the given filename. On error, a string +/// is written to the `error_out` parameter (if it is not NULL) and NULL is returned. #[no_mangle] pub extern "C" fn tc_replica_new_on_disk<'a>( path: *mut TCString, error_out: *mut *mut TCString, ) -> *mut TCReplica { - // SAFETY: - // - tcstring is not NULL (promised by caller) - // - caller is exclusive owner of tcstring (implicitly promised by caller) - let path = unsafe { TCString::from_arg(path) }; + // SAFETY: see TCString docstring + let path = unsafe { TCString::take_from_arg(path) }; let storage_res = StorageConfig::OnDisk { taskdb_dir: path.to_path_buf(), } @@ -141,7 +125,8 @@ pub extern "C" fn tc_replica_new_on_disk<'a>( } }; - TCReplica::from(Replica::new(storage)).return_val() + // SAFETY: see type docstring + unsafe { TCReplica::from(Replica::new(storage)).return_val() } } // TODO: tc_replica_all_tasks @@ -153,11 +138,11 @@ pub extern "C" fn tc_replica_new_on_disk<'a>( /// Returns NULL when the task does not exist, and on error. Consult tc_replica_error /// to distinguish the two conditions. #[no_mangle] -pub extern "C" fn tc_replica_get_task(rep: *mut TCReplica, uuid: TCUuid) -> *mut TCTask { +pub extern "C" fn tc_replica_get_task(rep: *mut TCReplica, tcuuid: TCUuid) -> *mut TCTask { wrap( rep, |rep| { - let uuid: Uuid = uuid.into(); + let uuid = Uuid::from_arg(tcuuid); if let Some(task) = rep.get_task(uuid)? { Ok(TCTask::from(task).return_val()) } else { @@ -170,8 +155,6 @@ pub extern "C" fn tc_replica_get_task(rep: *mut TCReplica, uuid: TCUuid) -> *mut /// Create a new task. The task must not already exist. /// -/// The description must not be NULL. -/// /// Returns the task, or NULL on error. #[no_mangle] pub extern "C" fn tc_replica_new_task( @@ -179,10 +162,8 @@ pub extern "C" fn tc_replica_new_task( status: TCStatus, description: *mut TCString, ) -> *mut TCTask { - // SAFETY: - // - tcstring is not NULL (promised by caller) - // - caller is exclusive owner of tcstring (implicitly promised by caller) - let description = unsafe { TCString::from_arg(description) }; + // SAFETY: see TCString docstring + let description = unsafe { TCString::take_from_arg(description) }; wrap( rep, |rep| { @@ -199,12 +180,12 @@ pub extern "C" fn tc_replica_new_task( #[no_mangle] pub extern "C" fn tc_replica_import_task_with_uuid( rep: *mut TCReplica, - uuid: TCUuid, + tcuuid: TCUuid, ) -> *mut TCTask { wrap( rep, |rep| { - let uuid: Uuid = uuid.into(); + let uuid = Uuid::from_arg(tcuuid); let task = rep.import_task_with_uuid(uuid)?; Ok(TCTask::from(task).return_val()) }, @@ -241,12 +222,11 @@ pub extern "C" fn tc_replica_undo<'a>(rep: *mut TCReplica, undone_out: *mut i32) /// returned string. #[no_mangle] pub extern "C" fn tc_replica_error<'a>(rep: *mut TCReplica) -> *mut TCString<'static> { - // SAFETY: - // - rep is not null (promised by caller) - // - rep outlives 'a (promised by caller) - let rep: &'a mut TCReplica = unsafe { TCReplica::from_arg_ref(rep) }; + // SAFETY: see type docstring + let rep: &'a mut TCReplica = unsafe { TCReplica::from_arg_ref_mut(rep) }; if let Some(tcstring) = rep.error.take() { - tcstring.return_val() + // SAFETY: see TCString docstring + unsafe { tcstring.return_val() } } else { std::ptr::null_mut() } @@ -256,10 +236,8 @@ pub extern "C" fn tc_replica_error<'a>(rep: *mut TCReplica) -> *mut TCString<'st /// more than once. #[no_mangle] pub extern "C" fn tc_replica_free(rep: *mut TCReplica) { - // SAFETY: - // - rep is not NULL (promised by caller) - // - caller will not use the TCReplica after this (promised by caller) - let replica = unsafe { TCReplica::from_arg(rep) }; + // SAFETY: see type docstring + let replica = unsafe { TCReplica::take_from_arg(rep) }; if replica.mut_borrowed { panic!("replica is borrowed and cannot be freed"); } diff --git a/lib/src/string.rs b/lib/src/string.rs index e711966a3..d3f952189 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -1,24 +1,39 @@ +use crate::traits::*; use std::ffi::{CStr, CString, OsStr}; use std::os::unix::ffi::OsStrExt; use std::path::PathBuf; use std::str::Utf8Error; -/// TCString supports passing strings into and out of the TaskChampion API. A string can contain -/// embedded NUL characters. Strings containing such embedded NULs cannot be represented as a "C -/// string" and must be accessed using `tc_string_content_and_len` and `tc_string_clone_with_len`. -/// In general, these two functions should be used for handling arbitrary data, while more -/// convenient forms may be used where embedded NUL characters are impossible, such as in static -/// strings. +/// TCString supports passing strings into and out of the TaskChampion API. /// -/// Rust expects all strings to be UTF-8, and API functions will fail if given a TCString -/// containing invalid UTF-8. +/// # Rust Strings and C Strings /// -/// Unless specified otherwise, functions in this API take ownership of a TCString when it is given -/// as a function argument, and free the string before returning. Callers must not use or free -/// strings after passing them to such API functions. +/// A Rust string can contain embedded NUL characters, while C considers such a character to mark +/// the end of a string. Strings containing embedded NULs cannot be represented as a "C string" +/// and must be accessed using `tc_string_content_and_len` and `tc_string_clone_with_len`. In +/// general, these two functions should be used for handling arbitrary data, while more convenient +/// forms may be used where embedded NUL characters are impossible, such as in static strings. /// -/// When a TCString appears as a return value or output argument, it is the responsibility of the -/// caller to free the string. +/// # UTF-8 +/// +/// TaskChampion expects all strings to be valid UTF-8. `tc_string_…` functions will fail if given +/// a `*TCString` containing invalid UTF-8. +/// +/// # Safety +/// +/// When a `*TCString` appears as a return value or output argument, ownership is passed to the +/// caller. The caller must pass that ownerhsip back to another function or free the string. +/// +/// Any function taking a `*TCReplica` requires: +/// - the pointer must not be NUL; +/// - the pointer must be one previously returned from a tc_… function; and +/// - the memory referenced by the pointer must never be modified by C code. +/// +/// Unless specified otherwise, TaskChampion functions take ownership of a `*TCString` when it is +/// given as a function argument, and the pointer is invalid when the function returns. Callers +/// must not use or free TCStrings after passing them to such API functions. +/// +/// TCStrings are not threadsafe. pub enum TCString<'a> { CString(CString), CStr(&'a CStr), @@ -39,36 +54,9 @@ impl<'a> Default for TCString<'a> { } } +impl<'a> PassByPointer for TCString<'a> {} + impl<'a> TCString<'a> { - /// Take a TCString from C as an argument. - /// - /// C callers generally expect TC functions to take ownership of a string, which is what this - /// function does. - /// - /// # Safety - /// - /// The pointer must not be NULL. It is the caller's responsibility to ensure that the - /// lifetime assigned to the reference and the lifetime of the TCString itself do not outlive - /// the lifetime promised by C. - pub(crate) unsafe fn from_arg(tcstring: *mut TCString<'a>) -> Self { - debug_assert!(!tcstring.is_null()); - // SAFETY: see docstring - unsafe { *(Box::from_raw(tcstring)) } - } - - /// Borrow a TCString from C as an argument. - /// - /// # Safety - /// - /// The pointer must not be NULL. It is the caller's responsibility to ensure that the - /// lifetime assigned to the reference and the lifetime of the TCString itself do not outlive - /// the lifetime promised by C. - pub(crate) unsafe fn from_arg_ref(tcstring: *mut TCString<'a>) -> &'a mut Self { - debug_assert!(!tcstring.is_null()); - // SAFETY: see docstring - unsafe { &mut *tcstring } - } - /// Get a regular Rust &str for this value. pub(crate) fn as_str(&self) -> Result<&str, std::str::Utf8Error> { match self { @@ -95,11 +83,6 @@ impl<'a> TCString<'a> { let path: &OsStr = OsStr::from_bytes(self.as_bytes()); path.to_os_string().into() } - - /// Convert this to a return value for handing off to C. - pub(crate) fn return_val(self) -> *mut TCString<'a> { - Box::into_raw(Box::new(self)) - } } impl<'a> From for TCString<'a> { @@ -137,7 +120,8 @@ pub extern "C" fn tc_string_borrow(cstr: *const libc::c_char) -> *mut TCString<' // - cstr contains a valid NUL terminator (promised by caller) // - cstr's content will not change before it is destroyed (promised by caller) let cstr: &CStr = unsafe { CStr::from_ptr(cstr) }; - TCString::CStr(cstr).return_val() + // SAFETY: see docstring + unsafe { TCString::CStr(cstr).return_val() } } /// Create a new TCString by cloning the content of the given C string. The resulting TCString @@ -151,7 +135,8 @@ pub extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> *mut TCString<'s // - cstr contains a valid NUL terminator (promised by caller) // - cstr's content will not change before it is destroyed (by C convention) let cstr: &CStr = unsafe { CStr::from_ptr(cstr) }; - TCString::CString(cstr.into()).return_val() + // SAFETY: see docstring + unsafe { TCString::CString(cstr.into()).return_val() } } /// Create a new TCString containing the given string with the given length. This allows creation @@ -176,14 +161,15 @@ pub extern "C" fn tc_string_clone_with_len( let vec = slice.to_vec(); // try converting to a string, which is the only variant that can contain embedded NULs. If // the bytes are not valid utf-8, store that information for reporting later. - match String::from_utf8(vec) { + let tcstring = match String::from_utf8(vec) { Ok(string) => TCString::String(string), Err(e) => { let (e, vec) = (e.utf8_error(), e.into_bytes()); TCString::InvalidUtf8(e, vec) } - } - .return_val() + }; + // SAFETY: see docstring + unsafe { tcstring.return_val() } } /// Get the content of the string as a regular C string. The given string must not be NULL. The @@ -200,11 +186,12 @@ pub extern "C" fn tc_string_content(tcstring: *mut TCString) -> *const libc::c_c // - tcstring is not NULL (promised by caller) // - lifetime of tcstring outlives the lifetime of this function // - lifetime of tcstring outlives the lifetime of the returned pointer (promised by caller) - let tcstring = unsafe { TCString::from_arg_ref(tcstring) }; + let tcstring = unsafe { TCString::from_arg_ref_mut(tcstring) }; // if we have a String, we need to consume it and turn it into // a CString. if matches!(tcstring, TCString::String(_)) { + // TODO: put this in a method if let TCString::String(string) = std::mem::take(tcstring) { match CString::new(string) { Ok(cstring) => { @@ -237,7 +224,8 @@ pub extern "C" fn tc_string_content(tcstring: *mut TCString) -> *const libc::c_c /// Get the content of the string as a pointer and length. The given string must not be NULL. /// This function can return any string, even one including NUL bytes or invalid UTF-8. The -/// returned buffer is valid until the TCString is freed or passed to another TC API function. +/// returned buffer is valid until the TCString is freed or passed to another TaskChampio +/// function. /// /// This function does _not_ take ownership of the TCString. #[no_mangle] @@ -274,5 +262,5 @@ pub extern "C" fn tc_string_free(tcstring: *mut TCString) { // SAFETY: // - tcstring is not NULL (promised by caller) // - caller is exclusive owner of tcstring (promised by caller) - drop(unsafe { TCString::from_arg(tcstring) }); + drop(unsafe { TCString::take_from_arg(tcstring) }); } diff --git a/lib/src/task.rs b/lib/src/task.rs index 6c305792d..87dcdd773 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -1,3 +1,4 @@ +use crate::traits::*; use crate::util::err_to_tcstring; use crate::{ arrays::TCTags, replica::TCReplica, result::TCResult, status::TCStatus, string::TCString, @@ -93,7 +94,8 @@ impl TCTask { // SAFETY: // - tcreplica is not null (promised by caller) // - tcreplica outlives the pointer in this variant (promised by caller) - let tcreplica_ref: &mut TCReplica = unsafe { TCReplica::from_arg_ref(tcreplica) }; + let tcreplica_ref: &mut TCReplica = + unsafe { TCReplica::from_arg_ref_mut(tcreplica) }; let rep_ref = tcreplica_ref.borrow_mut(); Inner::Mutable(task.into_mut(rep_ref), tcreplica) } @@ -112,7 +114,8 @@ impl TCTask { // - tcreplica is not null (promised by caller of to_mut, which created this // variant) // - tcreplica is still alive (promised by caller of to_mut) - let tcreplica_ref: &mut TCReplica = unsafe { TCReplica::from_arg_ref(tcreplica) }; + let tcreplica_ref: &mut TCReplica = + unsafe { TCReplica::from_arg_ref_mut(tcreplica) }; tcreplica_ref.release_borrow(); Inner::Immutable(task.into_immut()) } @@ -246,7 +249,7 @@ pub extern "C" fn tc_task_to_immut<'a>(task: *mut TCTask) { /// Get a task's UUID. #[no_mangle] pub extern "C" fn tc_task_get_uuid(task: *mut TCTask) -> TCUuid { - wrap(task, |task| task.get_uuid().into()) + wrap(task, |task| task.get_uuid().return_val()) } /// Get a task's status. @@ -263,7 +266,8 @@ pub extern "C" fn tc_task_get_status<'a>(task: *mut TCTask) -> TCStatus { pub extern "C" fn tc_task_get_description<'a>(task: *mut TCTask) -> *mut TCString<'static> { wrap(task, |task| { let descr: TCString = task.get_description().into(); - descr.return_val() + // SAFETY: see TCString docstring + unsafe { descr.return_val() } }) } @@ -298,13 +302,12 @@ pub extern "C" fn tc_task_is_active(task: *mut TCTask) -> bool { } /// Check if a task has the given tag. If the tag is invalid, this function will simply return -/// false with no error from `tc_task_error`. The given tag must not be NULL. +/// false, with no error from `tc_task_error`. +// TODO: why no error?? #[no_mangle] pub extern "C" fn tc_task_has_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> bool { - // SAFETY: - // - tcstring is not NULL (promised by caller) - // - caller is exclusive owner of tcstring (implicitly promised by caller) - let tcstring = unsafe { TCString::from_arg(tag) }; + // SAFETY: see TCString docstring + let tcstring = unsafe { TCString::take_from_arg(tag) }; wrap(task, |task| { if let Ok(tag) = Tag::try_from(tcstring) { task.has_tag(&tag) @@ -321,7 +324,8 @@ pub extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCTags { let tcstrings: Vec>> = task .get_tags() .map(|t| { - let t_ptr = TCString::from(t.as_ref()).return_val(); + // SAFETY: see TCString docstring + let t_ptr = unsafe { TCString::from(t.as_ref()).return_val() }; // SAFETY: t_ptr was just created and is not NULL unsafe { NonNull::new_unchecked(t_ptr) } }) @@ -355,10 +359,8 @@ pub extern "C" fn tc_task_set_description<'a>( task: *mut TCTask, description: *mut TCString, ) -> TCResult { - // SAFETY: - // - tcstring is not NULL (promised by caller) - // - caller is exclusive owner of tcstring (implicitly promised by caller) - let description = unsafe { TCString::from_arg(description) }; + // SAFETY: see TCString docstring + let description = unsafe { TCString::take_from_arg(description) }; wrap_mut( task, |task| { @@ -466,10 +468,8 @@ pub extern "C" fn tc_task_delete(task: *mut TCTask) -> TCResult { /// Add a tag to a mutable task. #[no_mangle] pub extern "C" fn tc_task_add_tag(task: *mut TCTask, tag: *mut TCString) -> TCResult { - // SAFETY: - // - tcstring is not NULL (promised by caller) - // - caller is exclusive owner of tcstring (implicitly promised by caller) - let tcstring = unsafe { TCString::from_arg(tag) }; + // SAFETY: see TCString docstring + let tcstring = unsafe { TCString::take_from_arg(tag) }; wrap_mut( task, |task| { @@ -484,10 +484,8 @@ pub extern "C" fn tc_task_add_tag(task: *mut TCTask, tag: *mut TCString) -> TCRe /// Remove a tag from a mutable task. #[no_mangle] pub extern "C" fn tc_task_remove_tag(task: *mut TCTask, tag: *mut TCString) -> TCResult { - // SAFETY: - // - tcstring is not NULL (promised by caller) - // - caller is exclusive owner of tcstring (implicitly promised by caller) - let tcstring = unsafe { TCString::from_arg(tag) }; + // SAFETY: see TCString docstring + let tcstring = unsafe { TCString::take_from_arg(tag) }; wrap_mut( task, |task| { @@ -516,7 +514,7 @@ pub extern "C" fn tc_task_error<'a>(task: *mut TCTask) -> *mut TCString<'static> // - task outlives 'a (promised by caller) let task: &'a mut TCTask = unsafe { TCTask::from_arg_ref(task) }; if let Some(tcstring) = task.error.take() { - tcstring.return_val() + unsafe { tcstring.return_val() } } else { std::ptr::null_mut() } diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index 9dcd3797b..8f57c7d1a 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -1,4 +1,5 @@ use crate::string::TCString; +use crate::traits::*; use libc; use taskchampion::Uuid; @@ -9,28 +10,30 @@ use taskchampion::Uuid; #[repr(C)] pub struct TCUuid([u8; 16]); -impl From for TCUuid { - fn from(uuid: Uuid) -> TCUuid { - TCUuid(*uuid.as_bytes()) - } -} +impl PassByValue for Uuid { + type CType = TCUuid; -impl From for Uuid { - fn from(uuid: TCUuid) -> Uuid { - Uuid::from_bytes(uuid.0) + unsafe fn from_ctype(arg: TCUuid) -> Self { + // SAFETY: + // - any 16-byte value is a valid Uuid + Uuid::from_bytes(arg.0) + } + + fn as_ctype(self) -> TCUuid { + TCUuid(*self.as_bytes()) } } /// Create a new, randomly-generated UUID. #[no_mangle] pub extern "C" fn tc_uuid_new_v4() -> TCUuid { - Uuid::new_v4().into() + Uuid::new_v4().return_val() } /// Create a new UUID with the nil value. #[no_mangle] pub extern "C" fn tc_uuid_nil() -> TCUuid { - Uuid::nil().into() + Uuid::nil().return_val() } // NOTE: this must be a simple constant so that cbindgen can evaluate it @@ -40,7 +43,7 @@ pub const TC_UUID_STRING_BYTES: usize = 36; /// Write the string representation of a TCUuid into the given buffer, which must be /// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. #[no_mangle] -pub extern "C" fn tc_uuid_to_buf<'a>(uuid: TCUuid, buf: *mut libc::c_char) { +pub extern "C" fn tc_uuid_to_buf<'a>(tcuuid: TCUuid, buf: *mut libc::c_char) { debug_assert!(!buf.is_null()); // SAFETY: // - buf is valid for len bytes (by C convention) @@ -51,34 +54,34 @@ pub extern "C" fn tc_uuid_to_buf<'a>(uuid: TCUuid, buf: *mut libc::c_char) { let buf: &'a mut [u8] = unsafe { std::slice::from_raw_parts_mut(buf as *mut u8, ::uuid::adapter::Hyphenated::LENGTH) }; - let uuid: Uuid = uuid.into(); + let uuid: Uuid = Uuid::from_arg(tcuuid); uuid.to_hyphenated().encode_lower(buf); } /// Write the string representation of a TCUuid into the given buffer, which must be /// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. #[no_mangle] -pub extern "C" fn tc_uuid_to_str(uuid: TCUuid) -> *mut TCString<'static> { - let uuid: Uuid = uuid.into(); +pub extern "C" fn tc_uuid_to_str(tcuuid: TCUuid) -> *mut TCString<'static> { + let uuid: Uuid = Uuid::from_arg(tcuuid); let s = uuid.to_string(); - TCString::from(s).return_val() + // SAFETY: see TCString docstring + unsafe { TCString::from(s).return_val() } } -/// Parse the given string as a UUID. The string must not be NULL. Returns false on failure. +/// Parse the given string as a UUID. Returns false on failure. #[no_mangle] pub extern "C" fn tc_uuid_from_str<'a>(s: *mut TCString, uuid_out: *mut TCUuid) -> bool { + // TODO: TCResult instead debug_assert!(!s.is_null()); debug_assert!(!uuid_out.is_null()); - // SAFETY: - // - tcstring is not NULL (promised by caller) - // - caller is exclusive owner of tcstring (implicitly promised by caller) - let s = unsafe { TCString::from_arg(s) }; + // SAFETY: see TCString docstring + let s = unsafe { TCString::take_from_arg(s) }; if let Ok(s) = s.as_str() { if let Ok(u) = Uuid::parse_str(s) { // SAFETY: // - uuid_out is not NULL (promised by caller) // - alignment is not required - unsafe { *uuid_out = u.into() }; + unsafe { u.to_arg_out(uuid_out) }; return true; } } diff --git a/lib/taskchampion.h b/lib/taskchampion.h index c001538a6..ccb8ff1db 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -41,30 +41,59 @@ typedef enum TCStatus { * A replica represents an instance of a user's task data, providing an easy interface * for querying and modifying that data. * - * TCReplicas are not threadsafe. + * # Error Handling * * When a `tc_replica_..` function that returns a TCResult returns TC_RESULT_ERROR, then * `tc_replica_error` will return the error message. + * + * # Safety + * + * The `*TCReplica` returned from `tc_replica_new…` functions is owned by the caller and + * must later be freed to avoid a memory leak. + * + * Any function taking a `*TCReplica` requires: + * - the pointer must not be NUL; + * - the pointer must be one previously returned from a tc_… function; + * - the memory referenced by the pointer must never be modified by C code; and + * - except for `tc_replica_free`, ownership of a `*TCReplica` remains with the caller. + * + * Once passed to `tc_replica_free`, a `*TCReplica` becmes invalid and must not be used again. + * + * TCReplicas are not threadsafe. */ typedef struct TCReplica TCReplica; /** - * TCString supports passing strings into and out of the TaskChampion API. A string can contain - * embedded NUL characters. Strings containing such embedded NULs cannot be represented as a "C - * string" and must be accessed using `tc_string_content_and_len` and `tc_string_clone_with_len`. - * In general, these two functions should be used for handling arbitrary data, while more - * convenient forms may be used where embedded NUL characters are impossible, such as in static - * strings. + * TCString supports passing strings into and out of the TaskChampion API. * - * Rust expects all strings to be UTF-8, and API functions will fail if given a TCString - * containing invalid UTF-8. + * # Rust Strings and C Strings * - * Unless specified otherwise, functions in this API take ownership of a TCString when it is given - * as a function argument, and free the string before returning. Callers must not use or free - * strings after passing them to such API functions. + * A Rust string can contain embedded NUL characters, while C considers such a character to mark + * the end of a string. Strings containing embedded NULs cannot be represented as a "C string" + * and must be accessed using `tc_string_content_and_len` and `tc_string_clone_with_len`. In + * general, these two functions should be used for handling arbitrary data, while more convenient + * forms may be used where embedded NUL characters are impossible, such as in static strings. * - * When a TCString appears as a return value or output argument, it is the responsibility of the - * caller to free the string. + * # UTF-8 + * + * TaskChampion expects all strings to be valid UTF-8. `tc_string_…` functions will fail if given + * a `*TCString` containing invalid UTF-8. + * + * # Safety + * + * When a `*TCString` appears as a return value or output argument, ownership is passed to the + * caller. The caller must pass that ownerhsip back to another function or free the string. + * + * Any function taking a `*TCReplica` requires: + * - the pointer must not be NUL; + * - the pointer must be one previously returned from a tc_… function; and + * - the memory referenced by the pointer must never be modified by C code. + * + * Unless specified otherwise, TaskChampion functions take ownership of a `*TCString` when it is + * given as a function argument, and the pointer is invalid when the function returns. Callers + * must not use or free TCStrings after passing them to such API functions. + * + * TCStrings are not threadsafe. */ typedef struct TCString TCString; @@ -138,9 +167,8 @@ void tc_tags_free(struct TCTags *tctags); struct TCReplica *tc_replica_new_in_memory(void); /** - * Create a new TCReplica with an on-disk database having the given filename. The filename must - * not be NULL. On error, a string is written to the `error_out` parameter (if it is not NULL) and - * NULL is returned. + * Create a new TCReplica with an on-disk database having the given filename. On error, a string + * is written to the `error_out` parameter (if it is not NULL) and NULL is returned. */ struct TCReplica *tc_replica_new_on_disk(struct TCString *path, struct TCString **error_out); @@ -150,13 +178,11 @@ struct TCReplica *tc_replica_new_on_disk(struct TCString *path, struct TCString * Returns NULL when the task does not exist, and on error. Consult tc_replica_error * to distinguish the two conditions. */ -struct TCTask *tc_replica_get_task(struct TCReplica *rep, struct TCUuid uuid); +struct TCTask *tc_replica_get_task(struct TCReplica *rep, struct TCUuid tcuuid); /** * Create a new task. The task must not already exist. * - * The description must not be NULL. - * * Returns the task, or NULL on error. */ struct TCTask *tc_replica_new_task(struct TCReplica *rep, @@ -168,7 +194,7 @@ struct TCTask *tc_replica_new_task(struct TCReplica *rep, * * Returns the task, or NULL on error. */ -struct TCTask *tc_replica_import_task_with_uuid(struct TCReplica *rep, struct TCUuid uuid); +struct TCTask *tc_replica_import_task_with_uuid(struct TCReplica *rep, struct TCUuid tcuuid); /** * Undo local operations until the most recent UndoPoint. @@ -239,7 +265,8 @@ const char *tc_string_content(struct TCString *tcstring); /** * Get the content of the string as a pointer and length. The given string must not be NULL. * This function can return any string, even one including NUL bytes or invalid UTF-8. The - * returned buffer is valid until the TCString is freed or passed to another TC API function. + * returned buffer is valid until the TCString is freed or passed to another TaskChampio + * function. * * This function does _not_ take ownership of the TCString. */ @@ -323,7 +350,7 @@ bool tc_task_is_active(struct TCTask *task); /** * Check if a task has the given tag. If the tag is invalid, this function will simply return - * false with no error from `tc_task_error`. The given tag must not be NULL. + * false, with no error from `tc_task_error`. */ bool tc_task_has_tag(struct TCTask *task, struct TCString *tag); @@ -417,16 +444,16 @@ struct TCUuid tc_uuid_nil(void); * Write the string representation of a TCUuid into the given buffer, which must be * at least TC_UUID_STRING_BYTES long. No NUL terminator is added. */ -void tc_uuid_to_buf(struct TCUuid uuid, char *buf); +void tc_uuid_to_buf(struct TCUuid tcuuid, char *buf); /** * Write the string representation of a TCUuid into the given buffer, which must be * at least TC_UUID_STRING_BYTES long. No NUL terminator is added. */ -struct TCString *tc_uuid_to_str(struct TCUuid uuid); +struct TCString *tc_uuid_to_str(struct TCUuid tcuuid); /** - * Parse the given string as a UUID. The string must not be NULL. Returns false on failure. + * Parse the given string as a UUID. Returns false on failure. */ bool tc_uuid_from_str(struct TCString *s, struct TCUuid *uuid_out); From f4c6e04d444ecfee0d754ac220a25a74d33d9a97 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 6 Feb 2022 05:04:44 +0000 Subject: [PATCH 43/95] TCTags as PassByValue --- integration-tests/src/bindings_tests/task.c | 8 +- lib/src/arrays.rs | 122 ++++++++--------- lib/src/replica.rs | 6 +- lib/src/task.rs | 11 +- lib/src/traits.rs | 143 ++++++++++++++++++++ lib/src/uuid.rs | 8 +- lib/taskchampion.h | 20 +-- 7 files changed, 227 insertions(+), 91 deletions(-) create mode 100644 lib/src/traits.rs diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 2ef89204c..c011509ca 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -315,11 +315,11 @@ static void test_task_get_tags(void) { TCTags tags = tc_task_get_tags(task); int found_pending = false, found_next = false; - for (size_t i = 0; i < tags.num_tags; i++) { - if (strcmp("PENDING", tc_string_content(tags.tags[i])) == 0) { + for (size_t i = 0; i < tags.len; i++) { + if (strcmp("PENDING", tc_string_content(tags.items[i])) == 0) { found_pending = true; } - if (strcmp("next", tc_string_content(tags.tags[i])) == 0) { + if (strcmp("next", tc_string_content(tags.items[i])) == 0) { found_next = true; } } @@ -327,7 +327,7 @@ static void test_task_get_tags(void) { TEST_ASSERT_TRUE(found_next); tc_tags_free(&tags); - TEST_ASSERT_NULL(tags.tags); + TEST_ASSERT_NULL(tags.items); tc_task_free(task); tc_replica_free(rep); diff --git a/lib/src/arrays.rs b/lib/src/arrays.rs index 3c266cb3c..a9d5dde49 100644 --- a/lib/src/arrays.rs +++ b/lib/src/arrays.rs @@ -2,8 +2,6 @@ use crate::string::TCString; use crate::traits::*; use std::ptr::NonNull; -// TODO: generalize to TCStrings? - /// TCTags represents a list of tags associated with a task. /// /// The content of this struct must be treated as read-only. @@ -12,86 +10,74 @@ use std::ptr::NonNull; /// will remain valid even if the task is freed. #[repr(C)] pub struct TCTags { - // TODO: can we use NonNull here? - /// strings representing each tag. these remain owned by the - /// TCTags instance and will be freed by tc_tags_free. - tags: *const NonNull>, - /// number of tags in tags - num_tags: libc::size_t, - /// total size of tags (internal use only) + // WARNING: this struct must match CPointerArray exactly, in size and order + // of fields. Names can differ, as can the pointer type. + /// number of tags in items + len: libc::size_t, + + /// total size of items (internal use only) _capacity: libc::size_t, + + /// strings representing each tag. these remain owned by the TCTags instance and will be freed + /// by tc_tags_free. + items: *const NonNull>, +} + +impl PassByValue for Vec>> { + type CType = TCTags; + + unsafe fn from_ctype(arg: TCTags) -> Self { + // SAFETY: + // - C treats TCTags as read-only, so items, len, and _capacity all came + // from a Vec originally. + unsafe { Vec::from_raw_parts(arg.items as *mut _, arg.len, arg._capacity) } + } + + fn as_ctype(self) -> TCTags { + // emulate Vec::into_raw_parts(): + // - disable dropping the Vec with ManuallyDrop + // - extract ptr, len, and capacity using those methods + let mut vec = std::mem::ManuallyDrop::new(self); + TCTags { + len: vec.len(), + _capacity: vec.capacity(), + items: vec.as_mut_ptr(), + } + } } impl Default for TCTags { fn default() -> Self { Self { - tags: std::ptr::null_mut(), - num_tags: 0, + len: 0, _capacity: 0, + items: std::ptr::null(), } } } -impl TCTags { - /// Create a Vec of TCStrings into a TCTags instance. - pub(crate) fn new(tags: Vec>>) -> Self { - // emulate Vec::into_raw_parts(): - // - disable dropping the Vec with ManuallyDrop - // - extract ptr, len, and capacity using those methods - let mut tags = std::mem::ManuallyDrop::new(tags); - Self { - tags: tags.as_mut_ptr(), - num_tags: tags.len(), - _capacity: tags.capacity(), - } - } - - /// Convert a TCTags to a Vec<_>. - /// - /// # Safety - /// - /// Tags must be _exactly_ as created by [`new`] - unsafe fn into_vec(self) -> Vec>> { - // SAFETY: - // - // * tags.tags needs to have been previously allocated via Vec<*mut TCString> - // * TCString needs to have the same size and alignment as what ptr was allocated with. - // * length needs to be less than or equal to capacity. - // * capacity needs to be the capacity that the pointer was allocated with. - // * vec elements are not NULL - // - // All of this is true for a value returned from `new`, which the caller promised - // not to change. - unsafe { Vec::from_raw_parts(self.tags as *mut _, self.num_tags, self._capacity) } - } -} - -/// Free a TCTags instance. The given pointer must not be NULL. The instance must not be used -/// after this call. +/// Free a TCTags instance. The instance, and all TCStrings it contains, must not be used after +/// this call. #[no_mangle] pub extern "C" fn tc_tags_free<'a>(tctags: *mut TCTags) { debug_assert!(!tctags.is_null()); // SAFETY: - // - tctags is not NULL - // - tctags is valid (caller promises it has not been changed) - // - caller will not use the TCTags after this (promised by caller) - let tctags: &'a mut TCTags = unsafe { &mut *tctags }; + // - *tctags is a valid TCTags (caller promises to treat it as read-only) + let tags = unsafe { Vec::take_from_arg(tctags, TCTags::default()) }; - debug_assert!(!tctags.tags.is_null()); - - // replace the caller's TCTags with one containing a NULL pointer - let tctags: TCTags = std::mem::take(tctags); - - // convert to a regular Vec - // SAFETY: - // - tctags is exactly as returned from TCTags::new (promised by caller) - let mut vec: Vec<_> = unsafe { tctags.into_vec() }; - - // drop each contained string - for tcstring in vec.drain(..) { - // SAFETY: see TCString docstring - drop(unsafe { TCString::take_from_arg(tcstring.as_ptr()) }); - } - - drop(vec); + // tags is a Vec>, so we convert it to a Vec that + // will properly drop those strings when dropped. + let tags: Vec = tags + .iter() + .map(|p| { + // SAFETY: + // *p is a pointer delivered to us from a Vec>, so + // - *p is not NULL + // - *p was generated by Rust + // - *p was not modified (promised by caller) + // - the caller will not use this pointer again (promised by caller) + unsafe { TCString::take_from_arg(p.as_ptr()) } + }) + .collect(); + drop(tags); } diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 7512d1f91..131a8eb91 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -142,7 +142,8 @@ pub extern "C" fn tc_replica_get_task(rep: *mut TCReplica, tcuuid: TCUuid) -> *m wrap( rep, |rep| { - let uuid = Uuid::from_arg(tcuuid); + // SAFETY: see TCUuid docstring + let uuid = unsafe { Uuid::from_arg(tcuuid) }; if let Some(task) = rep.get_task(uuid)? { Ok(TCTask::from(task).return_val()) } else { @@ -185,7 +186,8 @@ pub extern "C" fn tc_replica_import_task_with_uuid( wrap( rep, |rep| { - let uuid = Uuid::from_arg(tcuuid); + // SAFETY: see TCUuid docstring + let uuid = unsafe { Uuid::from_arg(tcuuid) }; let task = rep.import_task_with_uuid(uuid)?; Ok(TCTask::from(task).return_val()) }, diff --git a/lib/src/task.rs b/lib/src/task.rs index 87dcdd773..75bf12888 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -324,13 +324,14 @@ pub extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCTags { let tcstrings: Vec>> = task .get_tags() .map(|t| { - // SAFETY: see TCString docstring - let t_ptr = unsafe { TCString::from(t.as_ref()).return_val() }; - // SAFETY: t_ptr was just created and is not NULL - unsafe { NonNull::new_unchecked(t_ptr) } + NonNull::new( + // SAFETY: see TCString docstring + unsafe { TCString::from(t.as_ref()).return_val() }, + ) + .expect("TCString::return_val() returned NULL") }) .collect(); - TCTags::new(tcstrings) + tcstrings.return_val() }) } diff --git a/lib/src/traits.rs b/lib/src/traits.rs new file mode 100644 index 000000000..8011a46e4 --- /dev/null +++ b/lib/src/traits.rs @@ -0,0 +1,143 @@ +/// Support for values passed to Rust by value. These are represented as full structs in C. Such +/// values are implicitly copyable, via C's struct assignment. +/// +/// The Rust and C types may differ, with from_ctype and as_ctype converting between them. +pub(crate) trait PassByValue: Sized { + type CType; + + /// Convert a C value to a Rust value. + /// + /// # Safety + /// + /// `arg` must be a valid CType. + unsafe fn from_ctype(arg: Self::CType) -> Self; + + /// Convert a Rust value to a C value. + fn as_ctype(self) -> Self::CType; + + /// Take a value from C as an argument. + /// + /// # Safety + /// + /// `arg` must be a valid CType. This is typically ensured either by requiring that C + /// code not modify it, or by defining the valid values in C comments. + unsafe fn from_arg(arg: Self::CType) -> Self { + // SAFETY: + // - arg is a valid CType (promised by caller) + unsafe { Self::from_ctype(arg) } + } + + /// Take a value from C as a pointer argument, replacing it with the given value. This is used + /// to invalidate the C value as an additional assurance against subsequent use of the value. + /// + /// # Safety + /// + /// `*arg` must be a valid CType, as with [`from_arg`]. + unsafe fn take_from_arg(arg: *mut Self::CType, mut replacement: Self::CType) -> Self { + // SAFETY: + // - arg is valid (promised by caller) + // - replacement is valid (guaranteed by Rust) + unsafe { std::ptr::swap(arg, &mut replacement) }; + // SAFETY: + // - replacement (formerly *arg) is a valid CType (promised by caller) + unsafe { PassByValue::from_arg(replacement) } + } + + /// Return a value to C + fn return_val(self) -> Self::CType { + self.as_ctype() + } + + /// Return a value to C, via an "output parameter" + /// + /// # Safety + /// + /// `arg_out` must not be NULL and must be properly aligned and pointing to valid memory + /// of the size of CType. + unsafe fn to_arg_out(self, arg_out: *mut Self::CType) { + debug_assert!(!arg_out.is_null()); + // SAFETY: + // - arg_out is not NULL (promised by caller, asserted) + // - arg_out is properly aligned and points to valid memory (promised by caller) + unsafe { *arg_out = self.as_ctype() }; + } +} + +/// Support for values passed to Rust by pointer. These are represented as opaque structs in C, +/// and always handled as pointers. +/// +/// # Safety +/// +/// The functions provided by this trait are used directly in C interface functions, and make the +/// following expectations of the C code: +/// +/// - When passing a value to Rust (via the `…arg…` functions), +/// - the pointer must not be NULL; +/// - the pointer must be one previously returned from Rust; and +/// - the memory addressed by the pointer must never be modified by C code. +/// - For `from_arg_ref`, the value must not be modified during the call to the Rust function +/// - For `from_arg_ref_mut`, the value must not be accessed (read or write) during the call +/// (these last two points are trivially ensured by all TC… types being non-threadsafe) +/// - For `take_from_arg`, the pointer becomes invalid and must not be used in _any way_ after it +/// is passed to the Rust function. +/// - For `return_val` and `to_arg_out`, it is the C caller's responsibility to later free the value. +/// - For `to_arg_out`, `arg_out` must not be NULL and must be properly aligned and pointing to +/// valid memory. +/// +/// These requirements should be expressed in the C documentation for the type implementing this +/// trait. +pub(crate) trait PassByPointer: Sized { + /// Take a value from C as an argument. + /// + /// # Safety + /// + /// See trait documentation. + unsafe fn take_from_arg(arg: *mut Self) -> Self { + debug_assert!(!arg.is_null()); + // SAFETY: see trait documentation + unsafe { *(Box::from_raw(arg)) } + } + + /// Borrow a value from C as an argument. + /// + /// # Safety + /// + /// See trait documentation. + unsafe fn from_arg_ref<'a>(arg: *const Self) -> &'a Self { + debug_assert!(!arg.is_null()); + // SAFETY: see trait documentation + unsafe { &*arg } + } + + /// Mutably borrow a value from C as an argument. + /// + /// # Safety + /// + /// See trait documentation. + unsafe fn from_arg_ref_mut<'a>(arg: *mut Self) -> &'a mut Self { + debug_assert!(!arg.is_null()); + // SAFETY: see trait documentation + unsafe { &mut *arg } + } + + /// Return a value to C, transferring ownership + /// + /// # Safety + /// + /// See trait documentation. + unsafe fn return_val(self) -> *mut Self { + Box::into_raw(Box::new(self)) + } + + /// Return a value to C, transferring ownership, via an "output parameter". + /// + /// # Safety + /// + /// See trait documentation. + unsafe fn to_arg_out(self, arg_out: *mut *mut Self) { + // SAFETY: see trait documentation + unsafe { + *arg_out = self.return_val(); + } + } +} diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index 8f57c7d1a..dfb084887 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -54,7 +54,9 @@ pub extern "C" fn tc_uuid_to_buf<'a>(tcuuid: TCUuid, buf: *mut libc::c_char) { let buf: &'a mut [u8] = unsafe { std::slice::from_raw_parts_mut(buf as *mut u8, ::uuid::adapter::Hyphenated::LENGTH) }; - let uuid: Uuid = Uuid::from_arg(tcuuid); + // SAFETY: + // - tcuuid is a valid TCUuid (all byte patterns are valid) + let uuid: Uuid = unsafe { Uuid::from_arg(tcuuid) }; uuid.to_hyphenated().encode_lower(buf); } @@ -62,7 +64,9 @@ pub extern "C" fn tc_uuid_to_buf<'a>(tcuuid: TCUuid, buf: *mut libc::c_char) { /// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. #[no_mangle] pub extern "C" fn tc_uuid_to_str(tcuuid: TCUuid) -> *mut TCString<'static> { - let uuid: Uuid = Uuid::from_arg(tcuuid); + // SAFETY: + // - tcuuid is a valid TCUuid (all byte patterns are valid) + let uuid: Uuid = unsafe { Uuid::from_arg(tcuuid) }; let s = uuid.to_string(); // SAFETY: see TCString docstring unsafe { TCString::from(s).return_val() } diff --git a/lib/taskchampion.h b/lib/taskchampion.h index ccb8ff1db..b60658d58 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -127,18 +127,18 @@ typedef struct TCTask TCTask; */ typedef struct TCTags { /** - * strings representing each tag. these remain owned by the - * TCTags instance and will be freed by tc_tags_free. + * number of tags in items */ - struct TCString *const *tags; + size_t len; /** - * number of tags in tags - */ - size_t num_tags; - /** - * total size of tags (internal use only) + * total size of items (internal use only) */ size_t _capacity; + /** + * strings representing each tag. these remain owned by the TCTags instance and will be freed + * by tc_tags_free. + */ + struct TCString *const *items; } TCTags; /** @@ -155,8 +155,8 @@ extern "C" { #endif // __cplusplus /** - * Free a TCTags instance. The given pointer must not be NULL. The instance must not be used - * after this call. + * Free a TCTags instance. The instance, and all TCStrings it contains, must not be used after + * this call. */ void tc_tags_free(struct TCTags *tctags); From dadc9473d3b1d419a5aa531b775b46b834af5fd3 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 6 Feb 2022 16:21:42 +0000 Subject: [PATCH 44/95] unit tests for TCString --- Cargo.lock | 1 + lib/Cargo.toml | 3 + lib/src/string.rs | 181 ++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 155 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ad2920754..ca9160c27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3038,6 +3038,7 @@ dependencies = [ "cbindgen", "chrono", "libc", + "pretty_assertions", "taskchampion", "uuid", ] diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 66b8c6051..86a2ec735 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -15,5 +15,8 @@ taskchampion = { path = "../taskchampion" } uuid = { version = "^0.8.2", features = ["v4"] } anyhow = "1.0" +[dev-dependencies] +pretty_assertions = "1" + [build-dependencies] cbindgen = "0.20.0" diff --git a/lib/src/string.rs b/lib/src/string.rs index d3f952189..08eed500c 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -34,6 +34,7 @@ use std::str::Utf8Error; /// must not use or free TCStrings after passing them to such API functions. /// /// TCStrings are not threadsafe. +#[derive(PartialEq, Debug)] pub enum TCString<'a> { CString(CString), CStr(&'a CStr), @@ -78,6 +79,32 @@ impl<'a> TCString<'a> { } } + /// Convert the TCString, in place, into one of the C variants. If this is not + /// possible, such as if the string contains an embedded NUL, then the string + /// remains unchanged. + fn to_c_string(&mut self) { + if matches!(self, TCString::String(_)) { + // we must take ownership of the String in order to try converting it, + // leaving the underlying TCString as its default (None) + if let TCString::String(string) = std::mem::take(self) { + match CString::new(string) { + Ok(cstring) => *self = TCString::CString(cstring), + Err(nul_err) => { + // recover the underlying String from the NulError and restore + // the TCString + let original_bytes = nul_err.into_vec(); + // SAFETY: original_bytes came from a String moments ago, so still valid utf8 + let string = unsafe { String::from_utf8_unchecked(original_bytes) }; + *self = TCString::String(string); + } + } + } else { + // the `matches!` above verified self was a TCString::String + unreachable!() + } + } + } + pub(crate) fn to_path_buf(&self) -> PathBuf { // TODO: this is UNIX-specific. let path: &OsStr = OsStr::from_bytes(self.as_bytes()); @@ -158,7 +185,10 @@ pub extern "C" fn tc_string_clone_with_len( // does not outlive this function call) // - the length of the buffer is less than isize::MAX (promised by caller) let slice = unsafe { std::slice::from_raw_parts(buf as *const u8, len) }; + + // allocate and copy into Rust-controlled memory let vec = slice.to_vec(); + // try converting to a string, which is the only variant that can contain embedded NULs. If // the bytes are not valid utf-8, store that information for reporting later. let tcstring = match String::from_utf8(vec) { @@ -168,6 +198,7 @@ pub extern "C" fn tc_string_clone_with_len( TCString::InvalidUtf8(e, vec) } }; + // SAFETY: see docstring unsafe { tcstring.return_val() } } @@ -190,32 +221,11 @@ pub extern "C" fn tc_string_content(tcstring: *mut TCString) -> *const libc::c_c // if we have a String, we need to consume it and turn it into // a CString. - if matches!(tcstring, TCString::String(_)) { - // TODO: put this in a method - if let TCString::String(string) = std::mem::take(tcstring) { - match CString::new(string) { - Ok(cstring) => { - *tcstring = TCString::CString(cstring); - } - Err(nul_err) => { - // recover the underlying String from the NulError - let original_bytes = nul_err.into_vec(); - // SAFETY: original_bytes came from a String moments ago, so still valid utf8 - let string = unsafe { String::from_utf8_unchecked(original_bytes) }; - *tcstring = TCString::String(string); - - // and return NULL as advertized - return std::ptr::null(); - } - } - } else { - unreachable!() - } - } + tcstring.to_c_string(); match tcstring { TCString::CString(cstring) => cstring.as_ptr(), - TCString::String(_) => unreachable!(), // just converted to CString + TCString::String(_) => std::ptr::null(), // to_c_string failed TCString::CStr(cstr) => cstr.as_ptr(), TCString::InvalidUtf8(_, _) => std::ptr::null(), TCString::None => unreachable!(), @@ -240,13 +250,8 @@ pub extern "C" fn tc_string_content_with_len( let tcstring = unsafe { TCString::from_arg_ref(tcstring) }; debug_assert!(!len_out.is_null()); - let bytes = match tcstring { - TCString::CString(cstring) => cstring.as_bytes(), - TCString::String(string) => string.as_bytes(), - TCString::CStr(cstr) => cstr.to_bytes(), - TCString::InvalidUtf8(_, ref v) => v.as_ref(), - TCString::None => unreachable!(), - }; + let bytes = tcstring.as_bytes(); + // SAFETY: // - len_out is not NULL (checked by assertion, promised by caller) // - len_out points to valid memory (promised by caller) @@ -264,3 +269,119 @@ pub extern "C" fn tc_string_free(tcstring: *mut TCString) { // - caller is exclusive owner of tcstring (promised by caller) drop(unsafe { TCString::take_from_arg(tcstring) }); } + +#[cfg(test)] +mod test { + use super::*; + use pretty_assertions::assert_eq; + + const INVALID_UTF8: &[u8] = b"abc\xf0\x28\x8c\x28"; + + fn make_cstring() -> TCString<'static> { + TCString::CString(CString::new("a string").unwrap()) + } + + fn make_cstr() -> TCString<'static> { + let cstr = CStr::from_bytes_with_nul(b"a string\0").unwrap(); + TCString::CStr(&cstr) + } + + fn make_string() -> TCString<'static> { + TCString::String("a string".into()) + } + + fn make_string_with_nul() -> TCString<'static> { + TCString::String("a \0 nul!".into()) + } + + fn make_invalid() -> TCString<'static> { + let e = String::from_utf8(INVALID_UTF8.to_vec()).unwrap_err(); + TCString::InvalidUtf8(e.utf8_error(), e.into_bytes()) + } + + #[test] + fn cstring_as_str() { + assert_eq!(make_cstring().as_str().unwrap(), "a string"); + } + + #[test] + fn cstr_as_str() { + assert_eq!(make_cstr().as_str().unwrap(), "a string"); + } + + #[test] + fn string_as_str() { + assert_eq!(make_string().as_str().unwrap(), "a string"); + } + + #[test] + fn string_with_nul_as_str() { + assert_eq!(make_string_with_nul().as_str().unwrap(), "a \0 nul!"); + } + + #[test] + fn invalid_as_str() { + let as_str_err = make_invalid().as_str().unwrap_err(); + assert_eq!(as_str_err.valid_up_to(), 3); // "abc" is valid + } + + #[test] + fn cstring_as_bytes() { + assert_eq!(make_cstring().as_bytes(), b"a string"); + } + + #[test] + fn cstr_as_bytes() { + assert_eq!(make_cstr().as_bytes(), b"a string"); + } + + #[test] + fn string_as_bytes() { + assert_eq!(make_string().as_bytes(), b"a string"); + } + + #[test] + fn string_with_nul_as_bytes() { + assert_eq!(make_string_with_nul().as_bytes(), b"a \0 nul!"); + } + + #[test] + fn invalid_as_bytes() { + assert_eq!(make_invalid().as_bytes(), INVALID_UTF8); + } + + #[test] + fn cstring_to_c_string() { + let mut tcstring = make_cstring(); + tcstring.to_c_string(); + assert_eq!(tcstring, make_cstring()); // unchanged + } + + #[test] + fn cstr_to_c_string() { + let mut tcstring = make_cstr(); + tcstring.to_c_string(); + assert_eq!(tcstring, make_cstr()); // unchanged + } + + #[test] + fn string_to_c_string() { + let mut tcstring = make_string(); + tcstring.to_c_string(); + assert_eq!(tcstring, make_cstring()); // converted to CString, same content + } + + #[test] + fn string_with_nul_to_c_string() { + let mut tcstring = make_string_with_nul(); + tcstring.to_c_string(); + assert_eq!(tcstring, make_string_with_nul()); // unchanged + } + + #[test] + fn invalid_to_c_string() { + let mut tcstring = make_invalid(); + tcstring.to_c_string(); + assert_eq!(tcstring, make_invalid()); // unchanged + } +} From b0f785071111322c1719ac5d2391040216e5b506 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 6 Feb 2022 16:26:09 +0000 Subject: [PATCH 45/95] trivially implement PassByValue for usize --- lib/src/atomic.rs | 15 +++++++++++++++ lib/src/lib.rs | 1 + lib/src/string.rs | 5 ++--- 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 lib/src/atomic.rs diff --git a/lib/src/atomic.rs b/lib/src/atomic.rs new file mode 100644 index 000000000..341f7b339 --- /dev/null +++ b/lib/src/atomic.rs @@ -0,0 +1,15 @@ +//! Trait implementations for a few atomic types + +use crate::traits::*; + +impl PassByValue for usize { + type CType = usize; + + unsafe fn from_ctype(arg: usize) -> usize { + arg + } + + fn as_ctype(self) -> usize { + self + } +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 18cba4ac9..cd3839f8c 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -4,6 +4,7 @@ mod traits; mod util; pub mod arrays; +pub mod atomic; pub mod replica; pub mod result; pub mod status; diff --git a/lib/src/string.rs b/lib/src/string.rs index 08eed500c..3e95e8f6e 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -248,15 +248,14 @@ pub extern "C" fn tc_string_content_with_len( // - lifetime of tcstring outlives the lifetime of this function // - lifetime of tcstring outlives the lifetime of the returned pointer (promised by caller) let tcstring = unsafe { TCString::from_arg_ref(tcstring) }; - debug_assert!(!len_out.is_null()); let bytes = tcstring.as_bytes(); // SAFETY: - // - len_out is not NULL (checked by assertion, promised by caller) + // - len_out is not NULL (promised by caller) // - len_out points to valid memory (promised by caller) // - len_out is properly aligned (C convention) - unsafe { *len_out = bytes.len() }; + unsafe { bytes.len().to_arg_out(len_out) }; bytes.as_ptr() as *const libc::c_char } From 3d248b55fdbd0349f3861294fd339c606af1d676 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 6 Feb 2022 16:38:31 +0000 Subject: [PATCH 46/95] factor out some utilities for pointer arrays --- lib/src/arrays.rs | 28 ++++++---------------------- lib/src/util.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/lib/src/arrays.rs b/lib/src/arrays.rs index a9d5dde49..301d273d3 100644 --- a/lib/src/arrays.rs +++ b/lib/src/arrays.rs @@ -1,5 +1,6 @@ use crate::string::TCString; use crate::traits::*; +use crate::util::{drop_pointer_array, vec_into_raw_parts}; use std::ptr::NonNull; /// TCTags represents a list of tags associated with a task. @@ -10,8 +11,6 @@ use std::ptr::NonNull; /// will remain valid even if the task is freed. #[repr(C)] pub struct TCTags { - // WARNING: this struct must match CPointerArray exactly, in size and order - // of fields. Names can differ, as can the pointer type. /// number of tags in items len: libc::size_t, @@ -37,11 +36,11 @@ impl PassByValue for Vec>> { // emulate Vec::into_raw_parts(): // - disable dropping the Vec with ManuallyDrop // - extract ptr, len, and capacity using those methods - let mut vec = std::mem::ManuallyDrop::new(self); + let (items, len, _capacity) = vec_into_raw_parts(self); TCTags { - len: vec.len(), - _capacity: vec.capacity(), - items: vec.as_mut_ptr(), + len, + _capacity, + items, } } } @@ -64,20 +63,5 @@ pub extern "C" fn tc_tags_free<'a>(tctags: *mut TCTags) { // SAFETY: // - *tctags is a valid TCTags (caller promises to treat it as read-only) let tags = unsafe { Vec::take_from_arg(tctags, TCTags::default()) }; - - // tags is a Vec>, so we convert it to a Vec that - // will properly drop those strings when dropped. - let tags: Vec = tags - .iter() - .map(|p| { - // SAFETY: - // *p is a pointer delivered to us from a Vec>, so - // - *p is not NULL - // - *p was generated by Rust - // - *p was not modified (promised by caller) - // - the caller will not use this pointer again (promised by caller) - unsafe { TCString::take_from_arg(p.as_ptr()) } - }) - .collect(); - drop(tags); + drop_pointer_array(tags); } diff --git a/lib/src/util.rs b/lib/src/util.rs index 0709d2640..00676424a 100644 --- a/lib/src/util.rs +++ b/lib/src/util.rs @@ -1,5 +1,34 @@ use crate::string::TCString; +use crate::traits::*; +use std::ptr::NonNull; pub(crate) fn err_to_tcstring(e: impl std::string::ToString) -> TCString<'static> { TCString::from(e.to_string()) } + +/// A version of Vec::into_raw_parts, which is still unstable. Returns ptr, len, cap. +pub(crate) fn vec_into_raw_parts(vec: Vec) -> (*mut T, usize, usize) { + // emulate Vec::into_raw_parts(): + // - disable dropping the Vec with ManuallyDrop + // - extract ptr, len, and capacity using those methods + let mut vec = std::mem::ManuallyDrop::new(vec); + return (vec.as_mut_ptr(), vec.len(), vec.capacity()); +} + +/// Drop an array of PassByPointer values +pub(crate) fn drop_pointer_array(mut array: Vec>) +where + T: 'static + PassByPointer, +{ + // first, drop each of the elements in turn + for p in array.drain(..) { + // SAFETY: + // - p is not NULL (NonNull) + // - p was generated by Rust (true for all arrays) + // - p was not modified (all arrays are immutable from C) + // - caller will not use this pointer again (promised by caller; p has been drain'd from + // the vector) + drop(unsafe { PassByPointer::take_from_arg(p.as_ptr()) }); + } + drop(array); +} From 831eb0bb152720e99fdd16ce7e3fccda36a386a4 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 6 Feb 2022 16:40:17 +0000 Subject: [PATCH 47/95] TCTags -> TCStrings to be more general --- integration-tests/src/bindings_tests/task.c | 4 +- lib/src/lib.rs | 2 +- lib/src/{arrays.rs => strings.rs} | 30 +++++++------- lib/src/task.rs | 4 +- lib/taskchampion.h | 44 ++++++++++----------- 5 files changed, 42 insertions(+), 42 deletions(-) rename lib/src/{arrays.rs => strings.rs} (61%) diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index c011509ca..2ab5e3602 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -312,7 +312,7 @@ static void test_task_get_tags(void) { TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_tag(task, tc_string_borrow("next"))); - TCTags tags = tc_task_get_tags(task); + TCStrings tags = tc_task_get_tags(task); int found_pending = false, found_next = false; for (size_t i = 0; i < tags.len; i++) { @@ -326,7 +326,7 @@ static void test_task_get_tags(void) { TEST_ASSERT_TRUE(found_pending); TEST_ASSERT_TRUE(found_next); - tc_tags_free(&tags); + tc_strings_free(&tags); TEST_ASSERT_NULL(tags.items); tc_task_free(task); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index cd3839f8c..5b9160970 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -3,11 +3,11 @@ mod traits; mod util; -pub mod arrays; pub mod atomic; pub mod replica; pub mod result; pub mod status; pub mod string; +pub mod strings; pub mod task; pub mod uuid; diff --git a/lib/src/arrays.rs b/lib/src/strings.rs similarity index 61% rename from lib/src/arrays.rs rename to lib/src/strings.rs index 301d273d3..341680de3 100644 --- a/lib/src/arrays.rs +++ b/lib/src/strings.rs @@ -3,41 +3,41 @@ use crate::traits::*; use crate::util::{drop_pointer_array, vec_into_raw_parts}; use std::ptr::NonNull; -/// TCTags represents a list of tags associated with a task. +/// TCStrings represents a list of tags associated with a task. /// /// The content of this struct must be treated as read-only. /// -/// The lifetime of a TCTags instance is independent of the task, and it +/// The lifetime of a TCStrings instance is independent of the task, and it /// will remain valid even if the task is freed. #[repr(C)] -pub struct TCTags { +pub struct TCStrings { /// number of tags in items len: libc::size_t, /// total size of items (internal use only) _capacity: libc::size_t, - /// strings representing each tag. these remain owned by the TCTags instance and will be freed - /// by tc_tags_free. + /// strings representing each tag. these remain owned by the TCStrings instance and will be freed + /// by tc_strings_free. items: *const NonNull>, } impl PassByValue for Vec>> { - type CType = TCTags; + type CType = TCStrings; - unsafe fn from_ctype(arg: TCTags) -> Self { + unsafe fn from_ctype(arg: TCStrings) -> Self { // SAFETY: - // - C treats TCTags as read-only, so items, len, and _capacity all came + // - C treats TCStrings as read-only, so items, len, and _capacity all came // from a Vec originally. unsafe { Vec::from_raw_parts(arg.items as *mut _, arg.len, arg._capacity) } } - fn as_ctype(self) -> TCTags { + fn as_ctype(self) -> TCStrings { // emulate Vec::into_raw_parts(): // - disable dropping the Vec with ManuallyDrop // - extract ptr, len, and capacity using those methods let (items, len, _capacity) = vec_into_raw_parts(self); - TCTags { + TCStrings { len, _capacity, items, @@ -45,7 +45,7 @@ impl PassByValue for Vec>> { } } -impl Default for TCTags { +impl Default for TCStrings { fn default() -> Self { Self { len: 0, @@ -55,13 +55,13 @@ impl Default for TCTags { } } -/// Free a TCTags instance. The instance, and all TCStrings it contains, must not be used after +/// Free a TCStrings instance. The instance, and all TCStrings it contains, must not be used after /// this call. #[no_mangle] -pub extern "C" fn tc_tags_free<'a>(tctags: *mut TCTags) { +pub extern "C" fn tc_strings_free<'a>(tctags: *mut TCStrings) { debug_assert!(!tctags.is_null()); // SAFETY: - // - *tctags is a valid TCTags (caller promises to treat it as read-only) - let tags = unsafe { Vec::take_from_arg(tctags, TCTags::default()) }; + // - *tctags is a valid TCStrings (caller promises to treat it as read-only) + let tags = unsafe { Vec::take_from_arg(tctags, TCStrings::default()) }; drop_pointer_array(tags); } diff --git a/lib/src/task.rs b/lib/src/task.rs index 75bf12888..65574b583 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -1,7 +1,7 @@ use crate::traits::*; use crate::util::err_to_tcstring; use crate::{ - arrays::TCTags, replica::TCReplica, result::TCResult, status::TCStatus, string::TCString, + replica::TCReplica, result::TCResult, status::TCStatus, string::TCString, strings::TCStrings, uuid::TCUuid, }; use chrono::{DateTime, TimeZone, Utc}; @@ -319,7 +319,7 @@ pub extern "C" fn tc_task_has_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> /// Get the tags for the task. The task must not be NULL. #[no_mangle] -pub extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCTags { +pub extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCStrings { wrap(task, |task| { let tcstrings: Vec>> = task .get_tags() diff --git a/lib/taskchampion.h b/lib/taskchampion.h index b60658d58..84c471b26 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -118,14 +118,23 @@ typedef struct TCString TCString; typedef struct TCTask TCTask; /** - * TCTags represents a list of tags associated with a task. + * TCUuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. + * Uuids are typically treated as opaque, but the bytes are available in big-endian format. + * + */ +typedef struct TCUuid { + uint8_t bytes[16]; +} TCUuid; + +/** + * TCStrings represents a list of tags associated with a task. * * The content of this struct must be treated as read-only. * - * The lifetime of a TCTags instance is independent of the task, and it + * The lifetime of a TCStrings instance is independent of the task, and it * will remain valid even if the task is freed. */ -typedef struct TCTags { +typedef struct TCStrings { /** * number of tags in items */ @@ -135,31 +144,16 @@ typedef struct TCTags { */ size_t _capacity; /** - * strings representing each tag. these remain owned by the TCTags instance and will be freed - * by tc_tags_free. + * strings representing each tag. these remain owned by the TCStrings instance and will be freed + * by tc_strings_free. */ struct TCString *const *items; -} TCTags; - -/** - * TCUuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. - * Uuids are typically treated as opaque, but the bytes are available in big-endian format. - * - */ -typedef struct TCUuid { - uint8_t bytes[16]; -} TCUuid; +} TCStrings; #ifdef __cplusplus extern "C" { #endif // __cplusplus -/** - * Free a TCTags instance. The instance, and all TCStrings it contains, must not be used after - * this call. - */ -void tc_tags_free(struct TCTags *tctags); - /** * Create a new TCReplica with an in-memory database. The contents of the database will be * lost when it is freed. @@ -278,6 +272,12 @@ const char *tc_string_content_with_len(struct TCString *tcstring, size_t *len_ou */ void tc_string_free(struct TCString *tcstring); +/** + * Free a TCStrings instance. The instance, and all TCStrings it contains, must not be used after + * this call. + */ +void tc_strings_free(struct TCStrings *tctags); + /** * Convert an immutable task into a mutable task. * @@ -357,7 +357,7 @@ bool tc_task_has_tag(struct TCTask *task, struct TCString *tag); /** * Get the tags for the task. The task must not be NULL. */ -struct TCTags tc_task_get_tags(struct TCTask *task); +struct TCStrings tc_task_get_tags(struct TCTask *task); /** * Set a mutable task's status. From 1e585ba0d94dc825e7c08b9a33b95af45aa650bd Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 6 Feb 2022 16:46:19 +0000 Subject: [PATCH 48/95] comment updates --- lib/src/strings.rs | 5 +---- lib/src/task.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/lib/src/strings.rs b/lib/src/strings.rs index 341680de3..c9f30460d 100644 --- a/lib/src/strings.rs +++ b/lib/src/strings.rs @@ -3,12 +3,9 @@ use crate::traits::*; use crate::util::{drop_pointer_array, vec_into_raw_parts}; use std::ptr::NonNull; -/// TCStrings represents a list of tags associated with a task. +/// TCStrings represents a list of string. /// /// The content of this struct must be treated as read-only. -/// -/// The lifetime of a TCStrings instance is independent of the task, and it -/// will remain valid even if the task is freed. #[repr(C)] pub struct TCStrings { /// number of tags in items diff --git a/lib/src/task.rs b/lib/src/task.rs index 65574b583..fff4c490a 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -11,8 +11,6 @@ use std::ptr::NonNull; use std::str::FromStr; use taskchampion::{Tag, Task, TaskMut}; -// TODO: use NonNull more - /// A task, as publicly exposed by this library. /// /// A task begins in "immutable" mode. It must be converted to "mutable" mode @@ -301,9 +299,8 @@ pub extern "C" fn tc_task_is_active(task: *mut TCTask) -> bool { wrap(task, |task| task.is_active()) } -/// Check if a task has the given tag. If the tag is invalid, this function will simply return -/// false, with no error from `tc_task_error`. -// TODO: why no error?? +/// Check if a task has the given tag. If the tag is invalid, this function will return false, as +/// that (invalid) tag is not present. No error will be reported via `tc_task_error`. #[no_mangle] pub extern "C" fn tc_task_has_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> bool { // SAFETY: see TCString docstring @@ -317,7 +314,10 @@ pub extern "C" fn tc_task_has_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> }) } -/// Get the tags for the task. The task must not be NULL. +/// Get the tags for the task. +/// +/// The caller must free the returned TCStrings instance. The TCStrings instance does not +/// reference the task and the two may be freed in any order. #[no_mangle] pub extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCStrings { wrap(task, |task| { From e11506ee6a70fc2f14d5c3a45739c88c9400cc8e Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 6 Feb 2022 23:05:33 +0000 Subject: [PATCH 49/95] always implement traits for C type --- lib/src/atomic.rs | 12 ++++++------ lib/src/replica.rs | 6 +++--- lib/src/string.rs | 2 +- lib/src/strings.rs | 28 ++++++++++++++-------------- lib/src/task.rs | 4 ++-- lib/src/traits.rs | 25 +++++++++++++------------ lib/src/uuid.rs | 22 +++++++++++----------- lib/taskchampion.h | 20 ++++++++++---------- 8 files changed, 60 insertions(+), 59 deletions(-) diff --git a/lib/src/atomic.rs b/lib/src/atomic.rs index 341f7b339..db8315922 100644 --- a/lib/src/atomic.rs +++ b/lib/src/atomic.rs @@ -3,13 +3,13 @@ use crate::traits::*; impl PassByValue for usize { - type CType = usize; + type RustType = usize; - unsafe fn from_ctype(arg: usize) -> usize { - arg - } - - fn as_ctype(self) -> usize { + unsafe fn from_ctype(self) -> usize { self } + + fn as_ctype(arg: usize) -> usize { + arg + } } diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 131a8eb91..d393d95c2 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -1,7 +1,7 @@ use crate::traits::*; use crate::util::err_to_tcstring; use crate::{result::TCResult, status::TCStatus, string::TCString, task::TCTask, uuid::TCUuid}; -use taskchampion::{Replica, StorageConfig, Uuid}; +use taskchampion::{Replica, StorageConfig}; /// A replica represents an instance of a user's task data, providing an easy interface /// for querying and modifying that data. @@ -143,7 +143,7 @@ pub extern "C" fn tc_replica_get_task(rep: *mut TCReplica, tcuuid: TCUuid) -> *m rep, |rep| { // SAFETY: see TCUuid docstring - let uuid = unsafe { Uuid::from_arg(tcuuid) }; + let uuid = unsafe { TCUuid::from_arg(tcuuid) }; if let Some(task) = rep.get_task(uuid)? { Ok(TCTask::from(task).return_val()) } else { @@ -187,7 +187,7 @@ pub extern "C" fn tc_replica_import_task_with_uuid( rep, |rep| { // SAFETY: see TCUuid docstring - let uuid = unsafe { Uuid::from_arg(tcuuid) }; + let uuid = unsafe { TCUuid::from_arg(tcuuid) }; let task = rep.import_task_with_uuid(uuid)?; Ok(TCTask::from(task).return_val()) }, diff --git a/lib/src/string.rs b/lib/src/string.rs index 3e95e8f6e..138ff31ea 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -255,7 +255,7 @@ pub extern "C" fn tc_string_content_with_len( // - len_out is not NULL (promised by caller) // - len_out points to valid memory (promised by caller) // - len_out is properly aligned (C convention) - unsafe { bytes.len().to_arg_out(len_out) }; + unsafe { usize::to_arg_out(bytes.len(), len_out) }; bytes.as_ptr() as *const libc::c_char } diff --git a/lib/src/strings.rs b/lib/src/strings.rs index c9f30460d..7d7e5bda3 100644 --- a/lib/src/strings.rs +++ b/lib/src/strings.rs @@ -3,37 +3,37 @@ use crate::traits::*; use crate::util::{drop_pointer_array, vec_into_raw_parts}; use std::ptr::NonNull; -/// TCStrings represents a list of string. +/// TCStrings represents a list of strings. /// /// The content of this struct must be treated as read-only. #[repr(C)] pub struct TCStrings { - /// number of tags in items + /// number of strings in items len: libc::size_t, /// total size of items (internal use only) _capacity: libc::size_t, - /// strings representing each tag. these remain owned by the TCStrings instance and will be freed + /// strings representing each string. these remain owned by the TCStrings instance and will be freed /// by tc_strings_free. items: *const NonNull>, } -impl PassByValue for Vec>> { - type CType = TCStrings; +impl PassByValue for TCStrings { + type RustType = Vec>>; - unsafe fn from_ctype(arg: TCStrings) -> Self { + unsafe fn from_ctype(self) -> Self::RustType { // SAFETY: // - C treats TCStrings as read-only, so items, len, and _capacity all came // from a Vec originally. - unsafe { Vec::from_raw_parts(arg.items as *mut _, arg.len, arg._capacity) } + unsafe { Vec::from_raw_parts(self.items as *mut _, self.len, self._capacity) } } - fn as_ctype(self) -> TCStrings { + fn as_ctype(arg: Self::RustType) -> Self { // emulate Vec::into_raw_parts(): // - disable dropping the Vec with ManuallyDrop // - extract ptr, len, and capacity using those methods - let (items, len, _capacity) = vec_into_raw_parts(self); + let (items, len, _capacity) = vec_into_raw_parts(arg); TCStrings { len, _capacity, @@ -55,10 +55,10 @@ impl Default for TCStrings { /// Free a TCStrings instance. The instance, and all TCStrings it contains, must not be used after /// this call. #[no_mangle] -pub extern "C" fn tc_strings_free<'a>(tctags: *mut TCStrings) { - debug_assert!(!tctags.is_null()); +pub extern "C" fn tc_strings_free<'a>(tcstrings: *mut TCStrings) { + debug_assert!(!tcstrings.is_null()); // SAFETY: - // - *tctags is a valid TCStrings (caller promises to treat it as read-only) - let tags = unsafe { Vec::take_from_arg(tctags, TCStrings::default()) }; - drop_pointer_array(tags); + // - *tcstrings is a valid TCStrings (caller promises to treat it as read-only) + let strings = unsafe { TCStrings::take_from_arg(tcstrings, TCStrings::default()) }; + drop_pointer_array(strings); } diff --git a/lib/src/task.rs b/lib/src/task.rs index fff4c490a..1a2bbaf6d 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -247,7 +247,7 @@ pub extern "C" fn tc_task_to_immut<'a>(task: *mut TCTask) { /// Get a task's UUID. #[no_mangle] pub extern "C" fn tc_task_get_uuid(task: *mut TCTask) -> TCUuid { - wrap(task, |task| task.get_uuid().return_val()) + wrap(task, |task| TCUuid::return_val(task.get_uuid())) } /// Get a task's status. @@ -331,7 +331,7 @@ pub extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCStrings { .expect("TCString::return_val() returned NULL") }) .collect(); - tcstrings.return_val() + TCStrings::return_val(tcstrings) }) } diff --git a/lib/src/traits.rs b/lib/src/traits.rs index 8011a46e4..df57d11c7 100644 --- a/lib/src/traits.rs +++ b/lib/src/traits.rs @@ -2,29 +2,30 @@ /// values are implicitly copyable, via C's struct assignment. /// /// The Rust and C types may differ, with from_ctype and as_ctype converting between them. +/// Implement this trait for the C type. pub(crate) trait PassByValue: Sized { - type CType; + type RustType; /// Convert a C value to a Rust value. /// /// # Safety /// - /// `arg` must be a valid CType. - unsafe fn from_ctype(arg: Self::CType) -> Self; + /// `self` must be a valid CType. + unsafe fn from_ctype(self) -> Self::RustType; /// Convert a Rust value to a C value. - fn as_ctype(self) -> Self::CType; + fn as_ctype(arg: Self::RustType) -> Self; /// Take a value from C as an argument. /// /// # Safety /// - /// `arg` must be a valid CType. This is typically ensured either by requiring that C + /// `self` must be a valid CType. This is typically ensured either by requiring that C /// code not modify it, or by defining the valid values in C comments. - unsafe fn from_arg(arg: Self::CType) -> Self { + unsafe fn from_arg(arg: Self) -> Self::RustType { // SAFETY: // - arg is a valid CType (promised by caller) - unsafe { Self::from_ctype(arg) } + unsafe { arg.from_ctype() } } /// Take a value from C as a pointer argument, replacing it with the given value. This is used @@ -33,7 +34,7 @@ pub(crate) trait PassByValue: Sized { /// # Safety /// /// `*arg` must be a valid CType, as with [`from_arg`]. - unsafe fn take_from_arg(arg: *mut Self::CType, mut replacement: Self::CType) -> Self { + unsafe fn take_from_arg(arg: *mut Self, mut replacement: Self) -> Self::RustType { // SAFETY: // - arg is valid (promised by caller) // - replacement is valid (guaranteed by Rust) @@ -44,8 +45,8 @@ pub(crate) trait PassByValue: Sized { } /// Return a value to C - fn return_val(self) -> Self::CType { - self.as_ctype() + fn return_val(arg: Self::RustType) -> Self { + Self::as_ctype(arg) } /// Return a value to C, via an "output parameter" @@ -54,12 +55,12 @@ pub(crate) trait PassByValue: Sized { /// /// `arg_out` must not be NULL and must be properly aligned and pointing to valid memory /// of the size of CType. - unsafe fn to_arg_out(self, arg_out: *mut Self::CType) { + unsafe fn to_arg_out(val: Self::RustType, arg_out: *mut Self) { debug_assert!(!arg_out.is_null()); // SAFETY: // - arg_out is not NULL (promised by caller, asserted) // - arg_out is properly aligned and points to valid memory (promised by caller) - unsafe { *arg_out = self.as_ctype() }; + unsafe { *arg_out = Self::as_ctype(val) }; } } diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index dfb084887..3c7751747 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -10,30 +10,30 @@ use taskchampion::Uuid; #[repr(C)] pub struct TCUuid([u8; 16]); -impl PassByValue for Uuid { - type CType = TCUuid; +impl PassByValue for TCUuid { + type RustType = Uuid; - unsafe fn from_ctype(arg: TCUuid) -> Self { + unsafe fn from_ctype(self) -> Self::RustType { // SAFETY: // - any 16-byte value is a valid Uuid - Uuid::from_bytes(arg.0) + Uuid::from_bytes(self.0) } - fn as_ctype(self) -> TCUuid { - TCUuid(*self.as_bytes()) + fn as_ctype(arg: Uuid) -> Self { + TCUuid(*arg.as_bytes()) } } /// Create a new, randomly-generated UUID. #[no_mangle] pub extern "C" fn tc_uuid_new_v4() -> TCUuid { - Uuid::new_v4().return_val() + TCUuid::return_val(Uuid::new_v4()) } /// Create a new UUID with the nil value. #[no_mangle] pub extern "C" fn tc_uuid_nil() -> TCUuid { - Uuid::nil().return_val() + TCUuid::return_val(Uuid::nil()) } // NOTE: this must be a simple constant so that cbindgen can evaluate it @@ -56,7 +56,7 @@ pub extern "C" fn tc_uuid_to_buf<'a>(tcuuid: TCUuid, buf: *mut libc::c_char) { }; // SAFETY: // - tcuuid is a valid TCUuid (all byte patterns are valid) - let uuid: Uuid = unsafe { Uuid::from_arg(tcuuid) }; + let uuid: Uuid = unsafe { TCUuid::from_arg(tcuuid) }; uuid.to_hyphenated().encode_lower(buf); } @@ -66,7 +66,7 @@ pub extern "C" fn tc_uuid_to_buf<'a>(tcuuid: TCUuid, buf: *mut libc::c_char) { pub extern "C" fn tc_uuid_to_str(tcuuid: TCUuid) -> *mut TCString<'static> { // SAFETY: // - tcuuid is a valid TCUuid (all byte patterns are valid) - let uuid: Uuid = unsafe { Uuid::from_arg(tcuuid) }; + let uuid: Uuid = unsafe { TCUuid::from_arg(tcuuid) }; let s = uuid.to_string(); // SAFETY: see TCString docstring unsafe { TCString::from(s).return_val() } @@ -85,7 +85,7 @@ pub extern "C" fn tc_uuid_from_str<'a>(s: *mut TCString, uuid_out: *mut TCUuid) // SAFETY: // - uuid_out is not NULL (promised by caller) // - alignment is not required - unsafe { u.to_arg_out(uuid_out) }; + unsafe { TCUuid::to_arg_out(u, uuid_out) }; return true; } } diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 84c471b26..818fc166f 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -127,16 +127,13 @@ typedef struct TCUuid { } TCUuid; /** - * TCStrings represents a list of tags associated with a task. + * TCStrings represents a list of strings. * * The content of this struct must be treated as read-only. - * - * The lifetime of a TCStrings instance is independent of the task, and it - * will remain valid even if the task is freed. */ typedef struct TCStrings { /** - * number of tags in items + * number of strings in items */ size_t len; /** @@ -144,7 +141,7 @@ typedef struct TCStrings { */ size_t _capacity; /** - * strings representing each tag. these remain owned by the TCStrings instance and will be freed + * strings representing each string. these remain owned by the TCStrings instance and will be freed * by tc_strings_free. */ struct TCString *const *items; @@ -276,7 +273,7 @@ void tc_string_free(struct TCString *tcstring); * Free a TCStrings instance. The instance, and all TCStrings it contains, must not be used after * this call. */ -void tc_strings_free(struct TCStrings *tctags); +void tc_strings_free(struct TCStrings *tcstrings); /** * Convert an immutable task into a mutable task. @@ -349,13 +346,16 @@ bool tc_task_is_waiting(struct TCTask *task); bool tc_task_is_active(struct TCTask *task); /** - * Check if a task has the given tag. If the tag is invalid, this function will simply return - * false, with no error from `tc_task_error`. + * Check if a task has the given tag. If the tag is invalid, this function will return false, as + * that (invalid) tag is not present. No error will be reported via `tc_task_error`. */ bool tc_task_has_tag(struct TCTask *task, struct TCString *tag); /** - * Get the tags for the task. The task must not be NULL. + * Get the tags for the task. + * + * The caller must free the returned TCStrings instance. The TCStrings instance does not + * reference the task and the two may be freed in any order. */ struct TCStrings tc_task_get_tags(struct TCTask *task); From a270b6c254eab6acff2dab2acd07227e9e8b39b6 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Mon, 7 Feb 2022 00:15:09 +0000 Subject: [PATCH 50/95] Simplify implementation of arrays --- lib/src/strings.rs | 65 +++++++++++++++++++---------------- lib/src/task.rs | 4 +-- lib/src/traits.rs | 86 ++++++++++++++++++++++++++++++++++++++++++++++ lib/src/util.rs | 22 +----------- lib/taskchampion.h | 7 ++-- 5 files changed, 130 insertions(+), 54 deletions(-) diff --git a/lib/src/strings.rs b/lib/src/strings.rs index 7d7e5bda3..55572ec25 100644 --- a/lib/src/strings.rs +++ b/lib/src/strings.rs @@ -1,6 +1,5 @@ use crate::string::TCString; use crate::traits::*; -use crate::util::{drop_pointer_array, vec_into_raw_parts}; use std::ptr::NonNull; /// TCStrings represents a list of strings. @@ -14,51 +13,59 @@ pub struct TCStrings { /// total size of items (internal use only) _capacity: libc::size_t, - /// strings representing each string. these remain owned by the TCStrings instance and will be freed - /// by tc_strings_free. + /// TCStrings representing each string. these remain owned by the TCStrings instance and will + /// be freed by tc_strings_free. This pointer is never NULL for a valid TCStrings, and the + /// *TCStrings at indexes 0..len-1 are not NULL. items: *const NonNull>, } -impl PassByValue for TCStrings { - type RustType = Vec>>; +impl PointerArray for TCStrings { + type Element = TCString<'static>; - unsafe fn from_ctype(self) -> Self::RustType { - // SAFETY: - // - C treats TCStrings as read-only, so items, len, and _capacity all came - // from a Vec originally. - unsafe { Vec::from_raw_parts(self.items as *mut _, self.len, self._capacity) } - } - - fn as_ctype(arg: Self::RustType) -> Self { - // emulate Vec::into_raw_parts(): - // - disable dropping the Vec with ManuallyDrop - // - extract ptr, len, and capacity using those methods - let (items, len, _capacity) = vec_into_raw_parts(arg); + unsafe fn from_raw_parts(items: *const NonNull, len: usize, cap: usize) -> Self { TCStrings { len, - _capacity, + _capacity: cap, items, } } -} -impl Default for TCStrings { - fn default() -> Self { - Self { - len: 0, - _capacity: 0, - items: std::ptr::null(), - } + fn into_raw_parts(self) -> (*const NonNull, usize, usize) { + (self.items, self.len, self._capacity) } } /// Free a TCStrings instance. The instance, and all TCStrings it contains, must not be used after /// this call. +/// +/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCStrings. #[no_mangle] -pub extern "C" fn tc_strings_free<'a>(tcstrings: *mut TCStrings) { +pub extern "C" fn tc_strings_free(tcstrings: *mut TCStrings) { debug_assert!(!tcstrings.is_null()); // SAFETY: // - *tcstrings is a valid TCStrings (caller promises to treat it as read-only) - let strings = unsafe { TCStrings::take_from_arg(tcstrings, TCStrings::default()) }; - drop_pointer_array(strings); + let strings = unsafe { TCStrings::take_from_arg(tcstrings, TCStrings::null_value()) }; + TCStrings::drop_pointer_vector(strings); +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn empty_array_has_non_null_pointer() { + let tcstrings = TCStrings::return_val(Vec::new()); + assert!(!tcstrings.items.is_null()); + assert_eq!(tcstrings.len, 0); + assert_eq!(tcstrings._capacity, 0); + } + + #[test] + fn free_sets_null_pointer() { + let mut tcstrings = TCStrings::return_val(Vec::new()); + tc_strings_free(&mut tcstrings); + assert!(tcstrings.items.is_null()); + assert_eq!(tcstrings.len, 0); + assert_eq!(tcstrings._capacity, 0); + } } diff --git a/lib/src/task.rs b/lib/src/task.rs index 1a2bbaf6d..823fbbffe 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -321,7 +321,7 @@ pub extern "C" fn tc_task_has_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> #[no_mangle] pub extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCStrings { wrap(task, |task| { - let tcstrings: Vec>> = task + let vec: Vec>> = task .get_tags() .map(|t| { NonNull::new( @@ -331,7 +331,7 @@ pub extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCStrings { .expect("TCString::return_val() returned NULL") }) .collect(); - TCStrings::return_val(tcstrings) + TCStrings::return_val(vec) }) } diff --git a/lib/src/traits.rs b/lib/src/traits.rs index df57d11c7..1e4a67dc6 100644 --- a/lib/src/traits.rs +++ b/lib/src/traits.rs @@ -1,3 +1,6 @@ +use crate::util::vec_into_raw_parts; +use std::ptr::NonNull; + /// Support for values passed to Rust by value. These are represented as full structs in C. Such /// values are implicitly copyable, via C's struct assignment. /// @@ -142,3 +145,86 @@ pub(crate) trait PassByPointer: Sized { } } } + +/// Support for arrays of objects referenced by pointer. +/// +/// The underlying C type should have three fields, containing items, length, and capacity. The +/// required trait functions just fetch and set these fields. The PassByValue trait will be +/// implemented automatically, converting between the C type and `Vec>`. For most +/// cases, it is only necessary to implement `tc_.._free` that first calls +/// `PassByValue::take_from_arg(arg, PointerArray::null_value())` to take the existing value and +/// replace it with the null value; then `PointerArray::drop_pointer_vector(..)` to drop the +/// resulting vector and all of the objects it points to. +/// +/// # Safety +/// +/// The C type must be documented as read-only. None of the fields may be modified, nor anything +/// in the `items` array. +/// +/// This class guarantees that the items pointer is non-NULL for any valid array (even when len=0), +/// and that all pointers at indexes 0..len-1 are non-NULL. +pub(crate) trait PointerArray: Sized { + type Element: 'static + PassByPointer; + + /// Create a new PointerArray from the given items, len, and capacity. + /// + /// # Safety + /// + /// The arguments must either: + /// - be NULL, 0, and 0, respectively; or + /// - be valid for Vec::from_raw_parts + unsafe fn from_raw_parts(items: *const NonNull, len: usize, cap: usize) -> Self; + + /// Get the items, len, and capacity (in that order) for this instance. These must be + /// precisely the same values passed tearlier to `from_raw_parts`. + fn into_raw_parts(self) -> (*const NonNull, usize, usize); + + /// Generate a NULL value. By default this is a NULL items pointer with zero length and + /// capacity. + fn null_value() -> Self { + // SAFETY: + // - satisfies the first case in from_raw_parts' safety documentation + unsafe { Self::from_raw_parts(std::ptr::null(), 0, 0) } + } + + /// Drop a vector of element pointers. This is a convenience function for implementing + /// tc_.._free functions. + fn drop_pointer_vector(mut vec: Vec>) { + // first, drop each of the elements in turn + for p in vec.drain(..) { + // SAFETY: + // - p is not NULL (NonNull) + // - p was generated by Rust (true for all arrays) + // - p was not modified (all arrays are immutable from C) + // - caller will not use this pointer again (promised by caller; p has been drain'd from + // the vector) + drop(unsafe { PassByPointer::take_from_arg(p.as_ptr()) }); + } + drop(vec); + } +} + +impl PassByValue for A +where + A: PointerArray, +{ + type RustType = Vec>; + + unsafe fn from_ctype(self) -> Self::RustType { + let (items, len, cap) = self.into_raw_parts(); + debug_assert!(!items.is_null()); + // SAFETY: + // - PointerArray::from_raw_parts requires that items, len, and cap be valid for + // Vec::from_raw_parts if not NULL, and they are not NULL (as promised by caller) + // - PointerArray::into_raw_parts returns precisely the values passed to from_raw_parts. + // - those parts are passed to Vec::from_raw_parts here. + unsafe { Vec::from_raw_parts(items as *mut _, len, cap) } + } + + fn as_ctype(arg: Self::RustType) -> Self { + let (items, len, cap) = vec_into_raw_parts(arg); + // SAFETY: + // - satisfies the second case in from_raw_parts' safety documentation + unsafe { Self::from_raw_parts(items, len, cap) } + } +} diff --git a/lib/src/util.rs b/lib/src/util.rs index 00676424a..bcab209ec 100644 --- a/lib/src/util.rs +++ b/lib/src/util.rs @@ -1,12 +1,10 @@ use crate::string::TCString; -use crate::traits::*; -use std::ptr::NonNull; pub(crate) fn err_to_tcstring(e: impl std::string::ToString) -> TCString<'static> { TCString::from(e.to_string()) } -/// A version of Vec::into_raw_parts, which is still unstable. Returns ptr, len, cap. +/// An implementation of Vec::into_raw_parts, which is still unstable. Returns ptr, len, cap. pub(crate) fn vec_into_raw_parts(vec: Vec) -> (*mut T, usize, usize) { // emulate Vec::into_raw_parts(): // - disable dropping the Vec with ManuallyDrop @@ -14,21 +12,3 @@ pub(crate) fn vec_into_raw_parts(vec: Vec) -> (*mut T, usize, usize) { let mut vec = std::mem::ManuallyDrop::new(vec); return (vec.as_mut_ptr(), vec.len(), vec.capacity()); } - -/// Drop an array of PassByPointer values -pub(crate) fn drop_pointer_array(mut array: Vec>) -where - T: 'static + PassByPointer, -{ - // first, drop each of the elements in turn - for p in array.drain(..) { - // SAFETY: - // - p is not NULL (NonNull) - // - p was generated by Rust (true for all arrays) - // - p was not modified (all arrays are immutable from C) - // - caller will not use this pointer again (promised by caller; p has been drain'd from - // the vector) - drop(unsafe { PassByPointer::take_from_arg(p.as_ptr()) }); - } - drop(array); -} diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 818fc166f..86aa40cba 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -141,8 +141,9 @@ typedef struct TCStrings { */ size_t _capacity; /** - * strings representing each string. these remain owned by the TCStrings instance and will be freed - * by tc_strings_free. + * TCStrings representing each string. these remain owned by the TCStrings instance and will + * be freed by tc_strings_free. This pointer is never NULL for a valid TCStrings, and the + * *TCStrings at indexes 0..len-1 are not NULL. */ struct TCString *const *items; } TCStrings; @@ -272,6 +273,8 @@ void tc_string_free(struct TCString *tcstring); /** * Free a TCStrings instance. The instance, and all TCStrings it contains, must not be used after * this call. + * + * When this call returns, the `items` pointer will be NULL, signalling an invalid TCStrings. */ void tc_strings_free(struct TCStrings *tcstrings); From f96b5415c8a83fc7fbe8e81af49f9119a6f1ab8e Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 9 Feb 2022 02:59:01 +0000 Subject: [PATCH 51/95] fix some clippy warnings --- lib/src/lib.rs | 1 + lib/src/replica.rs | 4 ++-- lib/src/util.rs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 5b9160970..fe17d1efd 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,4 +1,5 @@ #![warn(unsafe_op_in_unsafe_fn)] +#![warn(clippy::undocumented_unsafe_blocks)] mod traits; mod util; diff --git a/lib/src/replica.rs b/lib/src/replica.rs index d393d95c2..efa5dcbc2 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -69,12 +69,12 @@ impl From for TCReplica { /// Utility function to allow using `?` notation to return an error value. This makes /// a mutable borrow, because most Replica methods require a `&mut`. -fn wrap<'a, T, F>(rep: *mut TCReplica, f: F, err_value: T) -> T +fn wrap(rep: *mut TCReplica, f: F, err_value: T) -> T where F: FnOnce(&mut Replica) -> anyhow::Result, { // SAFETY: see type docstring - let rep: &'a mut TCReplica = unsafe { TCReplica::from_arg_ref_mut(rep) }; + let rep: &mut TCReplica = unsafe { TCReplica::from_arg_ref_mut(rep) }; if rep.mut_borrowed { panic!("replica is borrowed and cannot be used"); } diff --git a/lib/src/util.rs b/lib/src/util.rs index bcab209ec..405a8b292 100644 --- a/lib/src/util.rs +++ b/lib/src/util.rs @@ -10,5 +10,5 @@ pub(crate) fn vec_into_raw_parts(vec: Vec) -> (*mut T, usize, usize) { // - disable dropping the Vec with ManuallyDrop // - extract ptr, len, and capacity using those methods let mut vec = std::mem::ManuallyDrop::new(vec); - return (vec.as_mut_ptr(), vec.len(), vec.capacity()); + (vec.as_mut_ptr(), vec.len(), vec.capacity()) } From 5cf3ce4bc89031e8f30aca985e6c1c18dc476788 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 9 Feb 2022 03:16:14 +0000 Subject: [PATCH 52/95] comment out failing clippy lint --- lib/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/lib.rs b/lib/src/lib.rs index fe17d1efd..2451efdaa 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,5 +1,6 @@ #![warn(unsafe_op_in_unsafe_fn)] -#![warn(clippy::undocumented_unsafe_blocks)] +// Not working yet in stable - https://github.com/rust-lang/rust-clippy/issues/8020 +// #![warn(clippy::undocumented_unsafe_blocks)] mod traits; mod util; From ae5afff4f7d3a08aac3a671eee7471a34da17196 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 9 Feb 2022 03:20:17 +0000 Subject: [PATCH 53/95] fix another lint --- taskchampion/src/storage/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/taskchampion/src/storage/mod.rs b/taskchampion/src/storage/mod.rs index d3d494a1b..dd3c9786a 100644 --- a/taskchampion/src/storage/mod.rs +++ b/taskchampion/src/storage/mod.rs @@ -106,6 +106,7 @@ pub trait StorageTxn { fn clear_working_set(&mut self) -> Result<()>; /// Check whether this storage is entirely empty + #[allow(clippy::wrong_self_convention)] // mut is required here for storage access fn is_empty(&mut self) -> Result { let mut empty = true; empty = empty && self.all_tasks()?.is_empty(); From 8caf442e3f4a3d492144a856a88fd7eeff67b240 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 9 Feb 2022 23:26:39 +0000 Subject: [PATCH 54/95] mark all extern-C functions as unsafe --- lib/src/replica.rs | 16 +++++++-------- lib/src/string.rs | 12 +++++------ lib/src/strings.rs | 5 +++-- lib/src/task.rs | 50 +++++++++++++++++++++++----------------------- lib/src/uuid.rs | 10 +++++----- 5 files changed, 47 insertions(+), 46 deletions(-) diff --git a/lib/src/replica.rs b/lib/src/replica.rs index efa5dcbc2..07cb3f7a8 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -91,7 +91,7 @@ where /// Create a new TCReplica with an in-memory database. The contents of the database will be /// lost when it is freed. #[no_mangle] -pub extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica { +pub unsafe extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica { let storage = StorageConfig::InMemory .into_storage() .expect("in-memory always succeeds"); @@ -102,7 +102,7 @@ pub extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica { /// Create a new TCReplica with an on-disk database having the given filename. On error, a string /// is written to the `error_out` parameter (if it is not NULL) and NULL is returned. #[no_mangle] -pub extern "C" fn tc_replica_new_on_disk<'a>( +pub unsafe extern "C" fn tc_replica_new_on_disk<'a>( path: *mut TCString, error_out: *mut *mut TCString, ) -> *mut TCReplica { @@ -138,7 +138,7 @@ pub extern "C" fn tc_replica_new_on_disk<'a>( /// Returns NULL when the task does not exist, and on error. Consult tc_replica_error /// to distinguish the two conditions. #[no_mangle] -pub extern "C" fn tc_replica_get_task(rep: *mut TCReplica, tcuuid: TCUuid) -> *mut TCTask { +pub unsafe extern "C" fn tc_replica_get_task(rep: *mut TCReplica, tcuuid: TCUuid) -> *mut TCTask { wrap( rep, |rep| { @@ -158,7 +158,7 @@ pub extern "C" fn tc_replica_get_task(rep: *mut TCReplica, tcuuid: TCUuid) -> *m /// /// Returns the task, or NULL on error. #[no_mangle] -pub extern "C" fn tc_replica_new_task( +pub unsafe extern "C" fn tc_replica_new_task( rep: *mut TCReplica, status: TCStatus, description: *mut TCString, @@ -179,7 +179,7 @@ pub extern "C" fn tc_replica_new_task( /// /// Returns the task, or NULL on error. #[no_mangle] -pub extern "C" fn tc_replica_import_task_with_uuid( +pub unsafe extern "C" fn tc_replica_import_task_with_uuid( rep: *mut TCReplica, tcuuid: TCUuid, ) -> *mut TCTask { @@ -202,7 +202,7 @@ pub extern "C" fn tc_replica_import_task_with_uuid( /// If undone_out is not NULL, then on success it is set to 1 if operations were undone, or 0 if /// there are no operations that can be done. #[no_mangle] -pub extern "C" fn tc_replica_undo<'a>(rep: *mut TCReplica, undone_out: *mut i32) -> TCResult { +pub unsafe extern "C" fn tc_replica_undo<'a>(rep: *mut TCReplica, undone_out: *mut i32) -> TCResult { wrap( rep, |rep| { @@ -223,7 +223,7 @@ pub extern "C" fn tc_replica_undo<'a>(rep: *mut TCReplica, undone_out: *mut i32) /// to this function will return NULL. The rep pointer must not be NULL. The caller must free the /// returned string. #[no_mangle] -pub extern "C" fn tc_replica_error<'a>(rep: *mut TCReplica) -> *mut TCString<'static> { +pub unsafe extern "C" fn tc_replica_error<'a>(rep: *mut TCReplica) -> *mut TCString<'static> { // SAFETY: see type docstring let rep: &'a mut TCReplica = unsafe { TCReplica::from_arg_ref_mut(rep) }; if let Some(tcstring) = rep.error.take() { @@ -237,7 +237,7 @@ pub extern "C" fn tc_replica_error<'a>(rep: *mut TCReplica) -> *mut TCString<'st /// Free a replica. The replica may not be used after this function returns and must not be freed /// more than once. #[no_mangle] -pub extern "C" fn tc_replica_free(rep: *mut TCReplica) { +pub unsafe extern "C" fn tc_replica_free(rep: *mut TCReplica) { // SAFETY: see type docstring let replica = unsafe { TCReplica::take_from_arg(rep) }; if replica.mut_borrowed { diff --git a/lib/src/string.rs b/lib/src/string.rs index 138ff31ea..b29258ae0 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -139,7 +139,7 @@ impl<'a> From<&str> for TCString<'static> { /// free(url); // string is no longer referenced and can be freed /// ``` #[no_mangle] -pub extern "C" fn tc_string_borrow(cstr: *const libc::c_char) -> *mut TCString<'static> { +pub unsafe extern "C" fn tc_string_borrow(cstr: *const libc::c_char) -> *mut TCString<'static> { debug_assert!(!cstr.is_null()); // SAFETY: // - cstr is not NULL (promised by caller, verified by assertion) @@ -154,7 +154,7 @@ pub extern "C" fn tc_string_borrow(cstr: *const libc::c_char) -> *mut TCString<' /// Create a new TCString by cloning the content of the given C string. The resulting TCString /// is independent of the given string, which can be freed or overwritten immediately. #[no_mangle] -pub extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> *mut TCString<'static> { +pub unsafe extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> *mut TCString<'static> { debug_assert!(!cstr.is_null()); // SAFETY: // - cstr is not NULL (promised by caller, verified by assertion) @@ -172,7 +172,7 @@ pub extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> *mut TCString<'s /// /// The given length must be less than half the maximum value of usize. #[no_mangle] -pub extern "C" fn tc_string_clone_with_len( +pub unsafe extern "C" fn tc_string_clone_with_len( buf: *const libc::c_char, len: usize, ) -> *mut TCString<'static> { @@ -212,7 +212,7 @@ pub extern "C" fn tc_string_clone_with_len( /// /// This function does _not_ take ownership of the TCString. #[no_mangle] -pub extern "C" fn tc_string_content(tcstring: *mut TCString) -> *const libc::c_char { +pub unsafe extern "C" fn tc_string_content(tcstring: *mut TCString) -> *const libc::c_char { // SAFETY: // - tcstring is not NULL (promised by caller) // - lifetime of tcstring outlives the lifetime of this function @@ -239,7 +239,7 @@ pub extern "C" fn tc_string_content(tcstring: *mut TCString) -> *const libc::c_c /// /// This function does _not_ take ownership of the TCString. #[no_mangle] -pub extern "C" fn tc_string_content_with_len( +pub unsafe extern "C" fn tc_string_content_with_len( tcstring: *mut TCString, len_out: *mut usize, ) -> *const libc::c_char { @@ -262,7 +262,7 @@ pub extern "C" fn tc_string_content_with_len( /// Free a TCString. The given string must not be NULL. The string must not be used /// after this function returns, and must not be freed more than once. #[no_mangle] -pub extern "C" fn tc_string_free(tcstring: *mut TCString) { +pub unsafe extern "C" fn tc_string_free(tcstring: *mut TCString) { // SAFETY: // - tcstring is not NULL (promised by caller) // - caller is exclusive owner of tcstring (promised by caller) diff --git a/lib/src/strings.rs b/lib/src/strings.rs index 55572ec25..751a0aaed 100644 --- a/lib/src/strings.rs +++ b/lib/src/strings.rs @@ -40,7 +40,7 @@ impl PointerArray for TCStrings { /// /// When this call returns, the `items` pointer will be NULL, signalling an invalid TCStrings. #[no_mangle] -pub extern "C" fn tc_strings_free(tcstrings: *mut TCStrings) { +pub unsafe extern "C" fn tc_strings_free(tcstrings: *mut TCStrings) { debug_assert!(!tcstrings.is_null()); // SAFETY: // - *tcstrings is a valid TCStrings (caller promises to treat it as read-only) @@ -63,7 +63,8 @@ mod test { #[test] fn free_sets_null_pointer() { let mut tcstrings = TCStrings::return_val(Vec::new()); - tc_strings_free(&mut tcstrings); + // SAFETY: testing expected behavior + unsafe { tc_strings_free(&mut tcstrings) }; assert!(tcstrings.items.is_null()); assert_eq!(tcstrings.len, 0); assert_eq!(tcstrings._capacity, 0); diff --git a/lib/src/task.rs b/lib/src/task.rs index 823fbbffe..f37f08a09 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -218,7 +218,7 @@ fn to_datetime(time: libc::time_t) -> Option> { /// if (!success) { ... } /// ``` #[no_mangle] -pub extern "C" fn tc_task_to_mut<'a>(task: *mut TCTask, tcreplica: *mut TCReplica) { +pub unsafe extern "C" fn tc_task_to_mut<'a>(task: *mut TCTask, tcreplica: *mut TCReplica) { // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) @@ -236,7 +236,7 @@ pub extern "C" fn tc_task_to_mut<'a>(task: *mut TCTask, tcreplica: *mut TCReplic /// /// The replica passed to `tc_task_to_mut` may be used freely after this call. #[no_mangle] -pub extern "C" fn tc_task_to_immut<'a>(task: *mut TCTask) { +pub unsafe extern "C" fn tc_task_to_immut<'a>(task: *mut TCTask) { // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) @@ -246,13 +246,13 @@ pub extern "C" fn tc_task_to_immut<'a>(task: *mut TCTask) { /// Get a task's UUID. #[no_mangle] -pub extern "C" fn tc_task_get_uuid(task: *mut TCTask) -> TCUuid { +pub unsafe extern "C" fn tc_task_get_uuid(task: *mut TCTask) -> TCUuid { wrap(task, |task| TCUuid::return_val(task.get_uuid())) } /// Get a task's status. #[no_mangle] -pub extern "C" fn tc_task_get_status<'a>(task: *mut TCTask) -> TCStatus { +pub unsafe extern "C" fn tc_task_get_status<'a>(task: *mut TCTask) -> TCStatus { wrap(task, |task| task.get_status().into()) } @@ -261,7 +261,7 @@ pub extern "C" fn tc_task_get_status<'a>(task: *mut TCTask) -> TCStatus { /// Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it /// contains embedded NUL characters). #[no_mangle] -pub extern "C" fn tc_task_get_description<'a>(task: *mut TCTask) -> *mut TCString<'static> { +pub unsafe extern "C" fn tc_task_get_description<'a>(task: *mut TCTask) -> *mut TCString<'static> { wrap(task, |task| { let descr: TCString = task.get_description().into(); // SAFETY: see TCString docstring @@ -271,38 +271,38 @@ pub extern "C" fn tc_task_get_description<'a>(task: *mut TCTask) -> *mut TCStrin /// Get the entry timestamp for a task (when it was created), or 0 if not set. #[no_mangle] -pub extern "C" fn tc_task_get_entry<'a>(task: *mut TCTask) -> libc::time_t { +pub unsafe extern "C" fn tc_task_get_entry<'a>(task: *mut TCTask) -> libc::time_t { wrap(task, |task| to_time_t(task.get_entry())) } /// Get the wait timestamp for a task, or 0 if not set. #[no_mangle] -pub extern "C" fn tc_task_get_wait<'a>(task: *mut TCTask) -> libc::time_t { +pub unsafe extern "C" fn tc_task_get_wait<'a>(task: *mut TCTask) -> libc::time_t { wrap(task, |task| to_time_t(task.get_wait())) } /// Get the modified timestamp for a task, or 0 if not set. #[no_mangle] -pub extern "C" fn tc_task_get_modified<'a>(task: *mut TCTask) -> libc::time_t { +pub unsafe extern "C" fn tc_task_get_modified<'a>(task: *mut TCTask) -> libc::time_t { wrap(task, |task| to_time_t(task.get_modified())) } /// Check if a task is waiting. #[no_mangle] -pub extern "C" fn tc_task_is_waiting(task: *mut TCTask) -> bool { +pub unsafe extern "C" fn tc_task_is_waiting(task: *mut TCTask) -> bool { wrap(task, |task| task.is_waiting()) } /// Check if a task is active (started and not stopped). #[no_mangle] -pub extern "C" fn tc_task_is_active(task: *mut TCTask) -> bool { +pub unsafe extern "C" fn tc_task_is_active(task: *mut TCTask) -> bool { wrap(task, |task| task.is_active()) } /// Check if a task has the given tag. If the tag is invalid, this function will return false, as /// that (invalid) tag is not present. No error will be reported via `tc_task_error`. #[no_mangle] -pub extern "C" fn tc_task_has_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> bool { +pub unsafe extern "C" fn tc_task_has_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> bool { // SAFETY: see TCString docstring let tcstring = unsafe { TCString::take_from_arg(tag) }; wrap(task, |task| { @@ -319,7 +319,7 @@ pub extern "C" fn tc_task_has_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> /// The caller must free the returned TCStrings instance. The TCStrings instance does not /// reference the task and the two may be freed in any order. #[no_mangle] -pub extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCStrings { +pub unsafe extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCStrings { wrap(task, |task| { let vec: Vec>> = task .get_tags() @@ -343,7 +343,7 @@ pub extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCStrings { /// Set a mutable task's status. #[no_mangle] -pub extern "C" fn tc_task_set_status<'a>(task: *mut TCTask, status: TCStatus) -> TCResult { +pub unsafe extern "C" fn tc_task_set_status<'a>(task: *mut TCTask, status: TCStatus) -> TCResult { wrap_mut( task, |task| { @@ -356,7 +356,7 @@ pub extern "C" fn tc_task_set_status<'a>(task: *mut TCTask, status: TCStatus) -> /// Set a mutable task's description. #[no_mangle] -pub extern "C" fn tc_task_set_description<'a>( +pub unsafe extern "C" fn tc_task_set_description<'a>( task: *mut TCTask, description: *mut TCString, ) -> TCResult { @@ -375,7 +375,7 @@ pub extern "C" fn tc_task_set_description<'a>( /// Set a mutable task's entry (creation time). Pass entry=0 to unset /// the entry field. #[no_mangle] -pub extern "C" fn tc_task_set_entry(task: *mut TCTask, entry: libc::time_t) -> TCResult { +pub unsafe extern "C" fn tc_task_set_entry(task: *mut TCTask, entry: libc::time_t) -> TCResult { wrap_mut( task, |task| { @@ -388,7 +388,7 @@ pub extern "C" fn tc_task_set_entry(task: *mut TCTask, entry: libc::time_t) -> T /// Set a mutable task's wait timestamp. Pass wait=0 to unset the wait field. #[no_mangle] -pub extern "C" fn tc_task_set_wait(task: *mut TCTask, wait: libc::time_t) -> TCResult { +pub unsafe extern "C" fn tc_task_set_wait(task: *mut TCTask, wait: libc::time_t) -> TCResult { wrap_mut( task, |task| { @@ -401,7 +401,7 @@ pub extern "C" fn tc_task_set_wait(task: *mut TCTask, wait: libc::time_t) -> TCR /// Set a mutable task's modified timestamp. The value cannot be zero. #[no_mangle] -pub extern "C" fn tc_task_set_modified(task: *mut TCTask, modified: libc::time_t) -> TCResult { +pub unsafe extern "C" fn tc_task_set_modified(task: *mut TCTask, modified: libc::time_t) -> TCResult { wrap_mut( task, |task| { @@ -416,7 +416,7 @@ pub extern "C" fn tc_task_set_modified(task: *mut TCTask, modified: libc::time_t /// Start a task. #[no_mangle] -pub extern "C" fn tc_task_start(task: *mut TCTask) -> TCResult { +pub unsafe extern "C" fn tc_task_start(task: *mut TCTask) -> TCResult { wrap_mut( task, |task| { @@ -429,7 +429,7 @@ pub extern "C" fn tc_task_start(task: *mut TCTask) -> TCResult { /// Stop a task. #[no_mangle] -pub extern "C" fn tc_task_stop(task: *mut TCTask) -> TCResult { +pub unsafe extern "C" fn tc_task_stop(task: *mut TCTask) -> TCResult { wrap_mut( task, |task| { @@ -442,7 +442,7 @@ pub extern "C" fn tc_task_stop(task: *mut TCTask) -> TCResult { /// Mark a task as done. #[no_mangle] -pub extern "C" fn tc_task_done(task: *mut TCTask) -> TCResult { +pub unsafe extern "C" fn tc_task_done(task: *mut TCTask) -> TCResult { wrap_mut( task, |task| { @@ -455,7 +455,7 @@ pub extern "C" fn tc_task_done(task: *mut TCTask) -> TCResult { /// Mark a task as deleted. #[no_mangle] -pub extern "C" fn tc_task_delete(task: *mut TCTask) -> TCResult { +pub unsafe extern "C" fn tc_task_delete(task: *mut TCTask) -> TCResult { wrap_mut( task, |task| { @@ -468,7 +468,7 @@ pub extern "C" fn tc_task_delete(task: *mut TCTask) -> TCResult { /// Add a tag to a mutable task. #[no_mangle] -pub extern "C" fn tc_task_add_tag(task: *mut TCTask, tag: *mut TCString) -> TCResult { +pub unsafe extern "C" fn tc_task_add_tag(task: *mut TCTask, tag: *mut TCString) -> TCResult { // SAFETY: see TCString docstring let tcstring = unsafe { TCString::take_from_arg(tag) }; wrap_mut( @@ -484,7 +484,7 @@ pub extern "C" fn tc_task_add_tag(task: *mut TCTask, tag: *mut TCString) -> TCRe /// Remove a tag from a mutable task. #[no_mangle] -pub extern "C" fn tc_task_remove_tag(task: *mut TCTask, tag: *mut TCString) -> TCResult { +pub unsafe extern "C" fn tc_task_remove_tag(task: *mut TCTask, tag: *mut TCString) -> TCResult { // SAFETY: see TCString docstring let tcstring = unsafe { TCString::take_from_arg(tag) }; wrap_mut( @@ -509,7 +509,7 @@ pub extern "C" fn tc_task_remove_tag(task: *mut TCTask, tag: *mut TCString) -> T /// to this function will return NULL. The task pointer must not be NULL. The caller must free the /// returned string. #[no_mangle] -pub extern "C" fn tc_task_error<'a>(task: *mut TCTask) -> *mut TCString<'static> { +pub unsafe extern "C" fn tc_task_error<'a>(task: *mut TCTask) -> *mut TCString<'static> { // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) @@ -526,7 +526,7 @@ pub extern "C" fn tc_task_error<'a>(task: *mut TCTask) -> *mut TCString<'static> /// /// If the task is currently mutable, it will first be made immutable. #[no_mangle] -pub extern "C" fn tc_task_free<'a>(task: *mut TCTask) { +pub unsafe extern "C" fn tc_task_free<'a>(task: *mut TCTask) { // SAFETY: // - rep is not NULL (promised by caller) // - caller will not use the TCTask after this (promised by caller) diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index 3c7751747..cef445e40 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -26,13 +26,13 @@ impl PassByValue for TCUuid { /// Create a new, randomly-generated UUID. #[no_mangle] -pub extern "C" fn tc_uuid_new_v4() -> TCUuid { +pub unsafe extern "C" fn tc_uuid_new_v4() -> TCUuid { TCUuid::return_val(Uuid::new_v4()) } /// Create a new UUID with the nil value. #[no_mangle] -pub extern "C" fn tc_uuid_nil() -> TCUuid { +pub unsafe extern "C" fn tc_uuid_nil() -> TCUuid { TCUuid::return_val(Uuid::nil()) } @@ -43,7 +43,7 @@ pub const TC_UUID_STRING_BYTES: usize = 36; /// Write the string representation of a TCUuid into the given buffer, which must be /// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. #[no_mangle] -pub extern "C" fn tc_uuid_to_buf<'a>(tcuuid: TCUuid, buf: *mut libc::c_char) { +pub unsafe extern "C" fn tc_uuid_to_buf<'a>(tcuuid: TCUuid, buf: *mut libc::c_char) { debug_assert!(!buf.is_null()); // SAFETY: // - buf is valid for len bytes (by C convention) @@ -63,7 +63,7 @@ pub extern "C" fn tc_uuid_to_buf<'a>(tcuuid: TCUuid, buf: *mut libc::c_char) { /// Write the string representation of a TCUuid into the given buffer, which must be /// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. #[no_mangle] -pub extern "C" fn tc_uuid_to_str(tcuuid: TCUuid) -> *mut TCString<'static> { +pub unsafe extern "C" fn tc_uuid_to_str(tcuuid: TCUuid) -> *mut TCString<'static> { // SAFETY: // - tcuuid is a valid TCUuid (all byte patterns are valid) let uuid: Uuid = unsafe { TCUuid::from_arg(tcuuid) }; @@ -74,7 +74,7 @@ pub extern "C" fn tc_uuid_to_str(tcuuid: TCUuid) -> *mut TCString<'static> { /// Parse the given string as a UUID. Returns false on failure. #[no_mangle] -pub extern "C" fn tc_uuid_from_str<'a>(s: *mut TCString, uuid_out: *mut TCUuid) -> bool { +pub unsafe extern "C" fn tc_uuid_from_str<'a>(s: *mut TCString, uuid_out: *mut TCUuid) -> bool { // TODO: TCResult instead debug_assert!(!s.is_null()); debug_assert!(!uuid_out.is_null()); From 28a4599a6a5fbb9a86b30eb847bbd413c9b8be6b Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 9 Feb 2022 23:37:32 +0000 Subject: [PATCH 55/95] rename TCStrings to TCStringList --- integration-tests/src/bindings_tests/task.c | 4 +-- lib/src/lib.rs | 2 +- lib/src/string.rs | 4 +-- lib/src/{strings.rs => stringlist.rs} | 32 ++++++++++----------- lib/src/task.rs | 15 ++++++---- lib/taskchampion.h | 26 ++++++++--------- 6 files changed, 43 insertions(+), 40 deletions(-) rename lib/src/{strings.rs => stringlist.rs} (55%) diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 2ab5e3602..5a0214ce3 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -312,7 +312,7 @@ static void test_task_get_tags(void) { TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_tag(task, tc_string_borrow("next"))); - TCStrings tags = tc_task_get_tags(task); + TCStringList tags = tc_task_get_tags(task); int found_pending = false, found_next = false; for (size_t i = 0; i < tags.len; i++) { @@ -326,7 +326,7 @@ static void test_task_get_tags(void) { TEST_ASSERT_TRUE(found_pending); TEST_ASSERT_TRUE(found_next); - tc_strings_free(&tags); + tc_string_list_free(&tags); TEST_ASSERT_NULL(tags.items); tc_task_free(task); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 2451efdaa..f60142262 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -10,6 +10,6 @@ pub mod replica; pub mod result; pub mod status; pub mod string; -pub mod strings; +pub mod stringlist; pub mod task; pub mod uuid; diff --git a/lib/src/string.rs b/lib/src/string.rs index b29258ae0..da35e1220 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -31,9 +31,9 @@ use std::str::Utf8Error; /// /// Unless specified otherwise, TaskChampion functions take ownership of a `*TCString` when it is /// given as a function argument, and the pointer is invalid when the function returns. Callers -/// must not use or free TCStrings after passing them to such API functions. +/// must not use or free TCStringList after passing them to such API functions. /// -/// TCStrings are not threadsafe. +/// TCString is not threadsafe. #[derive(PartialEq, Debug)] pub enum TCString<'a> { CString(CString), diff --git a/lib/src/strings.rs b/lib/src/stringlist.rs similarity index 55% rename from lib/src/strings.rs rename to lib/src/stringlist.rs index 751a0aaed..3daf5f3cd 100644 --- a/lib/src/strings.rs +++ b/lib/src/stringlist.rs @@ -2,28 +2,28 @@ use crate::string::TCString; use crate::traits::*; use std::ptr::NonNull; -/// TCStrings represents a list of strings. +/// TCStringList represents a list of strings. /// /// The content of this struct must be treated as read-only. #[repr(C)] -pub struct TCStrings { +pub struct TCStringList { /// number of strings in items len: libc::size_t, /// total size of items (internal use only) _capacity: libc::size_t, - /// TCStrings representing each string. these remain owned by the TCStrings instance and will - /// be freed by tc_strings_free. This pointer is never NULL for a valid TCStrings, and the - /// *TCStrings at indexes 0..len-1 are not NULL. + /// TCStringList representing each string. these remain owned by the TCStringList instance and will + /// be freed by tc_string_list_free. This pointer is never NULL for a valid TCStringList, and the + /// *TCStringList at indexes 0..len-1 are not NULL. items: *const NonNull>, } -impl PointerArray for TCStrings { +impl PointerArray for TCStringList { type Element = TCString<'static>; unsafe fn from_raw_parts(items: *const NonNull, len: usize, cap: usize) -> Self { - TCStrings { + TCStringList { len, _capacity: cap, items, @@ -35,17 +35,17 @@ impl PointerArray for TCStrings { } } -/// Free a TCStrings instance. The instance, and all TCStrings it contains, must not be used after +/// Free a TCStringList instance. The instance, and all TCStringList it contains, must not be used after /// this call. /// -/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCStrings. +/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCStringList. #[no_mangle] -pub unsafe extern "C" fn tc_strings_free(tcstrings: *mut TCStrings) { +pub unsafe extern "C" fn tc_string_list_free(tcstrings: *mut TCStringList) { debug_assert!(!tcstrings.is_null()); // SAFETY: - // - *tcstrings is a valid TCStrings (caller promises to treat it as read-only) - let strings = unsafe { TCStrings::take_from_arg(tcstrings, TCStrings::null_value()) }; - TCStrings::drop_pointer_vector(strings); + // - *tcstrings is a valid TCStringList (caller promises to treat it as read-only) + let strings = unsafe { TCStringList::take_from_arg(tcstrings, TCStringList::null_value()) }; + TCStringList::drop_pointer_vector(strings); } #[cfg(test)] @@ -54,7 +54,7 @@ mod test { #[test] fn empty_array_has_non_null_pointer() { - let tcstrings = TCStrings::return_val(Vec::new()); + let tcstrings = TCStringList::return_val(Vec::new()); assert!(!tcstrings.items.is_null()); assert_eq!(tcstrings.len, 0); assert_eq!(tcstrings._capacity, 0); @@ -62,9 +62,9 @@ mod test { #[test] fn free_sets_null_pointer() { - let mut tcstrings = TCStrings::return_val(Vec::new()); + let mut tcstrings = TCStringList::return_val(Vec::new()); // SAFETY: testing expected behavior - unsafe { tc_strings_free(&mut tcstrings) }; + unsafe { tc_string_list_free(&mut tcstrings) }; assert!(tcstrings.items.is_null()); assert_eq!(tcstrings.len, 0); assert_eq!(tcstrings._capacity, 0); diff --git a/lib/src/task.rs b/lib/src/task.rs index f37f08a09..a69aedf04 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -1,8 +1,8 @@ use crate::traits::*; use crate::util::err_to_tcstring; use crate::{ - replica::TCReplica, result::TCResult, status::TCStatus, string::TCString, strings::TCStrings, - uuid::TCUuid, + replica::TCReplica, result::TCResult, status::TCStatus, string::TCString, + stringlist::TCStringList, uuid::TCUuid, }; use chrono::{DateTime, TimeZone, Utc}; use std::convert::TryFrom; @@ -316,10 +316,10 @@ pub unsafe extern "C" fn tc_task_has_tag<'a>(task: *mut TCTask, tag: *mut TCStri /// Get the tags for the task. /// -/// The caller must free the returned TCStrings instance. The TCStrings instance does not +/// The caller must free the returned TCStringList instance. The TCStringList instance does not /// reference the task and the two may be freed in any order. #[no_mangle] -pub unsafe extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCStrings { +pub unsafe extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCStringList { wrap(task, |task| { let vec: Vec>> = task .get_tags() @@ -331,7 +331,7 @@ pub unsafe extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCStrings { .expect("TCString::return_val() returned NULL") }) .collect(); - TCStrings::return_val(vec) + TCStringList::return_val(vec) }) } @@ -401,7 +401,10 @@ pub unsafe extern "C" fn tc_task_set_wait(task: *mut TCTask, wait: libc::time_t) /// Set a mutable task's modified timestamp. The value cannot be zero. #[no_mangle] -pub unsafe extern "C" fn tc_task_set_modified(task: *mut TCTask, modified: libc::time_t) -> TCResult { +pub unsafe extern "C" fn tc_task_set_modified( + task: *mut TCTask, + modified: libc::time_t, +) -> TCResult { wrap_mut( task, |task| { diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 86aa40cba..fa15ae95d 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -91,9 +91,9 @@ typedef struct TCReplica TCReplica; * * Unless specified otherwise, TaskChampion functions take ownership of a `*TCString` when it is * given as a function argument, and the pointer is invalid when the function returns. Callers - * must not use or free TCStrings after passing them to such API functions. + * must not use or free TCStringList after passing them to such API functions. * - * TCStrings are not threadsafe. + * TCString is not threadsafe. */ typedef struct TCString TCString; @@ -127,11 +127,11 @@ typedef struct TCUuid { } TCUuid; /** - * TCStrings represents a list of strings. + * TCStringList represents a list of strings. * * The content of this struct must be treated as read-only. */ -typedef struct TCStrings { +typedef struct TCStringList { /** * number of strings in items */ @@ -141,12 +141,12 @@ typedef struct TCStrings { */ size_t _capacity; /** - * TCStrings representing each string. these remain owned by the TCStrings instance and will - * be freed by tc_strings_free. This pointer is never NULL for a valid TCStrings, and the - * *TCStrings at indexes 0..len-1 are not NULL. + * TCStringList representing each string. these remain owned by the TCStringList instance and will + * be freed by tc_string_list_free. This pointer is never NULL for a valid TCStringList, and the + * *TCStringList at indexes 0..len-1 are not NULL. */ struct TCString *const *items; -} TCStrings; +} TCStringList; #ifdef __cplusplus extern "C" { @@ -271,12 +271,12 @@ const char *tc_string_content_with_len(struct TCString *tcstring, size_t *len_ou void tc_string_free(struct TCString *tcstring); /** - * Free a TCStrings instance. The instance, and all TCStrings it contains, must not be used after + * Free a TCStringList instance. The instance, and all TCStringList it contains, must not be used after * this call. * - * When this call returns, the `items` pointer will be NULL, signalling an invalid TCStrings. + * When this call returns, the `items` pointer will be NULL, signalling an invalid TCStringList. */ -void tc_strings_free(struct TCStrings *tcstrings); +void tc_string_list_free(struct TCStringList *tcstrings); /** * Convert an immutable task into a mutable task. @@ -357,10 +357,10 @@ bool tc_task_has_tag(struct TCTask *task, struct TCString *tag); /** * Get the tags for the task. * - * The caller must free the returned TCStrings instance. The TCStrings instance does not + * The caller must free the returned TCStringList instance. The TCStringList instance does not * reference the task and the two may be freed in any order. */ -struct TCStrings tc_task_get_tags(struct TCTask *task); +struct TCStringList tc_task_get_tags(struct TCTask *task); /** * Set a mutable task's status. From c9c72b4fd33e439b2d4bbd65d2c2b59f7469b8d1 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 9 Feb 2022 23:43:23 +0000 Subject: [PATCH 56/95] return TCResult from tc_uuid_from_str --- integration-tests/src/bindings_tests/replica.c | 4 ++-- integration-tests/src/bindings_tests/uuid.c | 8 ++++---- lib/src/uuid.rs | 12 ++++++------ lib/taskchampion.h | 5 +++-- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/integration-tests/src/bindings_tests/replica.c b/integration-tests/src/bindings_tests/replica.c index bf3cdaa43..11fc6d85c 100644 --- a/integration-tests/src/bindings_tests/replica.c +++ b/integration-tests/src/bindings_tests/replica.c @@ -79,7 +79,7 @@ static void test_replica_task_import(void) { TEST_ASSERT_NULL(tc_replica_error(rep)); TCUuid uuid; - TEST_ASSERT_TRUE(tc_uuid_from_str(tc_string_borrow("23cb25e0-5d1a-4932-8131-594ac6d3a843"), &uuid)); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_uuid_from_str(tc_string_borrow("23cb25e0-5d1a-4932-8131-594ac6d3a843"), &uuid)); TCTask *task = tc_replica_import_task_with_uuid(rep, uuid); TEST_ASSERT_NOT_NULL(task); @@ -110,7 +110,7 @@ static void test_replica_get_task_not_found(void) { TEST_ASSERT_NULL(tc_replica_error(rep)); TCUuid uuid; - TEST_ASSERT_TRUE(tc_uuid_from_str(tc_string_borrow("23cb25e0-5d1a-4932-8131-594ac6d3a843"), &uuid)); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_uuid_from_str(tc_string_borrow("23cb25e0-5d1a-4932-8131-594ac6d3a843"), &uuid)); TCTask *task = tc_replica_get_task(rep, uuid); TEST_ASSERT_NULL(task); TEST_ASSERT_NULL(tc_replica_error(rep)); diff --git a/integration-tests/src/bindings_tests/uuid.c b/integration-tests/src/bindings_tests/uuid.c index 572a85322..0c9b72be1 100644 --- a/integration-tests/src/bindings_tests/uuid.c +++ b/integration-tests/src/bindings_tests/uuid.c @@ -33,7 +33,7 @@ static void test_uuid_to_str(void) { static void test_uuid_valid_from_str(void) { TCUuid u; char *ustr = "23cb25e0-5d1a-4932-8131-594ac6d3a843"; - TEST_ASSERT_TRUE(tc_uuid_from_str(tc_string_borrow(ustr), &u)); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_uuid_from_str(tc_string_borrow(ustr), &u)); TEST_ASSERT_EQUAL(0x23, u.bytes[0]); TEST_ASSERT_EQUAL(0x43, u.bytes[15]); } @@ -42,20 +42,20 @@ static void test_uuid_valid_from_str(void) { static void test_uuid_invalid_string_fails(void) { TCUuid u; char *ustr = "not-a-valid-uuid"; - TEST_ASSERT_FALSE(tc_uuid_from_str(tc_string_borrow(ustr), &u)); + TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_uuid_from_str(tc_string_borrow(ustr), &u)); } // converting invalid UTF-8 UUIDs from string fails as expected static void test_uuid_bad_utf8(void) { TCUuid u; char *ustr = "\xf0\x28\x8c\xbc"; - TEST_ASSERT_FALSE(tc_uuid_from_str(tc_string_borrow(ustr), &u)); + TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_uuid_from_str(tc_string_borrow(ustr), &u)); } // converting a string with embedded NUL fails as expected static void test_uuid_embedded_nul(void) { TCUuid u; - TEST_ASSERT_FALSE(tc_uuid_from_str(tc_string_clone_with_len("ab\0de", 5), &u)); + TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_uuid_from_str(tc_string_clone_with_len("ab\0de", 5), &u)); } int uuid_tests(void) { diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index cef445e40..415e9501a 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -1,5 +1,5 @@ -use crate::string::TCString; use crate::traits::*; +use crate::{result::TCResult, string::TCString}; use libc; use taskchampion::Uuid; @@ -72,10 +72,10 @@ pub unsafe extern "C" fn tc_uuid_to_str(tcuuid: TCUuid) -> *mut TCString<'static unsafe { TCString::from(s).return_val() } } -/// Parse the given string as a UUID. Returns false on failure. +/// Parse the given string as a UUID. Returns TC_RESULT_ERROR on parse failure or if the given +/// string is not valid. #[no_mangle] -pub unsafe extern "C" fn tc_uuid_from_str<'a>(s: *mut TCString, uuid_out: *mut TCUuid) -> bool { - // TODO: TCResult instead +pub unsafe extern "C" fn tc_uuid_from_str<'a>(s: *mut TCString, uuid_out: *mut TCUuid) -> TCResult { debug_assert!(!s.is_null()); debug_assert!(!uuid_out.is_null()); // SAFETY: see TCString docstring @@ -86,8 +86,8 @@ pub unsafe extern "C" fn tc_uuid_from_str<'a>(s: *mut TCString, uuid_out: *mut T // - uuid_out is not NULL (promised by caller) // - alignment is not required unsafe { TCUuid::to_arg_out(u, uuid_out) }; - return true; + return TCResult::Ok; } } - false + TCResult::Error } diff --git a/lib/taskchampion.h b/lib/taskchampion.h index fa15ae95d..4ac06acfd 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -456,9 +456,10 @@ void tc_uuid_to_buf(struct TCUuid tcuuid, char *buf); struct TCString *tc_uuid_to_str(struct TCUuid tcuuid); /** - * Parse the given string as a UUID. Returns false on failure. + * Parse the given string as a UUID. Returns TC_RESULT_ERROR on parse failure or if the given + * string is not valid. */ -bool tc_uuid_from_str(struct TCString *s, struct TCUuid *uuid_out); +TCResult tc_uuid_from_str(struct TCString *s, struct TCUuid *uuid_out); #ifdef __cplusplus } // extern "C" From 914017b46c49e712431a1427c9754151845894c0 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 10 Feb 2022 00:10:39 +0000 Subject: [PATCH 57/95] tc_replica_all_tasks --- .../src/bindings_tests/replica.c | 44 ++++++++++++ lib/src/lib.rs | 1 + lib/src/replica.rs | 49 +++++++++++-- lib/src/task.rs | 16 +++-- lib/src/tasklist.rs | 72 +++++++++++++++++++ lib/taskchampion.h | 37 ++++++++++ 6 files changed, 207 insertions(+), 12 deletions(-) create mode 100644 lib/src/tasklist.rs diff --git a/integration-tests/src/bindings_tests/replica.c b/integration-tests/src/bindings_tests/replica.c index 11fc6d85c..41cf2dfe8 100644 --- a/integration-tests/src/bindings_tests/replica.c +++ b/integration-tests/src/bindings_tests/replica.c @@ -73,6 +73,49 @@ static void test_replica_task_creation(void) { tc_replica_free(rep); } +// a replica with tasks in it returns an appropriate list of tasks +static void test_replica_all_tasks(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("task1")); + TEST_ASSERT_NOT_NULL(task); + tc_task_free(task); + + task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("task2")); + TEST_ASSERT_NOT_NULL(task); + tc_task_free(task); + + TCTaskList tasks = tc_replica_all_tasks(rep); + TEST_ASSERT_NOT_NULL(tasks.items); + TEST_ASSERT_EQUAL(2, tasks.len); + + int seen1, seen2 = false; + for (size_t i = 0; i < tasks.len; i++) { + TCTask *task = tasks.items[i]; + TCString *descr = tc_task_get_description(task); + if (0 == strcmp(tc_string_content(descr), "task1")) { + seen1 = true; + } else if (0 == strcmp(tc_string_content(descr), "task2")) { + seen2 = true; + } + tc_string_free(descr); + } + TEST_ASSERT_TRUE(seen1); + TEST_ASSERT_TRUE(seen2); + + tc_task_list_free(&tasks); + TEST_ASSERT_NULL(tasks.items); + + tc_replica_free(rep); +} + // importing a task succeeds and the resulting task looks good static void test_replica_task_import(void) { TCReplica *rep = tc_replica_new_in_memory(); @@ -124,6 +167,7 @@ int replica_tests(void) { RUN_TEST(test_replica_undo_empty); RUN_TEST(test_replica_undo_empty_null_undone_out); RUN_TEST(test_replica_task_creation); + RUN_TEST(test_replica_all_tasks); RUN_TEST(test_replica_task_import); RUN_TEST(test_replica_get_task_not_found); return UNITY_END(); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index f60142262..f9e353d21 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -12,4 +12,5 @@ pub mod status; pub mod string; pub mod stringlist; pub mod task; +pub mod tasklist; pub mod uuid; diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 07cb3f7a8..b17c256a1 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -1,6 +1,10 @@ use crate::traits::*; use crate::util::err_to_tcstring; -use crate::{result::TCResult, status::TCStatus, string::TCString, task::TCTask, uuid::TCUuid}; +use crate::{ + result::TCResult, status::TCStatus, string::TCString, task::TCTask, tasklist::TCTaskList, + uuid::TCUuid, +}; +use std::ptr::NonNull; use taskchampion::{Replica, StorageConfig}; /// A replica represents an instance of a user's task data, providing an easy interface @@ -129,7 +133,34 @@ pub unsafe extern "C" fn tc_replica_new_on_disk<'a>( unsafe { TCReplica::from(Replica::new(storage)).return_val() } } -// TODO: tc_replica_all_tasks +/// Get a list of all tasks in the replica. +/// +/// Returns a TCTaskList with a NULL items field on error. +#[no_mangle] +pub unsafe extern "C" fn tc_replica_all_tasks(rep: *mut TCReplica) -> TCTaskList { + wrap( + rep, + |rep| { + // note that the Replica API returns a hashmap here, but we discard + // the keys and return a simple array. The task UUIDs are available + // from task.get_uuid(), so information is not lost. + let tasks: Vec<_> = rep + .all_tasks()? + .drain() + .map(|(_uuid, t)| { + NonNull::new( + // SAFETY: see TCTask docstring + unsafe { TCTask::from(t).return_val() }, + ) + .expect("TCTask::return_val returned NULL") + }) + .collect(); + Ok(TCTaskList::return_val(tasks)) + }, + TCTaskList::null_value(), + ) +} + // TODO: tc_replica_all_task_uuids // TODO: tc_replica_working_set @@ -145,7 +176,8 @@ pub unsafe extern "C" fn tc_replica_get_task(rep: *mut TCReplica, tcuuid: TCUuid // SAFETY: see TCUuid docstring let uuid = unsafe { TCUuid::from_arg(tcuuid) }; if let Some(task) = rep.get_task(uuid)? { - Ok(TCTask::from(task).return_val()) + // SAFETY: caller promises to free this task + Ok(unsafe { TCTask::from(task).return_val() }) } else { Ok(std::ptr::null_mut()) } @@ -169,7 +201,8 @@ pub unsafe extern "C" fn tc_replica_new_task( rep, |rep| { let task = rep.new_task(status.into(), description.as_str()?.to_string())?; - Ok(TCTask::from(task).return_val()) + // SAFETY: caller promises to free this task + Ok(unsafe { TCTask::from(task).return_val() }) }, std::ptr::null_mut(), ) @@ -189,7 +222,8 @@ pub unsafe extern "C" fn tc_replica_import_task_with_uuid( // SAFETY: see TCUuid docstring let uuid = unsafe { TCUuid::from_arg(tcuuid) }; let task = rep.import_task_with_uuid(uuid)?; - Ok(TCTask::from(task).return_val()) + // SAFETY: caller promises to free this task + Ok(unsafe { TCTask::from(task).return_val() }) }, std::ptr::null_mut(), ) @@ -202,7 +236,10 @@ pub unsafe extern "C" fn tc_replica_import_task_with_uuid( /// If undone_out is not NULL, then on success it is set to 1 if operations were undone, or 0 if /// there are no operations that can be done. #[no_mangle] -pub unsafe extern "C" fn tc_replica_undo<'a>(rep: *mut TCReplica, undone_out: *mut i32) -> TCResult { +pub unsafe extern "C" fn tc_replica_undo<'a>( + rep: *mut TCReplica, + undone_out: *mut i32, +) -> TCResult { wrap( rep, |rep| { diff --git a/lib/src/task.rs b/lib/src/task.rs index a69aedf04..c244081ed 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -49,7 +49,10 @@ enum Inner { Invalid, } +impl PassByPointer for TCTask {} + impl TCTask { + /* /// Borrow a TCTask from C as an argument. /// /// # Safety @@ -78,6 +81,7 @@ impl TCTask { pub(crate) fn return_val(self) -> *mut TCTask { Box::into_raw(Box::new(self)) } + */ /// Make an immutable TCTask into a mutable TCTask. Does nothing if the task /// is already mutable. @@ -140,7 +144,7 @@ where // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) - let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref(task) }; + let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; let task: &'a Task = match &tctask.inner { Inner::Immutable(t) => t, Inner::Mutable(t, _) => t.deref(), @@ -160,7 +164,7 @@ where // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) - let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref(task) }; + let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; let task: &'a mut TaskMut = match tctask.inner { Inner::Immutable(_) => panic!("Task is immutable"), Inner::Mutable(ref mut t, _) => t, @@ -222,7 +226,7 @@ pub unsafe extern "C" fn tc_task_to_mut<'a>(task: *mut TCTask, tcreplica: *mut T // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) - let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref(task) }; + let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; // SAFETY: // - tcreplica is not NULL (promised by caller) // - tcreplica lives until later call to to_immut via tc_task_to_immut (promised by caller, @@ -240,7 +244,7 @@ pub unsafe extern "C" fn tc_task_to_immut<'a>(task: *mut TCTask) { // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) - let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref(task) }; + let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; tctask.to_immut(); } @@ -516,7 +520,7 @@ pub unsafe extern "C" fn tc_task_error<'a>(task: *mut TCTask) -> *mut TCString<' // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) - let task: &'a mut TCTask = unsafe { TCTask::from_arg_ref(task) }; + let task: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; if let Some(tcstring) = task.error.take() { unsafe { tcstring.return_val() } } else { @@ -533,7 +537,7 @@ pub unsafe extern "C" fn tc_task_free<'a>(task: *mut TCTask) { // SAFETY: // - rep is not NULL (promised by caller) // - caller will not use the TCTask after this (promised by caller) - let mut tctask = unsafe { TCTask::from_arg(task) }; + let mut tctask = unsafe { TCTask::take_from_arg(task) }; // convert to immut if it was mutable tctask.to_immut(); diff --git a/lib/src/tasklist.rs b/lib/src/tasklist.rs new file mode 100644 index 000000000..eaba75ca1 --- /dev/null +++ b/lib/src/tasklist.rs @@ -0,0 +1,72 @@ +use crate::task::TCTask; +use crate::traits::*; +use std::ptr::NonNull; + +/// TCTaskList represents a list of tasks. +/// +/// The content of this struct must be treated as read-only. +#[repr(C)] +pub struct TCTaskList { + /// number of tasks in items + len: libc::size_t, + + /// total size of items (internal use only) + _capacity: libc::size_t, + + /// array of pointers representing each task. these remain owned by the TCTaskList instance and + /// will be freed by tc_task_list_free. This pointer is never NULL for a valid TCTaskList, + /// and the *TCTaskList at indexes 0..len-1 are not NULL. + items: *const NonNull, +} + +impl PointerArray for TCTaskList { + type Element = TCTask; + + unsafe fn from_raw_parts(items: *const NonNull, len: usize, cap: usize) -> Self { + TCTaskList { + len, + _capacity: cap, + items, + } + } + + fn into_raw_parts(self) -> (*const NonNull, usize, usize) { + (self.items, self.len, self._capacity) + } +} + +/// Free a TCTaskList instance. The instance, and all TCTaskList it contains, must not be used after +/// this call. +/// +/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCTaskList. +#[no_mangle] +pub unsafe extern "C" fn tc_task_list_free(tctasks: *mut TCTaskList) { + debug_assert!(!tctasks.is_null()); + // SAFETY: + // - *tctasks is a valid TCTaskList (caller promises to treat it as read-only) + let tasks = unsafe { TCTaskList::take_from_arg(tctasks, TCTaskList::null_value()) }; + TCTaskList::drop_pointer_vector(tasks); +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn empty_array_has_non_null_pointer() { + let tctasks = TCTaskList::return_val(Vec::new()); + assert!(!tctasks.items.is_null()); + assert_eq!(tctasks.len, 0); + assert_eq!(tctasks._capacity, 0); + } + + #[test] + fn free_sets_null_pointer() { + let mut tctasks = TCTaskList::return_val(Vec::new()); + // SAFETY: testing expected behavior + unsafe { tc_task_list_free(&mut tctasks) }; + assert!(tctasks.items.is_null()); + assert_eq!(tctasks.len, 0); + assert_eq!(tctasks._capacity, 0); + } +} diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 4ac06acfd..e59a9c59d 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -117,6 +117,28 @@ typedef struct TCString TCString; */ typedef struct TCTask TCTask; +/** + * TCTaskList represents a list of tasks. + * + * The content of this struct must be treated as read-only. + */ +typedef struct TCTaskList { + /** + * number of tasks in items + */ + size_t len; + /** + * total size of items (internal use only) + */ + size_t _capacity; + /** + * array of pointers representing each task. these remain owned by the TCTaskList instance and + * will be freed by tc_task_list_free. This pointer is never NULL for a valid TCTaskList, + * and the *TCTaskList at indexes 0..len-1 are not NULL. + */ + struct TCTask *const *items; +} TCTaskList; + /** * TCUuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. * Uuids are typically treated as opaque, but the bytes are available in big-endian format. @@ -164,6 +186,13 @@ struct TCReplica *tc_replica_new_in_memory(void); */ struct TCReplica *tc_replica_new_on_disk(struct TCString *path, struct TCString **error_out); +/** + * Get a list of all tasks in the replica. + * + * Returns a TCTaskList with a NULL items field on error. + */ +struct TCTaskList tc_replica_all_tasks(struct TCReplica *rep); + /** * Get an existing task by its UUID. * @@ -433,6 +462,14 @@ struct TCString *tc_task_error(struct TCTask *task); */ void tc_task_free(struct TCTask *task); +/** + * Free a TCTaskList instance. The instance, and all TCTaskList it contains, must not be used after + * this call. + * + * When this call returns, the `items` pointer will be NULL, signalling an invalid TCTaskList. + */ +void tc_task_list_free(struct TCTaskList *tctasks); + /** * Create a new, randomly-generated UUID. */ From 8cbd44544c2523ade0aae993ad9b295bdd7f772f Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 10 Feb 2022 00:30:13 +0000 Subject: [PATCH 58/95] remove commented-out code --- lib/src/task.rs | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/lib/src/task.rs b/lib/src/task.rs index c244081ed..50acf04c8 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -52,37 +52,6 @@ enum Inner { impl PassByPointer for TCTask {} impl TCTask { - /* - /// Borrow a TCTask from C as an argument. - /// - /// # Safety - /// - /// The pointer must not be NULL. It is the caller's responsibility to ensure that the - /// lifetime assigned to the reference and the lifetime of the TCTask itself do not outlive - /// the lifetime promised by C. - pub(crate) unsafe fn from_arg_ref<'a>(tctask: *mut TCTask) -> &'a mut Self { - debug_assert!(!tctask.is_null()); - // SAFETY: see docstring - unsafe { &mut *tctask } - } - - /// Take a TCTask from C as an argument. - /// - /// # Safety - /// - /// The pointer must not be NULL. The pointer becomes invalid before this function returns. - pub(crate) unsafe fn from_arg<'a>(tctask: *mut TCTask) -> Self { - debug_assert!(!tctask.is_null()); - // SAFETY: see docstring - unsafe { *Box::from_raw(tctask) } - } - - /// Convert a TCTask to a return value for handing off to C. - pub(crate) fn return_val(self) -> *mut TCTask { - Box::into_raw(Box::new(self)) - } - */ - /// Make an immutable TCTask into a mutable TCTask. Does nothing if the task /// is already mutable. /// From a4d992012e469f6e917f19bacc96c6638da369c0 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 10 Feb 2022 00:55:34 +0000 Subject: [PATCH 59/95] TCUuidList, refactor traits --- .../src/bindings_tests/replica.c | 73 ++++++++---- lib/src/lib.rs | 1 + lib/src/replica.rs | 22 +++- lib/src/stringlist.rs | 10 +- lib/src/tasklist.rs | 10 +- lib/src/traits.rs | 112 +++++++++++++----- lib/src/uuidlist.rs | 70 +++++++++++ lib/taskchampion.h | 36 ++++++ 8 files changed, 271 insertions(+), 63 deletions(-) create mode 100644 lib/src/uuidlist.rs diff --git a/integration-tests/src/bindings_tests/replica.c b/integration-tests/src/bindings_tests/replica.c index 41cf2dfe8..14084ab00 100644 --- a/integration-tests/src/bindings_tests/replica.c +++ b/integration-tests/src/bindings_tests/replica.c @@ -73,45 +73,70 @@ static void test_replica_task_creation(void) { tc_replica_free(rep); } -// a replica with tasks in it returns an appropriate list of tasks +// a replica with tasks in it returns an appropriate list of tasks and list of uuids static void test_replica_all_tasks(void) { TCReplica *rep = tc_replica_new_in_memory(); TEST_ASSERT_NULL(tc_replica_error(rep)); - TCTask *task = tc_replica_new_task( + TCTask *task1 = tc_replica_new_task( rep, TC_STATUS_PENDING, tc_string_borrow("task1")); - TEST_ASSERT_NOT_NULL(task); - tc_task_free(task); + TEST_ASSERT_NOT_NULL(task1); + TCUuid uuid1 = tc_task_get_uuid(task1); + tc_task_free(task1); - task = tc_replica_new_task( + TCTask *task2 = tc_replica_new_task( rep, TC_STATUS_PENDING, tc_string_borrow("task2")); - TEST_ASSERT_NOT_NULL(task); - tc_task_free(task); + TEST_ASSERT_NOT_NULL(task2); + TCUuid uuid2 = tc_task_get_uuid(task2); + tc_task_free(task2); - TCTaskList tasks = tc_replica_all_tasks(rep); - TEST_ASSERT_NOT_NULL(tasks.items); - TEST_ASSERT_EQUAL(2, tasks.len); + { + TCTaskList tasks = tc_replica_all_tasks(rep); + TEST_ASSERT_NOT_NULL(tasks.items); + TEST_ASSERT_EQUAL(2, tasks.len); - int seen1, seen2 = false; - for (size_t i = 0; i < tasks.len; i++) { - TCTask *task = tasks.items[i]; - TCString *descr = tc_task_get_description(task); - if (0 == strcmp(tc_string_content(descr), "task1")) { - seen1 = true; - } else if (0 == strcmp(tc_string_content(descr), "task2")) { - seen2 = true; + bool seen1, seen2 = false; + for (size_t i = 0; i < tasks.len; i++) { + TCTask *task = tasks.items[i]; + TCString *descr = tc_task_get_description(task); + if (0 == strcmp(tc_string_content(descr), "task1")) { + seen1 = true; + } else if (0 == strcmp(tc_string_content(descr), "task2")) { + seen2 = true; + } + tc_string_free(descr); } - tc_string_free(descr); - } - TEST_ASSERT_TRUE(seen1); - TEST_ASSERT_TRUE(seen2); + TEST_ASSERT_TRUE(seen1); + TEST_ASSERT_TRUE(seen2); - tc_task_list_free(&tasks); - TEST_ASSERT_NULL(tasks.items); + tc_task_list_free(&tasks); + TEST_ASSERT_NULL(tasks.items); + } + + { + TCUuidList uuids = tc_replica_all_task_uuids(rep); + TEST_ASSERT_NOT_NULL(uuids.items); + TEST_ASSERT_EQUAL(2, uuids.len); + + bool seen1, seen2 = false; + for (size_t i = 0; i < uuids.len; i++) { + TCUuid uuid = uuids.items[i]; + if (0 == memcmp(&uuid1, &uuid, sizeof(TCUuid))) { + seen1 = true; + } else if (0 == memcmp(&uuid2, &uuid, sizeof(TCUuid))) { + seen2 = true; + } + } + TEST_ASSERT_TRUE(seen1); + TEST_ASSERT_TRUE(seen2); + + tc_uuid_list_free(&uuids); + TEST_ASSERT_NULL(uuids.items); + } tc_replica_free(rep); } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index f9e353d21..bb4ebe905 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -14,3 +14,4 @@ pub mod stringlist; pub mod task; pub mod tasklist; pub mod uuid; +pub mod uuidlist; diff --git a/lib/src/replica.rs b/lib/src/replica.rs index b17c256a1..5119ea520 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -2,7 +2,7 @@ use crate::traits::*; use crate::util::err_to_tcstring; use crate::{ result::TCResult, status::TCStatus, string::TCString, task::TCTask, tasklist::TCTaskList, - uuid::TCUuid, + uuid::TCUuid, uuidlist::TCUuidList, }; use std::ptr::NonNull; use taskchampion::{Replica, StorageConfig}; @@ -161,7 +161,25 @@ pub unsafe extern "C" fn tc_replica_all_tasks(rep: *mut TCReplica) -> TCTaskList ) } -// TODO: tc_replica_all_task_uuids +/// Get a list of all uuids for tasks in the replica. +/// +/// Returns a TCUuidList with a NULL items field on error. +#[no_mangle] +pub unsafe extern "C" fn tc_replica_all_task_uuids(rep: *mut TCReplica) -> TCUuidList { + wrap( + rep, + |rep| { + let uuids: Vec<_> = rep + .all_task_uuids()? + .drain(..) + .map(|uuid| TCUuid::return_val(uuid)) + .collect(); + Ok(TCUuidList::return_val(uuids)) + }, + TCUuidList::null_value(), + ) +} + // TODO: tc_replica_working_set /// Get an existing task by its UUID. diff --git a/lib/src/stringlist.rs b/lib/src/stringlist.rs index 3daf5f3cd..3ee3be48e 100644 --- a/lib/src/stringlist.rs +++ b/lib/src/stringlist.rs @@ -19,10 +19,10 @@ pub struct TCStringList { items: *const NonNull>, } -impl PointerArray for TCStringList { - type Element = TCString<'static>; +impl ValueArray for TCStringList { + type Element = NonNull>; - unsafe fn from_raw_parts(items: *const NonNull, len: usize, cap: usize) -> Self { + unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { TCStringList { len, _capacity: cap, @@ -30,7 +30,7 @@ impl PointerArray for TCStringList { } } - fn into_raw_parts(self) -> (*const NonNull, usize, usize) { + fn into_raw_parts(self) -> (*const Self::Element, usize, usize) { (self.items, self.len, self._capacity) } } @@ -45,7 +45,7 @@ pub unsafe extern "C" fn tc_string_list_free(tcstrings: *mut TCStringList) { // SAFETY: // - *tcstrings is a valid TCStringList (caller promises to treat it as read-only) let strings = unsafe { TCStringList::take_from_arg(tcstrings, TCStringList::null_value()) }; - TCStringList::drop_pointer_vector(strings); + TCStringList::drop_vector(strings); } #[cfg(test)] diff --git a/lib/src/tasklist.rs b/lib/src/tasklist.rs index eaba75ca1..c383139fe 100644 --- a/lib/src/tasklist.rs +++ b/lib/src/tasklist.rs @@ -19,10 +19,10 @@ pub struct TCTaskList { items: *const NonNull, } -impl PointerArray for TCTaskList { - type Element = TCTask; +impl ValueArray for TCTaskList { + type Element = NonNull; - unsafe fn from_raw_parts(items: *const NonNull, len: usize, cap: usize) -> Self { + unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { TCTaskList { len, _capacity: cap, @@ -30,7 +30,7 @@ impl PointerArray for TCTaskList { } } - fn into_raw_parts(self) -> (*const NonNull, usize, usize) { + fn into_raw_parts(self) -> (*const Self::Element, usize, usize) { (self.items, self.len, self._capacity) } } @@ -45,7 +45,7 @@ pub unsafe extern "C" fn tc_task_list_free(tctasks: *mut TCTaskList) { // SAFETY: // - *tctasks is a valid TCTaskList (caller promises to treat it as read-only) let tasks = unsafe { TCTaskList::take_from_arg(tctasks, TCTaskList::null_value()) }; - TCTaskList::drop_pointer_vector(tasks); + TCTaskList::drop_vector(tasks); } #[cfg(test)] diff --git a/lib/src/traits.rs b/lib/src/traits.rs index 1e4a67dc6..f0bde9e33 100644 --- a/lib/src/traits.rs +++ b/lib/src/traits.rs @@ -146,38 +146,100 @@ pub(crate) trait PassByPointer: Sized { } } -/// Support for arrays of objects referenced by pointer. +/// *mut P can be passed by value +impl

PassByValue for *mut P +where + P: PassByPointer + 'static, +{ + type RustType = &'static mut P; + + unsafe fn from_ctype(self) -> Self::RustType { + // SAFETY: + // - self must be a valid *mut P (promised by caller) + // - TODO for 'static + unsafe { &mut *self } + } + + /// Convert a Rust value to a C value. + fn as_ctype(arg: Self::RustType) -> Self { + arg + } +} + +/// *const P can be passed by value +impl

PassByValue for *const P +where + P: PassByPointer + 'static, +{ + type RustType = &'static P; + + unsafe fn from_ctype(self) -> Self::RustType { + // SAFETY: + // - self must be a valid *mut P (promised by caller) + // - TODO for 'static + unsafe { &*self } + } + + /// Convert a Rust value to a C value. + fn as_ctype(arg: Self::RustType) -> Self { + arg + } +} + +/// NonNull

can be passed by value +impl

PassByValue for NonNull

+where + P: PassByPointer + 'static, +{ + type RustType = &'static mut P; + + unsafe fn from_ctype(mut self) -> Self::RustType { + // SAFETY: + // - self must be a valid *mut P (promised by caller) + // - TODO for 'static + unsafe { self.as_mut() } + } + + /// Convert a Rust value to a C value. + fn as_ctype(arg: Self::RustType) -> Self { + NonNull::new(arg).expect("value must not be NULL") + } +} + +/// Support for arrays of objects referenced by value. /// /// The underlying C type should have three fields, containing items, length, and capacity. The /// required trait functions just fetch and set these fields. The PassByValue trait will be -/// implemented automatically, converting between the C type and `Vec>`. For most -/// cases, it is only necessary to implement `tc_.._free` that first calls -/// `PassByValue::take_from_arg(arg, PointerArray::null_value())` to take the existing value and -/// replace it with the null value; then `PointerArray::drop_pointer_vector(..)` to drop the -/// resulting vector and all of the objects it points to. +/// implemented automatically, converting between the C type and `Vec`. For most cases, +/// it is only necessary to implement `tc_.._free` that first calls +/// `PassByValue::take_from_arg(arg, ValueArray::null_value())` to take the existing value and +/// replace it with the null value; then `ValueArray::drop_value_vector(..)` to drop the resulting +/// vector and all of the objects it points to. +/// +/// This can be used for objects referenced by pointer, too, with an Element type of `*const T` /// /// # Safety /// /// The C type must be documented as read-only. None of the fields may be modified, nor anything /// in the `items` array. /// -/// This class guarantees that the items pointer is non-NULL for any valid array (even when len=0), -/// and that all pointers at indexes 0..len-1 are non-NULL. -pub(crate) trait PointerArray: Sized { - type Element: 'static + PassByPointer; +/// This class guarantees that the items pointer is non-NULL for any valid array (even when len=0). +// TODO: rename Array +pub(crate) trait ValueArray: Sized { + type Element: PassByValue; - /// Create a new PointerArray from the given items, len, and capacity. + /// Create a new ValueArray from the given items, len, and capacity. /// /// # Safety /// /// The arguments must either: /// - be NULL, 0, and 0, respectively; or /// - be valid for Vec::from_raw_parts - unsafe fn from_raw_parts(items: *const NonNull, len: usize, cap: usize) -> Self; + unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self; /// Get the items, len, and capacity (in that order) for this instance. These must be /// precisely the same values passed tearlier to `from_raw_parts`. - fn into_raw_parts(self) -> (*const NonNull, usize, usize); + fn into_raw_parts(self) -> (*const Self::Element, usize, usize); /// Generate a NULL value. By default this is a NULL items pointer with zero length and /// capacity. @@ -187,36 +249,32 @@ pub(crate) trait PointerArray: Sized { unsafe { Self::from_raw_parts(std::ptr::null(), 0, 0) } } - /// Drop a vector of element pointers. This is a convenience function for implementing + /// Drop a vector of elements. This is a convenience function for implementing /// tc_.._free functions. - fn drop_pointer_vector(mut vec: Vec>) { + fn drop_vector(mut vec: Vec) { // first, drop each of the elements in turn - for p in vec.drain(..) { - // SAFETY: - // - p is not NULL (NonNull) - // - p was generated by Rust (true for all arrays) - // - p was not modified (all arrays are immutable from C) - // - caller will not use this pointer again (promised by caller; p has been drain'd from - // the vector) - drop(unsafe { PassByPointer::take_from_arg(p.as_ptr()) }); + for e in vec.drain(..) { + // SAFETY: e is a valid Element (caller promisd not to change it) + drop(unsafe { PassByValue::from_arg(e) }); } + // then drop the vector drop(vec); } } impl PassByValue for A where - A: PointerArray, + A: ValueArray, { - type RustType = Vec>; + type RustType = Vec; unsafe fn from_ctype(self) -> Self::RustType { let (items, len, cap) = self.into_raw_parts(); debug_assert!(!items.is_null()); // SAFETY: - // - PointerArray::from_raw_parts requires that items, len, and cap be valid for + // - ValueArray::from_raw_parts requires that items, len, and cap be valid for // Vec::from_raw_parts if not NULL, and they are not NULL (as promised by caller) - // - PointerArray::into_raw_parts returns precisely the values passed to from_raw_parts. + // - ValueArray::into_raw_parts returns precisely the values passed to from_raw_parts. // - those parts are passed to Vec::from_raw_parts here. unsafe { Vec::from_raw_parts(items as *mut _, len, cap) } } diff --git a/lib/src/uuidlist.rs b/lib/src/uuidlist.rs new file mode 100644 index 000000000..862ece88e --- /dev/null +++ b/lib/src/uuidlist.rs @@ -0,0 +1,70 @@ +use crate::traits::*; +use crate::uuid::TCUuid; + +/// TCUuidList represents a list of uuids. +/// +/// The content of this struct must be treated as read-only. +#[repr(C)] +pub struct TCUuidList { + /// number of uuids in items + len: libc::size_t, + + /// total size of items (internal use only) + _capacity: libc::size_t, + + /// array of uuids. these remain owned by the TCUuidList instance and will be freed by + /// tc_uuid_list_free. This pointer is never NULL for a valid TCUuidList. + items: *const TCUuid, +} + +impl ValueArray for TCUuidList { + type Element = TCUuid; + + unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { + TCUuidList { + len, + _capacity: cap, + items, + } + } + + fn into_raw_parts(self) -> (*const Self::Element, usize, usize) { + (self.items, self.len, self._capacity) + } +} + +/// Free a TCUuidList instance. The instance, and all TCUuids it contains, must not be used after +/// this call. +/// +/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCUuidList. +#[no_mangle] +pub unsafe extern "C" fn tc_uuid_list_free(tcuuids: *mut TCUuidList) { + debug_assert!(!tcuuids.is_null()); + // SAFETY: + // - *tcuuids is a valid TCUuidList (caller promises to treat it as read-only) + let uuids = unsafe { TCUuidList::take_from_arg(tcuuids, TCUuidList::null_value()) }; + TCUuidList::drop_vector(uuids); +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn empty_array_has_non_null_pointer() { + let tcuuids = TCUuidList::return_val(Vec::new()); + assert!(!tcuuids.items.is_null()); + assert_eq!(tcuuids.len, 0); + assert_eq!(tcuuids._capacity, 0); + } + + #[test] + fn free_sets_null_pointer() { + let mut tcuuids = TCUuidList::return_val(Vec::new()); + // SAFETY: testing expected behavior + unsafe { tc_uuid_list_free(&mut tcuuids) }; + assert!(tcuuids.items.is_null()); + assert_eq!(tcuuids.len, 0); + assert_eq!(tcuuids._capacity, 0); + } +} diff --git a/lib/taskchampion.h b/lib/taskchampion.h index e59a9c59d..ed8321fd7 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -148,6 +148,27 @@ typedef struct TCUuid { uint8_t bytes[16]; } TCUuid; +/** + * TCUuidList represents a list of uuids. + * + * The content of this struct must be treated as read-only. + */ +typedef struct TCUuidList { + /** + * number of uuids in items + */ + size_t len; + /** + * total size of items (internal use only) + */ + size_t _capacity; + /** + * array of uuids. these remain owned by the TCUuidList instance and will be freed by + * tc_uuid_list_free. This pointer is never NULL for a valid TCUuidList. + */ + const struct TCUuid *items; +} TCUuidList; + /** * TCStringList represents a list of strings. * @@ -193,6 +214,13 @@ struct TCReplica *tc_replica_new_on_disk(struct TCString *path, struct TCString */ struct TCTaskList tc_replica_all_tasks(struct TCReplica *rep); +/** + * Get a list of all uuids for tasks in the replica. + * + * Returns a TCUuidList with a NULL items field on error. + */ +struct TCUuidList tc_replica_all_task_uuids(struct TCReplica *rep); + /** * Get an existing task by its UUID. * @@ -498,6 +526,14 @@ struct TCString *tc_uuid_to_str(struct TCUuid tcuuid); */ TCResult tc_uuid_from_str(struct TCString *s, struct TCUuid *uuid_out); +/** + * Free a TCUuidList instance. The instance, and all TCUuids it contains, must not be used after + * this call. + * + * When this call returns, the `items` pointer will be NULL, signalling an invalid TCUuidList. + */ +void tc_uuid_list_free(struct TCUuidList *tcuuids); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus From 7a473d0eda76884974db6e28a07b69653ed9d2e3 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 10 Feb 2022 01:01:02 +0000 Subject: [PATCH 60/95] simplify imports --- lib/src/lib.rs | 12 ++++++++++++ lib/src/replica.rs | 5 +---- lib/src/stringlist.rs | 2 +- lib/src/task.rs | 5 +---- lib/src/tasklist.rs | 2 +- lib/src/uuid.rs | 2 +- lib/src/uuidlist.rs | 2 +- 7 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/src/lib.rs b/lib/src/lib.rs index bb4ebe905..b1456e73b 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -15,3 +15,15 @@ pub mod task; pub mod tasklist; pub mod uuid; pub mod uuidlist; + +pub(crate) mod types { + pub(crate) use crate::replica::TCReplica; + pub(crate) use crate::result::TCResult; + pub(crate) use crate::status::TCStatus; + pub(crate) use crate::string::TCString; + pub(crate) use crate::stringlist::TCStringList; + pub(crate) use crate::task::TCTask; + pub(crate) use crate::tasklist::TCTaskList; + pub(crate) use crate::uuid::TCUuid; + pub(crate) use crate::uuidlist::TCUuidList; +} diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 5119ea520..619dc2c46 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -1,9 +1,6 @@ use crate::traits::*; +use crate::types::*; use crate::util::err_to_tcstring; -use crate::{ - result::TCResult, status::TCStatus, string::TCString, task::TCTask, tasklist::TCTaskList, - uuid::TCUuid, uuidlist::TCUuidList, -}; use std::ptr::NonNull; use taskchampion::{Replica, StorageConfig}; diff --git a/lib/src/stringlist.rs b/lib/src/stringlist.rs index 3ee3be48e..b0d3ea8d4 100644 --- a/lib/src/stringlist.rs +++ b/lib/src/stringlist.rs @@ -1,5 +1,5 @@ -use crate::string::TCString; use crate::traits::*; +use crate::types::*; use std::ptr::NonNull; /// TCStringList represents a list of strings. diff --git a/lib/src/task.rs b/lib/src/task.rs index 50acf04c8..8f4055a8a 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -1,9 +1,6 @@ use crate::traits::*; +use crate::types::*; use crate::util::err_to_tcstring; -use crate::{ - replica::TCReplica, result::TCResult, status::TCStatus, string::TCString, - stringlist::TCStringList, uuid::TCUuid, -}; use chrono::{DateTime, TimeZone, Utc}; use std::convert::TryFrom; use std::ops::Deref; diff --git a/lib/src/tasklist.rs b/lib/src/tasklist.rs index c383139fe..a3cdf536a 100644 --- a/lib/src/tasklist.rs +++ b/lib/src/tasklist.rs @@ -1,5 +1,5 @@ -use crate::task::TCTask; use crate::traits::*; +use crate::types::*; use std::ptr::NonNull; /// TCTaskList represents a list of tasks. diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index 415e9501a..63be3823c 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -1,5 +1,5 @@ use crate::traits::*; -use crate::{result::TCResult, string::TCString}; +use crate::types::*; use libc; use taskchampion::Uuid; diff --git a/lib/src/uuidlist.rs b/lib/src/uuidlist.rs index 862ece88e..4b6684299 100644 --- a/lib/src/uuidlist.rs +++ b/lib/src/uuidlist.rs @@ -1,5 +1,5 @@ use crate::traits::*; -use crate::uuid::TCUuid; +use crate::types::*; /// TCUuidList represents a list of uuids. /// From 1c734851ae3183eced25773340015adfa14286f0 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 10 Feb 2022 01:10:40 +0000 Subject: [PATCH 61/95] safety notes for new types --- lib/src/stringlist.rs | 2 +- lib/src/tasklist.rs | 2 +- lib/src/traits.rs | 51 ++++++++++++++++++++++--------------------- lib/src/uuidlist.rs | 2 +- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/lib/src/stringlist.rs b/lib/src/stringlist.rs index b0d3ea8d4..c4fe8d0f7 100644 --- a/lib/src/stringlist.rs +++ b/lib/src/stringlist.rs @@ -19,7 +19,7 @@ pub struct TCStringList { items: *const NonNull>, } -impl ValueArray for TCStringList { +impl CArray for TCStringList { type Element = NonNull>; unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { diff --git a/lib/src/tasklist.rs b/lib/src/tasklist.rs index a3cdf536a..8905384aa 100644 --- a/lib/src/tasklist.rs +++ b/lib/src/tasklist.rs @@ -19,7 +19,7 @@ pub struct TCTaskList { items: *const NonNull, } -impl ValueArray for TCTaskList { +impl CArray for TCTaskList { type Element = NonNull; unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { diff --git a/lib/src/traits.rs b/lib/src/traits.rs index f0bde9e33..007d60ac3 100644 --- a/lib/src/traits.rs +++ b/lib/src/traits.rs @@ -23,8 +23,8 @@ pub(crate) trait PassByValue: Sized { /// /// # Safety /// - /// `self` must be a valid CType. This is typically ensured either by requiring that C - /// code not modify it, or by defining the valid values in C comments. + /// `self` must be a valid instance of Self. This is typically ensured either by requiring + /// that C code not modify it, or by defining the valid values in C comments. unsafe fn from_arg(arg: Self) -> Self::RustType { // SAFETY: // - arg is a valid CType (promised by caller) @@ -146,7 +146,13 @@ pub(crate) trait PassByPointer: Sized { } } -/// *mut P can be passed by value +/// *mut P can be passed by value. +/// +/// # Safety +/// +/// The 'static bound means that the pointer is treated as an "owning" pointer, +/// and must not be copied (leading to a double-free) or dropped without dropping +/// its contents (a leak). impl

PassByValue for *mut P where P: PassByPointer + 'static, @@ -154,9 +160,7 @@ where type RustType = &'static mut P; unsafe fn from_ctype(self) -> Self::RustType { - // SAFETY: - // - self must be a valid *mut P (promised by caller) - // - TODO for 'static + // SAFETY: self must be a valid *mut P (promised by caller) unsafe { &mut *self } } @@ -166,7 +170,7 @@ where } } -/// *const P can be passed by value +/// *const P can be passed by value. See the implementation for *mut P. impl

PassByValue for *const P where P: PassByPointer + 'static, @@ -174,9 +178,7 @@ where type RustType = &'static P; unsafe fn from_ctype(self) -> Self::RustType { - // SAFETY: - // - self must be a valid *mut P (promised by caller) - // - TODO for 'static + // SAFETY: self must be a valid *const P (promised by caller) unsafe { &*self } } @@ -186,7 +188,7 @@ where } } -/// NonNull

can be passed by value +/// NonNull

can be passed by value. See the implementation for NonNull

. impl

PassByValue for NonNull

where P: PassByPointer + 'static, @@ -194,9 +196,7 @@ where type RustType = &'static mut P; unsafe fn from_ctype(mut self) -> Self::RustType { - // SAFETY: - // - self must be a valid *mut P (promised by caller) - // - TODO for 'static + // SAFETY: self must be a valid NonNull

(promised by caller) unsafe { self.as_mut() } } @@ -206,14 +206,14 @@ where } } -/// Support for arrays of objects referenced by value. +/// Support for C arrays of objects referenced by value. /// /// The underlying C type should have three fields, containing items, length, and capacity. The /// required trait functions just fetch and set these fields. The PassByValue trait will be /// implemented automatically, converting between the C type and `Vec`. For most cases, /// it is only necessary to implement `tc_.._free` that first calls -/// `PassByValue::take_from_arg(arg, ValueArray::null_value())` to take the existing value and -/// replace it with the null value; then `ValueArray::drop_value_vector(..)` to drop the resulting +/// `PassByValue::take_from_arg(arg, CArray::null_value())` to take the existing value and +/// replace it with the null value; then `CArray::drop_value_vector(..)` to drop the resulting /// vector and all of the objects it points to. /// /// This can be used for objects referenced by pointer, too, with an Element type of `*const T` @@ -224,11 +224,10 @@ where /// in the `items` array. /// /// This class guarantees that the items pointer is non-NULL for any valid array (even when len=0). -// TODO: rename Array -pub(crate) trait ValueArray: Sized { +pub(crate) trait CArray: Sized { type Element: PassByValue; - /// Create a new ValueArray from the given items, len, and capacity. + /// Create a new CArray from the given items, len, and capacity. /// /// # Safety /// @@ -254,8 +253,10 @@ pub(crate) trait ValueArray: Sized { fn drop_vector(mut vec: Vec) { // first, drop each of the elements in turn for e in vec.drain(..) { - // SAFETY: e is a valid Element (caller promisd not to change it) - drop(unsafe { PassByValue::from_arg(e) }); + // SAFETY: + // - e is a valid Element (caller promisd not to change it) + // - Vec::drain has invalidated this entry (value is owned) + drop(unsafe { PassByValue::from_ctype(e) }); } // then drop the vector drop(vec); @@ -264,7 +265,7 @@ pub(crate) trait ValueArray: Sized { impl PassByValue for A where - A: ValueArray, + A: CArray, { type RustType = Vec; @@ -272,9 +273,9 @@ where let (items, len, cap) = self.into_raw_parts(); debug_assert!(!items.is_null()); // SAFETY: - // - ValueArray::from_raw_parts requires that items, len, and cap be valid for + // - CArray::from_raw_parts requires that items, len, and cap be valid for // Vec::from_raw_parts if not NULL, and they are not NULL (as promised by caller) - // - ValueArray::into_raw_parts returns precisely the values passed to from_raw_parts. + // - CArray::into_raw_parts returns precisely the values passed to from_raw_parts. // - those parts are passed to Vec::from_raw_parts here. unsafe { Vec::from_raw_parts(items as *mut _, len, cap) } } diff --git a/lib/src/uuidlist.rs b/lib/src/uuidlist.rs index 4b6684299..139bb8bd3 100644 --- a/lib/src/uuidlist.rs +++ b/lib/src/uuidlist.rs @@ -17,7 +17,7 @@ pub struct TCUuidList { items: *const TCUuid, } -impl ValueArray for TCUuidList { +impl CArray for TCUuidList { type Element = TCUuid; unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { From b01285d7807e966967b380c543a8f51ca0649c36 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 10 Feb 2022 01:18:50 +0000 Subject: [PATCH 62/95] add some simple replica functions --- .../src/bindings_tests/replica.c | 20 ++++++++++ lib/src/replica.rs | 37 +++++++++++++++++-- lib/taskchampion.h | 15 ++++++++ taskchampion/src/replica.rs | 2 +- 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/integration-tests/src/bindings_tests/replica.c b/integration-tests/src/bindings_tests/replica.c index 14084ab00..7e598695b 100644 --- a/integration-tests/src/bindings_tests/replica.c +++ b/integration-tests/src/bindings_tests/replica.c @@ -31,6 +31,24 @@ static void test_replica_undo_empty(void) { tc_replica_free(rep); } +// adding an undo point succeeds +static void test_replica_add_undo_point(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_add_undo_point(rep, true)); + TEST_ASSERT_NULL(tc_replica_error(rep)); + tc_replica_free(rep); +} + +// rebuilding working set succeeds +static void test_replica_rebuild_working_set(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_rebuild_working_set(rep, true)); + TEST_ASSERT_NULL(tc_replica_error(rep)); + tc_replica_free(rep); +} + // When tc_replica_undo is passed NULL for undone_out, it still succeeds static void test_replica_undo_empty_null_undone_out(void) { TCReplica *rep = tc_replica_new_in_memory(); @@ -190,6 +208,8 @@ int replica_tests(void) { RUN_TEST(test_replica_creation); RUN_TEST(test_replica_creation_disk); RUN_TEST(test_replica_undo_empty); + RUN_TEST(test_replica_add_undo_point); + RUN_TEST(test_replica_rebuild_working_set); RUN_TEST(test_replica_undo_empty_null_undone_out); RUN_TEST(test_replica_task_creation); RUN_TEST(test_replica_all_tasks); diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 619dc2c46..b946efb34 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -271,6 +271,40 @@ pub unsafe extern "C" fn tc_replica_undo<'a>( ) } +/// Add an UndoPoint, if one has not already been added by this Replica. This occurs automatically +/// when a change is made. The `force` flag allows forcing a new UndoPoint even if one has already +/// been created by this Replica, and may be useful when a Replica instance is held for a long time +/// and used to apply more than one user-visible change. +#[no_mangle] +pub unsafe extern "C" fn tc_replica_add_undo_point(rep: *mut TCReplica, force: bool) -> TCResult { + wrap( + rep, + |rep| { + rep.add_undo_point(force)?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + +/// Rebuild this replica's working set, based on whether tasks are pending or not. If `renumber` +/// is true, then existing tasks may be moved to new working-set indices; in any case, on +/// completion all pending tasks are in the working set and all non- pending tasks are not. +#[no_mangle] +pub unsafe extern "C" fn tc_replica_rebuild_working_set( + rep: *mut TCReplica, + renumber: bool, +) -> TCResult { + wrap( + rep, + |rep| { + rep.rebuild_working_set(renumber)?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + /// Get the latest error for a replica, or NULL if the last operation succeeded. Subsequent calls /// to this function will return NULL. The rep pointer must not be NULL. The caller must free the /// returned string. @@ -297,6 +331,3 @@ pub unsafe extern "C" fn tc_replica_free(rep: *mut TCReplica) { } drop(replica); } - -// TODO: tc_replica_rebuild_working_set -// TODO: tc_replica_add_undo_point diff --git a/lib/taskchampion.h b/lib/taskchampion.h index ed8321fd7..0e5753f67 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -253,6 +253,21 @@ struct TCTask *tc_replica_import_task_with_uuid(struct TCReplica *rep, struct TC */ TCResult tc_replica_undo(struct TCReplica *rep, int32_t *undone_out); +/** + * Add an UndoPoint, if one has not already been added by this Replica. This occurs automatically + * when a change is made. The `force` flag allows forcing a new UndoPoint even if one has already + * been created by this Replica, and may be useful when a Replica instance is held for a long time + * and used to apply more than one user-visible change. + */ +TCResult tc_replica_add_undo_point(struct TCReplica *rep, bool force); + +/** + * Rebuild this replica's working set, based on whether tasks are pending or not. If `renumber` + * is true, then existing tasks may be moved to new working-set indices; in any case, on + * completion all pending tasks are in the working set and all non- pending tasks are not. + */ +TCResult tc_replica_rebuild_working_set(struct TCReplica *rep, bool renumber); + /** * Get the latest error for a replica, or NULL if the last operation succeeded. Subsequent calls * to this function will return NULL. The rep pointer must not be NULL. The caller must free the diff --git a/taskchampion/src/replica.rs b/taskchampion/src/replica.rs index 2dd1e4887..79938423f 100644 --- a/taskchampion/src/replica.rs +++ b/taskchampion/src/replica.rs @@ -177,7 +177,7 @@ impl Replica { /// Add an UndoPoint, if one has not already been added by this Replica. This occurs /// automatically when a change is made. The `force` flag allows forcing a new UndoPoint - /// even if one has laready been created by this Replica, and may be useful when a Replica + /// even if one has already been created by this Replica, and may be useful when a Replica /// instance is held for a long time and used to apply more than one user-visible change. pub fn add_undo_point(&mut self, force: bool) -> anyhow::Result<()> { if force || !self.added_undo_point { From 7996a989089401c3573bddd9b64d8f996a2d88ca Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Fri, 11 Feb 2022 23:54:52 +0000 Subject: [PATCH 63/95] add annotation support --- integration-tests/src/bindings_tests/task.c | 54 +++++++++ lib/src/annotation.rs | 127 ++++++++++++++++++++ lib/src/lib.rs | 2 + lib/src/string.rs | 12 ++ lib/src/task.rs | 49 +++++++- lib/taskchampion.h | 67 +++++++++++ 6 files changed, 308 insertions(+), 3 deletions(-) create mode 100644 lib/src/annotation.rs diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 5a0214ce3..0eb1e45c0 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -333,6 +333,59 @@ static void test_task_get_tags(void) { tc_replica_free(rep); } +// annotation manipulation (add, remove, list, free) +static void test_task_annotations(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + TCAnnotationList anns = tc_task_get_annotations(task); + TEST_ASSERT_EQUAL(0, anns.len); + TEST_ASSERT_NOT_NULL(anns.items); + tc_annotation_list_free(&anns); + + tc_task_to_mut(task, rep); + + TCAnnotation ann; + + ann.entry = 1644623411; + ann.description = tc_string_borrow("ann1"); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_annotation(task, &ann)); + TEST_ASSERT_NULL(ann.description); + + ann.entry = 1644623422; + ann.description = tc_string_borrow("ann2"); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_annotation(task, &ann)); + TEST_ASSERT_NULL(ann.description); + + anns = tc_task_get_annotations(task); + + int found1 = false, found2 = false; + for (size_t i = 0; i < anns.len; i++) { + if (0 == strcmp("ann1", tc_string_content(anns.items[i].description))) { + TEST_ASSERT_EQUAL(anns.items[i].entry, 1644623411); + found1 = true; + } + if (0 == strcmp("ann2", tc_string_content(anns.items[i].description))) { + TEST_ASSERT_EQUAL(anns.items[i].entry, 1644623422); + found2 = true; + } + } + TEST_ASSERT_TRUE(found1); + TEST_ASSERT_TRUE(found2); + + tc_annotation_list_free(&anns); + TEST_ASSERT_NULL(anns.items); + + tc_task_free(task); + tc_replica_free(rep); +} + int task_tests(void) { UNITY_BEGIN(); // each test case above should be named here, in order. @@ -347,5 +400,6 @@ int task_tests(void) { RUN_TEST(test_task_done_and_delete); RUN_TEST(test_task_add_remove_has_tag); RUN_TEST(test_task_get_tags); + RUN_TEST(test_task_annotations); return UNITY_END(); } diff --git a/lib/src/annotation.rs b/lib/src/annotation.rs new file mode 100644 index 000000000..8c403327b --- /dev/null +++ b/lib/src/annotation.rs @@ -0,0 +1,127 @@ +use crate::traits::*; +use crate::types::*; +use chrono::prelude::*; +use taskchampion::Annotation; + +/// TCAnnotation contains the details of an annotation. +#[repr(C)] +pub struct TCAnnotation { + /// Time the annotation was made, as a UNIX epoch timestamp + pub entry: i64, + /// Content of the annotation + pub description: *mut TCString<'static>, +} + +impl PassByValue for TCAnnotation { + type RustType = Annotation; + + unsafe fn from_ctype(self) -> Annotation { + let entry = Utc.timestamp(self.entry, 0); + // SAFETY: + // - self is owned, so we can take ownership of this TCString + // - self.description was created from a String so has valid UTF-8 + // - caller did not change it (promised by caller) + let description = unsafe { TCString::take_from_arg(self.description) } + .into_string() + .unwrap(); + Annotation { entry, description } + } + + fn as_ctype(arg: Annotation) -> Self { + let description: TCString = arg.description.into(); + TCAnnotation { + entry: arg.entry.timestamp(), + // SAFETY: caller will later free this value via tc_annotation_free + description: unsafe { description.return_val() }, + } + } +} + +impl Default for TCAnnotation { + fn default() -> Self { + TCAnnotation { + entry: 0, + description: std::ptr::null_mut(), + } + } +} + +/// TCAnnotationList represents a list of annotations. +/// +/// The content of this struct must be treated as read-only. +#[repr(C)] +pub struct TCAnnotationList { + /// number of annotations in items + len: libc::size_t, + + /// total size of items (internal use only) + _capacity: libc::size_t, + + /// array of annotations. these remain owned by the TCAnnotationList instance and will be freed by + /// tc_annotation_list_free. This pointer is never NULL for a valid TCAnnotationList. + items: *const TCAnnotation, +} + +impl CArray for TCAnnotationList { + type Element = TCAnnotation; + + unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { + TCAnnotationList { + len, + _capacity: cap, + items, + } + } + + fn into_raw_parts(self) -> (*const Self::Element, usize, usize) { + (self.items, self.len, self._capacity) + } +} + +/// Free a TCAnnotation instance. The instance, and the TCString it contains, must not be used +/// after this call. +#[no_mangle] +pub unsafe extern "C" fn tc_annotation_free(tcann: *mut TCAnnotation) { + debug_assert!(!tcann.is_null()); + // SAFETY: + // - *tcann is a valid TCAnnotation (caller promises to treat it as read-only) + let annotation = unsafe { TCAnnotation::take_from_arg(tcann, TCAnnotation::default()) }; + drop(annotation); +} + +/// Free a TCAnnotationList instance. The instance, and all TCAnnotations it contains, must not be used after +/// this call. +/// +/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCAnnotationList. +#[no_mangle] +pub unsafe extern "C" fn tc_annotation_list_free(tcanns: *mut TCAnnotationList) { + debug_assert!(!tcanns.is_null()); + // SAFETY: + // - *tcanns is a valid TCAnnotationList (caller promises to treat it as read-only) + let annotations = + unsafe { TCAnnotationList::take_from_arg(tcanns, TCAnnotationList::null_value()) }; + TCAnnotationList::drop_vector(annotations); +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn empty_array_has_non_null_pointer() { + let tcanns = TCAnnotationList::return_val(Vec::new()); + assert!(!tcanns.items.is_null()); + assert_eq!(tcanns.len, 0); + assert_eq!(tcanns._capacity, 0); + } + + #[test] + fn free_sets_null_pointer() { + let mut tcanns = TCAnnotationList::return_val(Vec::new()); + // SAFETY: testing expected behavior + unsafe { tc_annotation_list_free(&mut tcanns) }; + assert!(tcanns.items.is_null()); + assert_eq!(tcanns.len, 0); + assert_eq!(tcanns._capacity, 0); + } +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index b1456e73b..f5c43439c 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -5,6 +5,7 @@ mod traits; mod util; +pub mod annotation; pub mod atomic; pub mod replica; pub mod result; @@ -17,6 +18,7 @@ pub mod uuid; pub mod uuidlist; pub(crate) mod types { + pub(crate) use crate::annotation::{TCAnnotation, TCAnnotationList}; pub(crate) use crate::replica::TCReplica; pub(crate) use crate::result::TCResult; pub(crate) use crate::status::TCStatus; diff --git a/lib/src/string.rs b/lib/src/string.rs index da35e1220..257d105eb 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -69,6 +69,18 @@ impl<'a> TCString<'a> { } } + /// Consume this TCString and return an equivalent String, or an error if not + /// valid UTF-8. In the error condition, the original data is lost. + pub(crate) fn into_string(self) -> Result { + match self { + TCString::CString(cstring) => cstring.into_string().map_err(|e| e.utf8_error()), + TCString::CStr(cstr) => cstr.to_str().map(|s| s.to_string()), + TCString::String(string) => Ok(string), + TCString::InvalidUtf8(e, _) => Err(e), + TCString::None => unreachable!(), + } + } + fn as_bytes(&self) -> &[u8] { match self { TCString::CString(cstring) => cstring.as_bytes(), diff --git a/lib/src/task.rs b/lib/src/task.rs index 8f4055a8a..2f7da32ef 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -305,7 +305,21 @@ pub unsafe extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCStringList }) } -// TODO: tc_task_get_annotations +/// Get the annotations for the task. +/// +/// The caller must free the returned TCAnnotationList instance. The TCStringList instance does not +/// reference the task and the two may be freed in any order. +#[no_mangle] +pub unsafe extern "C" fn tc_task_get_annotations<'a>(task: *mut TCTask) -> TCAnnotationList { + wrap(task, |task| { + let vec: Vec = task + .get_annotations() + .map(|a| TCAnnotation::as_ctype(a)) + .collect(); + TCAnnotationList::return_val(vec) + }) +} + // TODO: tc_task_get_uda // TODO: tc_task_get_udas // TODO: tc_task_get_legacy_uda @@ -471,8 +485,37 @@ pub unsafe extern "C" fn tc_task_remove_tag(task: *mut TCTask, tag: *mut TCStrin ) } -// TODO: tc_task_add_annotation -// TODO: tc_task_remove_annotation +/// Add an annotation to a mutable task. +#[no_mangle] +pub unsafe extern "C" fn tc_task_add_annotation( + task: *mut TCTask, + annotation: *mut TCAnnotation, +) -> TCResult { + // SAFETY: see TCAnnotation docstring + let ann = unsafe { TCAnnotation::take_from_arg(annotation, TCAnnotation::default()) }; + wrap_mut( + task, + |task| { + task.add_annotation(ann)?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + +/// Remove an annotation from a mutable task. +#[no_mangle] +pub unsafe extern "C" fn tc_task_remove_annotation(task: *mut TCTask, entry: i64) -> TCResult { + wrap_mut( + task, + |task| { + task.remove_annotation(Utc.timestamp(entry, 0))?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + // TODO: tc_task_set_uda // TODO: tc_task_remove_uda // TODO: tc_task_set_legacy_uda diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 0e5753f67..a6ffc7644 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -117,6 +117,41 @@ typedef struct TCString TCString; */ typedef struct TCTask TCTask; +/** + * TCAnnotation contains the details of an annotation. + */ +typedef struct TCAnnotation { + /** + * Time the annotation was made, as a UNIX epoch timestamp + */ + int64_t entry; + /** + * Content of the annotation + */ + struct TCString *description; +} TCAnnotation; + +/** + * TCAnnotationList represents a list of annotations. + * + * The content of this struct must be treated as read-only. + */ +typedef struct TCAnnotationList { + /** + * number of annotations in items + */ + size_t len; + /** + * total size of items (internal use only) + */ + size_t _capacity; + /** + * array of annotations. these remain owned by the TCAnnotationList instance and will be freed by + * tc_annotation_list_free. This pointer is never NULL for a valid TCAnnotationList. + */ + const struct TCAnnotation *items; +} TCAnnotationList; + /** * TCTaskList represents a list of tasks. * @@ -195,6 +230,20 @@ typedef struct TCStringList { extern "C" { #endif // __cplusplus +/** + * Free a TCAnnotation instance. The instance, and the TCString it contains, must not be used + * after this call. + */ +void tc_annotation_free(struct TCAnnotation *tcann); + +/** + * Free a TCAnnotationList instance. The instance, and all TCAnnotations it contains, must not be used after + * this call. + * + * When this call returns, the `items` pointer will be NULL, signalling an invalid TCAnnotationList. + */ +void tc_annotation_list_free(struct TCAnnotationList *tcanns); + /** * Create a new TCReplica with an in-memory database. The contents of the database will be * lost when it is freed. @@ -434,6 +483,14 @@ bool tc_task_has_tag(struct TCTask *task, struct TCString *tag); */ struct TCStringList tc_task_get_tags(struct TCTask *task); +/** + * Get the annotations for the task. + * + * The caller must free the returned TCAnnotationList instance. The TCStringList instance does not + * reference the task and the two may be freed in any order. + */ +struct TCAnnotationList tc_task_get_annotations(struct TCTask *task); + /** * Set a mutable task's status. */ @@ -490,6 +547,16 @@ TCResult tc_task_add_tag(struct TCTask *task, struct TCString *tag); */ TCResult tc_task_remove_tag(struct TCTask *task, struct TCString *tag); +/** + * Add an annotation to a mutable task. + */ +TCResult tc_task_add_annotation(struct TCTask *task, struct TCAnnotation *annotation); + +/** + * Remove an annotation from a mutable task. + */ +TCResult tc_task_remove_annotation(struct TCTask *task, int64_t entry); + /** * Get the latest error for a task, or NULL if the last operation succeeded. Subsequent calls * to this function will return NULL. The task pointer must not be NULL. The caller must free the From af51e0382aa1a9c77d84f0afedac2a751b495409 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Fri, 11 Feb 2022 23:59:22 +0000 Subject: [PATCH 64/95] implement lists in the same files as singular data --- lib/src/lib.rs | 12 ++----- lib/src/string.rs | 65 +++++++++++++++++++++++++++++++++++++ lib/src/stringlist.rs | 72 ----------------------------------------- lib/src/task.rs | 69 ++++++++++++++++++++++++++++++++++++++++ lib/src/tasklist.rs | 72 ----------------------------------------- lib/src/uuid.rs | 74 +++++++++++++++++++++++++++++++++++++++++-- lib/src/uuidlist.rs | 70 ---------------------------------------- 7 files changed, 208 insertions(+), 226 deletions(-) delete mode 100644 lib/src/stringlist.rs delete mode 100644 lib/src/tasklist.rs delete mode 100644 lib/src/uuidlist.rs diff --git a/lib/src/lib.rs b/lib/src/lib.rs index f5c43439c..41fa16005 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -11,21 +11,15 @@ pub mod replica; pub mod result; pub mod status; pub mod string; -pub mod stringlist; pub mod task; -pub mod tasklist; pub mod uuid; -pub mod uuidlist; pub(crate) mod types { pub(crate) use crate::annotation::{TCAnnotation, TCAnnotationList}; pub(crate) use crate::replica::TCReplica; pub(crate) use crate::result::TCResult; pub(crate) use crate::status::TCStatus; - pub(crate) use crate::string::TCString; - pub(crate) use crate::stringlist::TCStringList; - pub(crate) use crate::task::TCTask; - pub(crate) use crate::tasklist::TCTaskList; - pub(crate) use crate::uuid::TCUuid; - pub(crate) use crate::uuidlist::TCUuidList; + pub(crate) use crate::string::{TCString, TCStringList}; + pub(crate) use crate::task::{TCTask, TCTaskList}; + pub(crate) use crate::uuid::{TCUuid, TCUuidList}; } diff --git a/lib/src/string.rs b/lib/src/string.rs index 257d105eb..d9fc90507 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -2,6 +2,7 @@ use crate::traits::*; use std::ffi::{CStr, CString, OsStr}; use std::os::unix::ffi::OsStrExt; use std::path::PathBuf; +use std::ptr::NonNull; use std::str::Utf8Error; /// TCString supports passing strings into and out of the TaskChampion API. @@ -136,6 +137,39 @@ impl<'a> From<&str> for TCString<'static> { } } +/// TCStringList represents a list of strings. +/// +/// The content of this struct must be treated as read-only. +#[repr(C)] +pub struct TCStringList { + /// number of strings in items + len: libc::size_t, + + /// total size of items (internal use only) + _capacity: libc::size_t, + + /// TCStringList representing each string. these remain owned by the TCStringList instance and will + /// be freed by tc_string_list_free. This pointer is never NULL for a valid TCStringList, and the + /// *TCStringList at indexes 0..len-1 are not NULL. + items: *const NonNull>, +} + +impl CArray for TCStringList { + type Element = NonNull>; + + unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { + TCStringList { + len, + _capacity: cap, + items, + } + } + + fn into_raw_parts(self) -> (*const Self::Element, usize, usize) { + (self.items, self.len, self._capacity) + } +} + /// Create a new TCString referencing the given C string. The C string must remain valid and /// unchanged until after the TCString is freed. It's typically easiest to ensure this by using a /// static string. @@ -281,11 +315,42 @@ pub unsafe extern "C" fn tc_string_free(tcstring: *mut TCString) { drop(unsafe { TCString::take_from_arg(tcstring) }); } +/// Free a TCStringList instance. The instance, and all TCStringList it contains, must not be used after +/// this call. +/// +/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCStringList. +#[no_mangle] +pub unsafe extern "C" fn tc_string_list_free(tcstrings: *mut TCStringList) { + debug_assert!(!tcstrings.is_null()); + // SAFETY: + // - *tcstrings is a valid TCStringList (caller promises to treat it as read-only) + let strings = unsafe { TCStringList::take_from_arg(tcstrings, TCStringList::null_value()) }; + TCStringList::drop_vector(strings); +} + #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; + #[test] + fn empty_array_has_non_null_pointer() { + let tcstrings = TCStringList::return_val(Vec::new()); + assert!(!tcstrings.items.is_null()); + assert_eq!(tcstrings.len, 0); + assert_eq!(tcstrings._capacity, 0); + } + + #[test] + fn free_sets_null_pointer() { + let mut tcstrings = TCStringList::return_val(Vec::new()); + // SAFETY: testing expected behavior + unsafe { tc_string_list_free(&mut tcstrings) }; + assert!(tcstrings.items.is_null()); + assert_eq!(tcstrings.len, 0); + assert_eq!(tcstrings._capacity, 0); + } + const INVALID_UTF8: &[u8] = b"abc\xf0\x28\x8c\x28"; fn make_cstring() -> TCString<'static> { diff --git a/lib/src/stringlist.rs b/lib/src/stringlist.rs deleted file mode 100644 index c4fe8d0f7..000000000 --- a/lib/src/stringlist.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::traits::*; -use crate::types::*; -use std::ptr::NonNull; - -/// TCStringList represents a list of strings. -/// -/// The content of this struct must be treated as read-only. -#[repr(C)] -pub struct TCStringList { - /// number of strings in items - len: libc::size_t, - - /// total size of items (internal use only) - _capacity: libc::size_t, - - /// TCStringList representing each string. these remain owned by the TCStringList instance and will - /// be freed by tc_string_list_free. This pointer is never NULL for a valid TCStringList, and the - /// *TCStringList at indexes 0..len-1 are not NULL. - items: *const NonNull>, -} - -impl CArray for TCStringList { - type Element = NonNull>; - - unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { - TCStringList { - len, - _capacity: cap, - items, - } - } - - fn into_raw_parts(self) -> (*const Self::Element, usize, usize) { - (self.items, self.len, self._capacity) - } -} - -/// Free a TCStringList instance. The instance, and all TCStringList it contains, must not be used after -/// this call. -/// -/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCStringList. -#[no_mangle] -pub unsafe extern "C" fn tc_string_list_free(tcstrings: *mut TCStringList) { - debug_assert!(!tcstrings.is_null()); - // SAFETY: - // - *tcstrings is a valid TCStringList (caller promises to treat it as read-only) - let strings = unsafe { TCStringList::take_from_arg(tcstrings, TCStringList::null_value()) }; - TCStringList::drop_vector(strings); -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn empty_array_has_non_null_pointer() { - let tcstrings = TCStringList::return_val(Vec::new()); - assert!(!tcstrings.items.is_null()); - assert_eq!(tcstrings.len, 0); - assert_eq!(tcstrings._capacity, 0); - } - - #[test] - fn free_sets_null_pointer() { - let mut tcstrings = TCStringList::return_val(Vec::new()); - // SAFETY: testing expected behavior - unsafe { tc_string_list_free(&mut tcstrings) }; - assert!(tcstrings.items.is_null()); - assert_eq!(tcstrings.len, 0); - assert_eq!(tcstrings._capacity, 0); - } -} diff --git a/lib/src/task.rs b/lib/src/task.rs index 2f7da32ef..b56a71498 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -171,6 +171,39 @@ fn to_datetime(time: libc::time_t) -> Option> { } } +/// TCTaskList represents a list of tasks. +/// +/// The content of this struct must be treated as read-only. +#[repr(C)] +pub struct TCTaskList { + /// number of tasks in items + len: libc::size_t, + + /// total size of items (internal use only) + _capacity: libc::size_t, + + /// array of pointers representing each task. these remain owned by the TCTaskList instance and + /// will be freed by tc_task_list_free. This pointer is never NULL for a valid TCTaskList, + /// and the *TCTaskList at indexes 0..len-1 are not NULL. + items: *const NonNull, +} + +impl CArray for TCTaskList { + type Element = NonNull; + + unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { + TCTaskList { + len, + _capacity: cap, + items, + } + } + + fn into_raw_parts(self) -> (*const Self::Element, usize, usize) { + (self.items, self.len, self._capacity) + } +} + /// Convert an immutable task into a mutable task. /// /// The task must not be NULL. It is modified in-place, and becomes mutable. @@ -553,3 +586,39 @@ pub unsafe extern "C" fn tc_task_free<'a>(task: *mut TCTask) { drop(tctask); } + +/// Free a TCTaskList instance. The instance, and all TCTaskList it contains, must not be used after +/// this call. +/// +/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCTaskList. +#[no_mangle] +pub unsafe extern "C" fn tc_task_list_free(tctasks: *mut TCTaskList) { + debug_assert!(!tctasks.is_null()); + // SAFETY: + // - *tctasks is a valid TCTaskList (caller promises to treat it as read-only) + let tasks = unsafe { TCTaskList::take_from_arg(tctasks, TCTaskList::null_value()) }; + TCTaskList::drop_vector(tasks); +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn empty_array_has_non_null_pointer() { + let tctasks = TCTaskList::return_val(Vec::new()); + assert!(!tctasks.items.is_null()); + assert_eq!(tctasks.len, 0); + assert_eq!(tctasks._capacity, 0); + } + + #[test] + fn free_sets_null_pointer() { + let mut tctasks = TCTaskList::return_val(Vec::new()); + // SAFETY: testing expected behavior + unsafe { tc_task_list_free(&mut tctasks) }; + assert!(tctasks.items.is_null()); + assert_eq!(tctasks.len, 0); + assert_eq!(tctasks._capacity, 0); + } +} diff --git a/lib/src/tasklist.rs b/lib/src/tasklist.rs deleted file mode 100644 index 8905384aa..000000000 --- a/lib/src/tasklist.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::traits::*; -use crate::types::*; -use std::ptr::NonNull; - -/// TCTaskList represents a list of tasks. -/// -/// The content of this struct must be treated as read-only. -#[repr(C)] -pub struct TCTaskList { - /// number of tasks in items - len: libc::size_t, - - /// total size of items (internal use only) - _capacity: libc::size_t, - - /// array of pointers representing each task. these remain owned by the TCTaskList instance and - /// will be freed by tc_task_list_free. This pointer is never NULL for a valid TCTaskList, - /// and the *TCTaskList at indexes 0..len-1 are not NULL. - items: *const NonNull, -} - -impl CArray for TCTaskList { - type Element = NonNull; - - unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { - TCTaskList { - len, - _capacity: cap, - items, - } - } - - fn into_raw_parts(self) -> (*const Self::Element, usize, usize) { - (self.items, self.len, self._capacity) - } -} - -/// Free a TCTaskList instance. The instance, and all TCTaskList it contains, must not be used after -/// this call. -/// -/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCTaskList. -#[no_mangle] -pub unsafe extern "C" fn tc_task_list_free(tctasks: *mut TCTaskList) { - debug_assert!(!tctasks.is_null()); - // SAFETY: - // - *tctasks is a valid TCTaskList (caller promises to treat it as read-only) - let tasks = unsafe { TCTaskList::take_from_arg(tctasks, TCTaskList::null_value()) }; - TCTaskList::drop_vector(tasks); -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn empty_array_has_non_null_pointer() { - let tctasks = TCTaskList::return_val(Vec::new()); - assert!(!tctasks.items.is_null()); - assert_eq!(tctasks.len, 0); - assert_eq!(tctasks._capacity, 0); - } - - #[test] - fn free_sets_null_pointer() { - let mut tctasks = TCTaskList::return_val(Vec::new()); - // SAFETY: testing expected behavior - unsafe { tc_task_list_free(&mut tctasks) }; - assert!(tctasks.items.is_null()); - assert_eq!(tctasks.len, 0); - assert_eq!(tctasks._capacity, 0); - } -} diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index 63be3823c..d140a9eed 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -3,6 +3,10 @@ use crate::types::*; use libc; use taskchampion::Uuid; +// NOTE: this must be a simple constant so that cbindgen can evaluate it +/// Length, in bytes, of the string representation of a UUID (without NUL terminator) +pub const TC_UUID_STRING_BYTES: usize = 36; + /// TCUuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. /// Uuids are typically treated as opaque, but the bytes are available in big-endian format. /// @@ -36,9 +40,37 @@ pub unsafe extern "C" fn tc_uuid_nil() -> TCUuid { TCUuid::return_val(Uuid::nil()) } -// NOTE: this must be a simple constant so that cbindgen can evaluate it -/// Length, in bytes, of the string representation of a UUID (without NUL terminator) -pub const TC_UUID_STRING_BYTES: usize = 36; +/// TCUuidList represents a list of uuids. +/// +/// The content of this struct must be treated as read-only. +#[repr(C)] +pub struct TCUuidList { + /// number of uuids in items + len: libc::size_t, + + /// total size of items (internal use only) + _capacity: libc::size_t, + + /// array of uuids. these remain owned by the TCUuidList instance and will be freed by + /// tc_uuid_list_free. This pointer is never NULL for a valid TCUuidList. + items: *const TCUuid, +} + +impl CArray for TCUuidList { + type Element = TCUuid; + + unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { + TCUuidList { + len, + _capacity: cap, + items, + } + } + + fn into_raw_parts(self) -> (*const Self::Element, usize, usize) { + (self.items, self.len, self._capacity) + } +} /// Write the string representation of a TCUuid into the given buffer, which must be /// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. @@ -91,3 +123,39 @@ pub unsafe extern "C" fn tc_uuid_from_str<'a>(s: *mut TCString, uuid_out: *mut T } TCResult::Error } + +/// Free a TCUuidList instance. The instance, and all TCUuids it contains, must not be used after +/// this call. +/// +/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCUuidList. +#[no_mangle] +pub unsafe extern "C" fn tc_uuid_list_free(tcuuids: *mut TCUuidList) { + debug_assert!(!tcuuids.is_null()); + // SAFETY: + // - *tcuuids is a valid TCUuidList (caller promises to treat it as read-only) + let uuids = unsafe { TCUuidList::take_from_arg(tcuuids, TCUuidList::null_value()) }; + TCUuidList::drop_vector(uuids); +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn empty_array_has_non_null_pointer() { + let tcuuids = TCUuidList::return_val(Vec::new()); + assert!(!tcuuids.items.is_null()); + assert_eq!(tcuuids.len, 0); + assert_eq!(tcuuids._capacity, 0); + } + + #[test] + fn free_sets_null_pointer() { + let mut tcuuids = TCUuidList::return_val(Vec::new()); + // SAFETY: testing expected behavior + unsafe { tc_uuid_list_free(&mut tcuuids) }; + assert!(tcuuids.items.is_null()); + assert_eq!(tcuuids.len, 0); + assert_eq!(tcuuids._capacity, 0); + } +} diff --git a/lib/src/uuidlist.rs b/lib/src/uuidlist.rs deleted file mode 100644 index 139bb8bd3..000000000 --- a/lib/src/uuidlist.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::traits::*; -use crate::types::*; - -/// TCUuidList represents a list of uuids. -/// -/// The content of this struct must be treated as read-only. -#[repr(C)] -pub struct TCUuidList { - /// number of uuids in items - len: libc::size_t, - - /// total size of items (internal use only) - _capacity: libc::size_t, - - /// array of uuids. these remain owned by the TCUuidList instance and will be freed by - /// tc_uuid_list_free. This pointer is never NULL for a valid TCUuidList. - items: *const TCUuid, -} - -impl CArray for TCUuidList { - type Element = TCUuid; - - unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { - TCUuidList { - len, - _capacity: cap, - items, - } - } - - fn into_raw_parts(self) -> (*const Self::Element, usize, usize) { - (self.items, self.len, self._capacity) - } -} - -/// Free a TCUuidList instance. The instance, and all TCUuids it contains, must not be used after -/// this call. -/// -/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCUuidList. -#[no_mangle] -pub unsafe extern "C" fn tc_uuid_list_free(tcuuids: *mut TCUuidList) { - debug_assert!(!tcuuids.is_null()); - // SAFETY: - // - *tcuuids is a valid TCUuidList (caller promises to treat it as read-only) - let uuids = unsafe { TCUuidList::take_from_arg(tcuuids, TCUuidList::null_value()) }; - TCUuidList::drop_vector(uuids); -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn empty_array_has_non_null_pointer() { - let tcuuids = TCUuidList::return_val(Vec::new()); - assert!(!tcuuids.items.is_null()); - assert_eq!(tcuuids.len, 0); - assert_eq!(tcuuids._capacity, 0); - } - - #[test] - fn free_sets_null_pointer() { - let mut tcuuids = TCUuidList::return_val(Vec::new()); - // SAFETY: testing expected behavior - unsafe { tc_uuid_list_free(&mut tcuuids) }; - assert!(tcuuids.items.is_null()); - assert_eq!(tcuuids.len, 0); - assert_eq!(tcuuids._capacity, 0); - } -} From 7ebdaa761cd32b0f0d49a2bb16493fa78ef2d5a0 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sat, 12 Feb 2022 00:18:01 +0000 Subject: [PATCH 65/95] treat libc::time_t as a PassByValue --- lib/src/annotation.rs | 20 +++++++++++--------- lib/src/atomic.rs | 19 +++++++++++++++++++ lib/src/task.rs | 34 +++++++++++----------------------- lib/taskchampion.h | 6 +++--- 4 files changed, 44 insertions(+), 35 deletions(-) diff --git a/lib/src/annotation.rs b/lib/src/annotation.rs index 8c403327b..8dfeb1b73 100644 --- a/lib/src/annotation.rs +++ b/lib/src/annotation.rs @@ -1,14 +1,13 @@ use crate::traits::*; use crate::types::*; -use chrono::prelude::*; use taskchampion::Annotation; /// TCAnnotation contains the details of an annotation. #[repr(C)] pub struct TCAnnotation { - /// Time the annotation was made, as a UNIX epoch timestamp - pub entry: i64, - /// Content of the annotation + /// Time the annotation was made. Must be nonzero. + pub entry: libc::time_t, + /// Content of the annotation. Must not be NULL. pub description: *mut TCString<'static>, } @@ -16,13 +15,16 @@ impl PassByValue for TCAnnotation { type RustType = Annotation; unsafe fn from_ctype(self) -> Annotation { - let entry = Utc.timestamp(self.entry, 0); + // SAFETY: + // - any time_t value is valid + // - time_t is not zero, so unwrap is safe (see type docstring) + let entry = unsafe { self.entry.from_ctype() }.unwrap(); // SAFETY: // - self is owned, so we can take ownership of this TCString - // - self.description was created from a String so has valid UTF-8 - // - caller did not change it (promised by caller) + // - self.description is a valid, non-null TCString (see type docstring) let description = unsafe { TCString::take_from_arg(self.description) } .into_string() + // TODO: might not be valid utf-8 .unwrap(); Annotation { entry, description } } @@ -30,7 +32,7 @@ impl PassByValue for TCAnnotation { fn as_ctype(arg: Annotation) -> Self { let description: TCString = arg.description.into(); TCAnnotation { - entry: arg.entry.timestamp(), + entry: libc::time_t::as_ctype(Some(arg.entry)), // SAFETY: caller will later free this value via tc_annotation_free description: unsafe { description.return_val() }, } @@ -40,7 +42,7 @@ impl PassByValue for TCAnnotation { impl Default for TCAnnotation { fn default() -> Self { TCAnnotation { - entry: 0, + entry: 0 as libc::time_t, description: std::ptr::null_mut(), } } diff --git a/lib/src/atomic.rs b/lib/src/atomic.rs index db8315922..8a0d9e3d1 100644 --- a/lib/src/atomic.rs +++ b/lib/src/atomic.rs @@ -1,6 +1,7 @@ //! Trait implementations for a few atomic types use crate::traits::*; +use chrono::prelude::*; impl PassByValue for usize { type RustType = usize; @@ -13,3 +14,21 @@ impl PassByValue for usize { arg } } + +/// Convert an Option> to a libc::time_t, or zero if not set. +impl PassByValue for libc::time_t { + type RustType = Option>; + + unsafe fn from_ctype(self) -> Option> { + if self == 0 { + None + } else { + Some(Utc.timestamp(self as i64, 0)) + } + } + + fn as_ctype(arg: Option>) -> libc::time_t { + arg.map(|ts| ts.timestamp() as libc::time_t) + .unwrap_or(0 as libc::time_t) + } +} diff --git a/lib/src/task.rs b/lib/src/task.rs index b56a71498..717f5de78 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -1,7 +1,7 @@ use crate::traits::*; use crate::types::*; use crate::util::err_to_tcstring; -use chrono::{DateTime, TimeZone, Utc}; +use chrono::{TimeZone, Utc}; use std::convert::TryFrom; use std::ops::Deref; use std::ptr::NonNull; @@ -155,22 +155,6 @@ impl TryFrom> for Tag { } } -/// Convert a DateTime to a libc::time_t, or zero if not set. -fn to_time_t(timestamp: Option>) -> libc::time_t { - timestamp - .map(|ts| ts.timestamp() as libc::time_t) - .unwrap_or(0 as libc::time_t) -} - -/// Convert a libc::time_t to Option>, treating time zero as None -fn to_datetime(time: libc::time_t) -> Option> { - if time == 0 { - None - } else { - Some(Utc.timestamp(time as i64, 0)) - } -} - /// TCTaskList represents a list of tasks. /// /// The content of this struct must be treated as read-only. @@ -275,19 +259,19 @@ pub unsafe extern "C" fn tc_task_get_description<'a>(task: *mut TCTask) -> *mut /// Get the entry timestamp for a task (when it was created), or 0 if not set. #[no_mangle] pub unsafe extern "C" fn tc_task_get_entry<'a>(task: *mut TCTask) -> libc::time_t { - wrap(task, |task| to_time_t(task.get_entry())) + wrap(task, |task| libc::time_t::as_ctype(task.get_entry())) } /// Get the wait timestamp for a task, or 0 if not set. #[no_mangle] pub unsafe extern "C" fn tc_task_get_wait<'a>(task: *mut TCTask) -> libc::time_t { - wrap(task, |task| to_time_t(task.get_wait())) + wrap(task, |task| libc::time_t::as_ctype(task.get_wait())) } /// Get the modified timestamp for a task, or 0 if not set. #[no_mangle] pub unsafe extern "C" fn tc_task_get_modified<'a>(task: *mut TCTask) -> libc::time_t { - wrap(task, |task| to_time_t(task.get_modified())) + wrap(task, |task| libc::time_t::as_ctype(task.get_modified())) } /// Check if a task is waiting. @@ -396,7 +380,8 @@ pub unsafe extern "C" fn tc_task_set_entry(task: *mut TCTask, entry: libc::time_ wrap_mut( task, |task| { - task.set_entry(to_datetime(entry))?; + // SAFETY: any time_t value is a valid timestamp + task.set_entry(unsafe { entry.from_ctype() })?; Ok(TCResult::Ok) }, TCResult::Error, @@ -409,7 +394,8 @@ pub unsafe extern "C" fn tc_task_set_wait(task: *mut TCTask, wait: libc::time_t) wrap_mut( task, |task| { - task.set_wait(to_datetime(wait))?; + // SAFETY: any time_t value is a valid timestamp + task.set_wait(unsafe { wait.from_ctype() })?; Ok(TCResult::Ok) }, TCResult::Error, @@ -426,7 +412,9 @@ pub unsafe extern "C" fn tc_task_set_modified( task, |task| { task.set_modified( - to_datetime(modified).ok_or_else(|| anyhow::anyhow!("modified cannot be zero"))?, + // SAFETY: any time_t value is a valid timestamp + unsafe { modified.from_ctype() } + .ok_or_else(|| anyhow::anyhow!("modified cannot be zero"))?, )?; Ok(TCResult::Ok) }, diff --git a/lib/taskchampion.h b/lib/taskchampion.h index a6ffc7644..4b993d8ff 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -122,11 +122,11 @@ typedef struct TCTask TCTask; */ typedef struct TCAnnotation { /** - * Time the annotation was made, as a UNIX epoch timestamp + * Time the annotation was made. Must be nonzero. */ - int64_t entry; + time_t entry; /** - * Content of the annotation + * Content of the annotation. Must not be NULL. */ struct TCString *description; } TCAnnotation; From 76cbc2880b6cef3781d6a2e67027d644306280cd Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sat, 12 Feb 2022 00:26:57 +0000 Subject: [PATCH 66/95] refactor annotations to handle invalid strings --- lib/src/annotation.rs | 22 ++++++++++------------ lib/src/task.rs | 13 +++++++++---- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/lib/src/annotation.rs b/lib/src/annotation.rs index 8dfeb1b73..167afc498 100644 --- a/lib/src/annotation.rs +++ b/lib/src/annotation.rs @@ -1,6 +1,6 @@ use crate::traits::*; use crate::types::*; -use taskchampion::Annotation; +use chrono::prelude::*; /// TCAnnotation contains the details of an annotation. #[repr(C)] @@ -12,9 +12,11 @@ pub struct TCAnnotation { } impl PassByValue for TCAnnotation { - type RustType = Annotation; + // NOTE: we cannot use `RustType = Annotation` here because conversion of the + // TCString to a String can fail. + type RustType = (DateTime, TCString<'static>); - unsafe fn from_ctype(self) -> Annotation { + unsafe fn from_ctype(self) -> Self::RustType { // SAFETY: // - any time_t value is valid // - time_t is not zero, so unwrap is safe (see type docstring) @@ -22,18 +24,14 @@ impl PassByValue for TCAnnotation { // SAFETY: // - self is owned, so we can take ownership of this TCString // - self.description is a valid, non-null TCString (see type docstring) - let description = unsafe { TCString::take_from_arg(self.description) } - .into_string() - // TODO: might not be valid utf-8 - .unwrap(); - Annotation { entry, description } + let description = unsafe { TCString::take_from_arg(self.description) }; + (entry, description) } - fn as_ctype(arg: Annotation) -> Self { - let description: TCString = arg.description.into(); + fn as_ctype((entry, description): Self::RustType) -> Self { TCAnnotation { - entry: libc::time_t::as_ctype(Some(arg.entry)), - // SAFETY: caller will later free this value via tc_annotation_free + entry: libc::time_t::as_ctype(Some(entry)), + // SAFETY: caller assumes ownership of this value description: unsafe { description.return_val() }, } } diff --git a/lib/src/task.rs b/lib/src/task.rs index 717f5de78..03b86d806 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -6,7 +6,7 @@ use std::convert::TryFrom; use std::ops::Deref; use std::ptr::NonNull; use std::str::FromStr; -use taskchampion::{Tag, Task, TaskMut}; +use taskchampion::{Annotation, Tag, Task, TaskMut}; /// A task, as publicly exposed by this library. /// @@ -331,7 +331,10 @@ pub unsafe extern "C" fn tc_task_get_annotations<'a>(task: *mut TCTask) -> TCAnn wrap(task, |task| { let vec: Vec = task .get_annotations() - .map(|a| TCAnnotation::as_ctype(a)) + .map(|a| { + let description = TCString::from(a.description); + TCAnnotation::as_ctype((a.entry, description)) + }) .collect(); TCAnnotationList::return_val(vec) }) @@ -513,11 +516,13 @@ pub unsafe extern "C" fn tc_task_add_annotation( annotation: *mut TCAnnotation, ) -> TCResult { // SAFETY: see TCAnnotation docstring - let ann = unsafe { TCAnnotation::take_from_arg(annotation, TCAnnotation::default()) }; + let (entry, description) = + unsafe { TCAnnotation::take_from_arg(annotation, TCAnnotation::default()) }; wrap_mut( task, |task| { - task.add_annotation(ann)?; + let description = description.into_string()?; + task.add_annotation(Annotation { entry, description })?; Ok(TCResult::Ok) }, TCResult::Error, From e9cd6adc5bc307e0bb5d93a0061fdccd94354b67 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sat, 12 Feb 2022 01:15:32 +0000 Subject: [PATCH 67/95] fix memory leak, remove blanket pointer-by-value impls --- lib/src/annotation.rs | 9 ++- lib/src/string.rs | 8 +-- lib/src/task.rs | 8 +-- lib/src/traits.rs | 140 ++++++++++++++++++++---------------------- lib/src/uuid.rs | 8 +-- 5 files changed, 81 insertions(+), 92 deletions(-) diff --git a/lib/src/annotation.rs b/lib/src/annotation.rs index 167afc498..22a81653d 100644 --- a/lib/src/annotation.rs +++ b/lib/src/annotation.rs @@ -95,12 +95,11 @@ pub unsafe extern "C" fn tc_annotation_free(tcann: *mut TCAnnotation) { /// When this call returns, the `items` pointer will be NULL, signalling an invalid TCAnnotationList. #[no_mangle] pub unsafe extern "C" fn tc_annotation_list_free(tcanns: *mut TCAnnotationList) { - debug_assert!(!tcanns.is_null()); // SAFETY: - // - *tcanns is a valid TCAnnotationList (caller promises to treat it as read-only) - let annotations = - unsafe { TCAnnotationList::take_from_arg(tcanns, TCAnnotationList::null_value()) }; - TCAnnotationList::drop_vector(annotations); + // - tcanns is not NULL and points to a valid TCAnnotationList (caller is not allowed to + // modify the list) + // - caller promises not to use the value after return + unsafe { drop_value_array(tcanns) } } #[cfg(test)] diff --git a/lib/src/string.rs b/lib/src/string.rs index d9fc90507..c02bdb1e6 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -321,11 +321,11 @@ pub unsafe extern "C" fn tc_string_free(tcstring: *mut TCString) { /// When this call returns, the `items` pointer will be NULL, signalling an invalid TCStringList. #[no_mangle] pub unsafe extern "C" fn tc_string_list_free(tcstrings: *mut TCStringList) { - debug_assert!(!tcstrings.is_null()); // SAFETY: - // - *tcstrings is a valid TCStringList (caller promises to treat it as read-only) - let strings = unsafe { TCStringList::take_from_arg(tcstrings, TCStringList::null_value()) }; - TCStringList::drop_vector(strings); + // - tcstrings is not NULL and points to a valid TCStringList (caller is not allowed to + // modify the list) + // - caller promises not to use the value after return + unsafe { drop_pointer_array(tcstrings) }; } #[cfg(test)] diff --git a/lib/src/task.rs b/lib/src/task.rs index 03b86d806..87a64493d 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -586,11 +586,11 @@ pub unsafe extern "C" fn tc_task_free<'a>(task: *mut TCTask) { /// When this call returns, the `items` pointer will be NULL, signalling an invalid TCTaskList. #[no_mangle] pub unsafe extern "C" fn tc_task_list_free(tctasks: *mut TCTaskList) { - debug_assert!(!tctasks.is_null()); // SAFETY: - // - *tctasks is a valid TCTaskList (caller promises to treat it as read-only) - let tasks = unsafe { TCTaskList::take_from_arg(tctasks, TCTaskList::null_value()) }; - TCTaskList::drop_vector(tasks); + // - tctasks is not NULL and points to a valid TCTaskList (caller is not allowed to + // modify the list) + // - caller promises not to use the value after return + unsafe { drop_pointer_array(tctasks) }; } #[cfg(test)] diff --git a/lib/src/traits.rs b/lib/src/traits.rs index 007d60ac3..fea6ddbca 100644 --- a/lib/src/traits.rs +++ b/lib/src/traits.rs @@ -6,6 +6,8 @@ use std::ptr::NonNull; /// /// The Rust and C types may differ, with from_ctype and as_ctype converting between them. /// Implement this trait for the C type. +/// +/// The RustType must be droppable (not containing raw pointers). pub(crate) trait PassByValue: Sized { type RustType; @@ -146,66 +148,6 @@ pub(crate) trait PassByPointer: Sized { } } -/// *mut P can be passed by value. -/// -/// # Safety -/// -/// The 'static bound means that the pointer is treated as an "owning" pointer, -/// and must not be copied (leading to a double-free) or dropped without dropping -/// its contents (a leak). -impl

PassByValue for *mut P -where - P: PassByPointer + 'static, -{ - type RustType = &'static mut P; - - unsafe fn from_ctype(self) -> Self::RustType { - // SAFETY: self must be a valid *mut P (promised by caller) - unsafe { &mut *self } - } - - /// Convert a Rust value to a C value. - fn as_ctype(arg: Self::RustType) -> Self { - arg - } -} - -/// *const P can be passed by value. See the implementation for *mut P. -impl

PassByValue for *const P -where - P: PassByPointer + 'static, -{ - type RustType = &'static P; - - unsafe fn from_ctype(self) -> Self::RustType { - // SAFETY: self must be a valid *const P (promised by caller) - unsafe { &*self } - } - - /// Convert a Rust value to a C value. - fn as_ctype(arg: Self::RustType) -> Self { - arg - } -} - -/// NonNull

can be passed by value. See the implementation for NonNull

. -impl

PassByValue for NonNull

-where - P: PassByPointer + 'static, -{ - type RustType = &'static mut P; - - unsafe fn from_ctype(mut self) -> Self::RustType { - // SAFETY: self must be a valid NonNull

(promised by caller) - unsafe { self.as_mut() } - } - - /// Convert a Rust value to a C value. - fn as_ctype(arg: Self::RustType) -> Self { - NonNull::new(arg).expect("value must not be NULL") - } -} - /// Support for C arrays of objects referenced by value. /// /// The underlying C type should have three fields, containing items, length, and capacity. The @@ -213,10 +155,10 @@ where /// implemented automatically, converting between the C type and `Vec`. For most cases, /// it is only necessary to implement `tc_.._free` that first calls /// `PassByValue::take_from_arg(arg, CArray::null_value())` to take the existing value and -/// replace it with the null value; then `CArray::drop_value_vector(..)` to drop the resulting +/// replace it with the null value; then one of hte `drop_.._array(..)` functions to drop the resulting /// vector and all of the objects it points to. /// -/// This can be used for objects referenced by pointer, too, with an Element type of `*const T` +/// This can be used for objects referenced by pointer, too, with an Element type of `NonNull` /// /// # Safety /// @@ -225,7 +167,7 @@ where /// /// This class guarantees that the items pointer is non-NULL for any valid array (even when len=0). pub(crate) trait CArray: Sized { - type Element: PassByValue; + type Element; /// Create a new CArray from the given items, len, and capacity. /// @@ -247,20 +189,68 @@ pub(crate) trait CArray: Sized { // - satisfies the first case in from_raw_parts' safety documentation unsafe { Self::from_raw_parts(std::ptr::null(), 0, 0) } } +} - /// Drop a vector of elements. This is a convenience function for implementing - /// tc_.._free functions. - fn drop_vector(mut vec: Vec) { - // first, drop each of the elements in turn - for e in vec.drain(..) { - // SAFETY: - // - e is a valid Element (caller promisd not to change it) - // - Vec::drain has invalidated this entry (value is owned) - drop(unsafe { PassByValue::from_ctype(e) }); - } - // then drop the vector - drop(vec); +/// Given a CArray containing pass-by-value values, drop all of the values and +/// the array. +/// +/// This is a convenience function for `tc_.._list_free` functions. +/// +/// # Safety +/// +/// - Array must be non-NULL and point to a valid CA instance +/// - The caller must not use the value array points to after this function, as +/// it has been freed. It will be replaced with the null value. +pub(crate) unsafe fn drop_value_array(array: *mut CA) +where + CA: CArray, + T: PassByValue, +{ + debug_assert!(!array.is_null()); + + // SAFETY: + // - *array is a valid CA (caller promises to treat it as read-only) + let mut vec = unsafe { CA::take_from_arg(array, CA::null_value()) }; + + // first, drop each of the elements in turn + for e in vec.drain(..) { + // SAFETY: + // - e is a valid Element (caller promisd not to change it) + // - Vec::drain has invalidated this entry (value is owned) + drop(unsafe { PassByValue::from_arg(e) }); } + // then drop the vector + drop(vec); +} + +/// Given a CArray containing NonNull pointers, drop all of the pointed-to values and the array. +/// +/// This is a convenience function for `tc_.._list_free` functions. +/// +/// # Safety +/// +/// - Array must be non-NULL and point to a valid CA instance +/// - The caller must not use the value array points to after this function, as +/// it has been freed. It will be replaced with the null value. +pub(crate) unsafe fn drop_pointer_array(array: *mut CA) +where + CA: CArray>, + T: PassByPointer, +{ + debug_assert!(!array.is_null()); + // SAFETY: + // - *array is a valid CA (caller promises to treat it as read-only) + let mut vec = unsafe { CA::take_from_arg(array, CA::null_value()) }; + + // first, drop each of the elements in turn + for e in vec.drain(..) { + // SAFETY: + // - e is a valid Element (caller promised not to change it) + // - Vec::drain has invalidated this entry (value is owned) + drop(unsafe { PassByPointer::take_from_arg(e.as_ptr()) }); + } + // then drop the vector + drop(vec); } impl PassByValue for A diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index d140a9eed..56311152b 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -130,11 +130,11 @@ pub unsafe extern "C" fn tc_uuid_from_str<'a>(s: *mut TCString, uuid_out: *mut T /// When this call returns, the `items` pointer will be NULL, signalling an invalid TCUuidList. #[no_mangle] pub unsafe extern "C" fn tc_uuid_list_free(tcuuids: *mut TCUuidList) { - debug_assert!(!tcuuids.is_null()); // SAFETY: - // - *tcuuids is a valid TCUuidList (caller promises to treat it as read-only) - let uuids = unsafe { TCUuidList::take_from_arg(tcuuids, TCUuidList::null_value()) }; - TCUuidList::drop_vector(uuids); + // - tcuuids is not NULL and points to a valid TCUuidList (caller is not allowed to + // modify the list) + // - caller promises not to use the value after return + unsafe { drop_value_array(tcuuids) }; } #[cfg(test)] From f81c4eec90d0d9e1a159047acc70eb0726ea7f69 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sat, 12 Feb 2022 15:20:46 +0000 Subject: [PATCH 68/95] rename array to list in rust types --- lib/src/annotation.rs | 6 +++--- lib/src/replica.rs | 2 +- lib/src/string.rs | 6 +++--- lib/src/task.rs | 6 +++--- lib/src/traits.rs | 48 +++++++++++++++++++++---------------------- lib/src/uuid.rs | 6 +++--- 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/lib/src/annotation.rs b/lib/src/annotation.rs index 22a81653d..558f0d8c4 100644 --- a/lib/src/annotation.rs +++ b/lib/src/annotation.rs @@ -62,7 +62,7 @@ pub struct TCAnnotationList { items: *const TCAnnotation, } -impl CArray for TCAnnotationList { +impl CList for TCAnnotationList { type Element = TCAnnotation; unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { @@ -99,7 +99,7 @@ pub unsafe extern "C" fn tc_annotation_list_free(tcanns: *mut TCAnnotationList) // - tcanns is not NULL and points to a valid TCAnnotationList (caller is not allowed to // modify the list) // - caller promises not to use the value after return - unsafe { drop_value_array(tcanns) } + unsafe { drop_value_list(tcanns) } } #[cfg(test)] @@ -107,7 +107,7 @@ mod test { use super::*; #[test] - fn empty_array_has_non_null_pointer() { + fn empty_list_has_non_null_pointer() { let tcanns = TCAnnotationList::return_val(Vec::new()); assert!(!tcanns.items.is_null()); assert_eq!(tcanns.len, 0); diff --git a/lib/src/replica.rs b/lib/src/replica.rs index b946efb34..99f07639d 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -139,7 +139,7 @@ pub unsafe extern "C" fn tc_replica_all_tasks(rep: *mut TCReplica) -> TCTaskList rep, |rep| { // note that the Replica API returns a hashmap here, but we discard - // the keys and return a simple array. The task UUIDs are available + // the keys and return a simple list. The task UUIDs are available // from task.get_uuid(), so information is not lost. let tasks: Vec<_> = rep .all_tasks()? diff --git a/lib/src/string.rs b/lib/src/string.rs index c02bdb1e6..af40714d9 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -154,7 +154,7 @@ pub struct TCStringList { items: *const NonNull>, } -impl CArray for TCStringList { +impl CList for TCStringList { type Element = NonNull>; unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { @@ -325,7 +325,7 @@ pub unsafe extern "C" fn tc_string_list_free(tcstrings: *mut TCStringList) { // - tcstrings is not NULL and points to a valid TCStringList (caller is not allowed to // modify the list) // - caller promises not to use the value after return - unsafe { drop_pointer_array(tcstrings) }; + unsafe { drop_pointer_list(tcstrings) }; } #[cfg(test)] @@ -334,7 +334,7 @@ mod test { use pretty_assertions::assert_eq; #[test] - fn empty_array_has_non_null_pointer() { + fn empty_list_has_non_null_pointer() { let tcstrings = TCStringList::return_val(Vec::new()); assert!(!tcstrings.items.is_null()); assert_eq!(tcstrings.len, 0); diff --git a/lib/src/task.rs b/lib/src/task.rs index 87a64493d..390386e93 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -172,7 +172,7 @@ pub struct TCTaskList { items: *const NonNull, } -impl CArray for TCTaskList { +impl CList for TCTaskList { type Element = NonNull; unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { @@ -590,7 +590,7 @@ pub unsafe extern "C" fn tc_task_list_free(tctasks: *mut TCTaskList) { // - tctasks is not NULL and points to a valid TCTaskList (caller is not allowed to // modify the list) // - caller promises not to use the value after return - unsafe { drop_pointer_array(tctasks) }; + unsafe { drop_pointer_list(tctasks) }; } #[cfg(test)] @@ -598,7 +598,7 @@ mod test { use super::*; #[test] - fn empty_array_has_non_null_pointer() { + fn empty_list_has_non_null_pointer() { let tctasks = TCTaskList::return_val(Vec::new()); assert!(!tctasks.items.is_null()); assert_eq!(tctasks.len, 0); diff --git a/lib/src/traits.rs b/lib/src/traits.rs index fea6ddbca..6a4ad2735 100644 --- a/lib/src/traits.rs +++ b/lib/src/traits.rs @@ -148,14 +148,14 @@ pub(crate) trait PassByPointer: Sized { } } -/// Support for C arrays of objects referenced by value. +/// Support for C lists of objects referenced by value. /// /// The underlying C type should have three fields, containing items, length, and capacity. The /// required trait functions just fetch and set these fields. The PassByValue trait will be /// implemented automatically, converting between the C type and `Vec`. For most cases, /// it is only necessary to implement `tc_.._free` that first calls -/// `PassByValue::take_from_arg(arg, CArray::null_value())` to take the existing value and -/// replace it with the null value; then one of hte `drop_.._array(..)` functions to drop the resulting +/// `PassByValue::take_from_arg(arg, CList::null_value())` to take the existing value and +/// replace it with the null value; then one of hte `drop_.._list(..)` functions to drop the resulting /// vector and all of the objects it points to. /// /// This can be used for objects referenced by pointer, too, with an Element type of `NonNull` @@ -165,11 +165,11 @@ pub(crate) trait PassByPointer: Sized { /// The C type must be documented as read-only. None of the fields may be modified, nor anything /// in the `items` array. /// -/// This class guarantees that the items pointer is non-NULL for any valid array (even when len=0). -pub(crate) trait CArray: Sized { +/// This class guarantees that the items pointer is non-NULL for any valid list (even when len=0). +pub(crate) trait CList: Sized { type Element; - /// Create a new CArray from the given items, len, and capacity. + /// Create a new CList from the given items, len, and capacity. /// /// # Safety /// @@ -191,26 +191,26 @@ pub(crate) trait CArray: Sized { } } -/// Given a CArray containing pass-by-value values, drop all of the values and -/// the array. +/// Given a CList containing pass-by-value values, drop all of the values and +/// the list. /// /// This is a convenience function for `tc_.._list_free` functions. /// /// # Safety /// -/// - Array must be non-NULL and point to a valid CA instance +/// - List must be non-NULL and point to a valid CL instance /// - The caller must not use the value array points to after this function, as /// it has been freed. It will be replaced with the null value. -pub(crate) unsafe fn drop_value_array(array: *mut CA) +pub(crate) unsafe fn drop_value_list(list: *mut CL) where - CA: CArray, + CL: CList, T: PassByValue, { - debug_assert!(!array.is_null()); + debug_assert!(!list.is_null()); // SAFETY: - // - *array is a valid CA (caller promises to treat it as read-only) - let mut vec = unsafe { CA::take_from_arg(array, CA::null_value()) }; + // - *list is a valid CL (caller promises to treat it as read-only) + let mut vec = unsafe { CL::take_from_arg(list, CL::null_value()) }; // first, drop each of the elements in turn for e in vec.drain(..) { @@ -223,24 +223,24 @@ where drop(vec); } -/// Given a CArray containing NonNull pointers, drop all of the pointed-to values and the array. +/// Given a CList containing NonNull pointers, drop all of the pointed-to values and the list. /// /// This is a convenience function for `tc_.._list_free` functions. /// /// # Safety /// -/// - Array must be non-NULL and point to a valid CA instance +/// - List must be non-NULL and point to a valid CL instance /// - The caller must not use the value array points to after this function, as /// it has been freed. It will be replaced with the null value. -pub(crate) unsafe fn drop_pointer_array(array: *mut CA) +pub(crate) unsafe fn drop_pointer_list(list: *mut CL) where - CA: CArray>, + CL: CList>, T: PassByPointer, { - debug_assert!(!array.is_null()); + debug_assert!(!list.is_null()); // SAFETY: - // - *array is a valid CA (caller promises to treat it as read-only) - let mut vec = unsafe { CA::take_from_arg(array, CA::null_value()) }; + // - *list is a valid CL (caller promises to treat it as read-only) + let mut vec = unsafe { CL::take_from_arg(list, CL::null_value()) }; // first, drop each of the elements in turn for e in vec.drain(..) { @@ -255,7 +255,7 @@ where impl PassByValue for A where - A: CArray, + A: CList, { type RustType = Vec; @@ -263,9 +263,9 @@ where let (items, len, cap) = self.into_raw_parts(); debug_assert!(!items.is_null()); // SAFETY: - // - CArray::from_raw_parts requires that items, len, and cap be valid for + // - CList::from_raw_parts requires that items, len, and cap be valid for // Vec::from_raw_parts if not NULL, and they are not NULL (as promised by caller) - // - CArray::into_raw_parts returns precisely the values passed to from_raw_parts. + // - CList::into_raw_parts returns precisely the values passed to from_raw_parts. // - those parts are passed to Vec::from_raw_parts here. unsafe { Vec::from_raw_parts(items as *mut _, len, cap) } } diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index 56311152b..8a26e4d11 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -56,7 +56,7 @@ pub struct TCUuidList { items: *const TCUuid, } -impl CArray for TCUuidList { +impl CList for TCUuidList { type Element = TCUuid; unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { @@ -134,7 +134,7 @@ pub unsafe extern "C" fn tc_uuid_list_free(tcuuids: *mut TCUuidList) { // - tcuuids is not NULL and points to a valid TCUuidList (caller is not allowed to // modify the list) // - caller promises not to use the value after return - unsafe { drop_value_array(tcuuids) }; + unsafe { drop_value_list(tcuuids) }; } #[cfg(test)] @@ -142,7 +142,7 @@ mod test { use super::*; #[test] - fn empty_array_has_non_null_pointer() { + fn empty_list_has_non_null_pointer() { let tcuuids = TCUuidList::return_val(Vec::new()); assert!(!tcuuids.items.is_null()); assert_eq!(tcuuids.len, 0); From ad560fdb79f9a9cb42cec088ac5b881c03eda106 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sat, 12 Feb 2022 16:22:45 +0000 Subject: [PATCH 69/95] add UDA support --- integration-tests/src/bindings_tests/task.c | 135 +++++++++++++++ lib/src/lib.rs | 2 + lib/src/task.rs | 178 +++++++++++++++++++- lib/src/uda.rs | 148 ++++++++++++++++ lib/taskchampion.h | 105 ++++++++++++ 5 files changed, 560 insertions(+), 8 deletions(-) create mode 100644 lib/src/uda.rs diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 0eb1e45c0..5dab6c510 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -386,6 +386,140 @@ static void test_task_annotations(void) { tc_replica_free(rep); } +// UDA manipulation (add, remove, list, free) +static void test_task_udas(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + tc_task_to_mut(task, rep); + + TCString *value; + TCUDAList udas; + + TEST_ASSERT_NULL(tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1"))); + TEST_ASSERT_NULL(tc_task_get_legacy_uda(task, tc_string_borrow("leg1"))); + + udas = tc_task_get_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(0, udas.len); + tc_uda_list_free(&udas); + + udas = tc_task_get_legacy_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(0, udas.len); + tc_uda_list_free(&udas); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, + tc_task_set_uda(task, + tc_string_borrow("ns"), + tc_string_borrow("u1"), + tc_string_borrow("vvv"))); + + value = tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1")); + TEST_ASSERT_NOT_NULL(value); + TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(value)); + tc_string_free(value); + TEST_ASSERT_NULL(tc_task_get_legacy_uda(task, tc_string_borrow("leg1"))); + + udas = tc_task_get_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(1, udas.len); + TEST_ASSERT_EQUAL_STRING("ns", tc_string_content(udas.items[0].ns)); + TEST_ASSERT_EQUAL_STRING("u1", tc_string_content(udas.items[0].key)); + TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(udas.items[0].value)); + tc_uda_list_free(&udas); + + udas = tc_task_get_legacy_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(1, udas.len); + TEST_ASSERT_NULL(udas.items[0].ns); + TEST_ASSERT_EQUAL_STRING("ns.u1", tc_string_content(udas.items[0].key)); + TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(udas.items[0].value)); + tc_uda_list_free(&udas); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, + tc_task_set_legacy_uda(task, + tc_string_borrow("leg1"), + tc_string_borrow("legv"))); + + value = tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1")); + TEST_ASSERT_NOT_NULL(value); + TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(value)); + tc_string_free(value); + + value = tc_task_get_legacy_uda(task, tc_string_borrow("leg1")); + TEST_ASSERT_NOT_NULL(value); + TEST_ASSERT_EQUAL_STRING("legv", tc_string_content(value)); + tc_string_free(value); + + udas = tc_task_get_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(2, udas.len); + tc_uda_list_free(&udas); + + udas = tc_task_get_legacy_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(2, udas.len); + tc_uda_list_free(&udas); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, + tc_task_remove_uda(task, + tc_string_borrow("ns"), + tc_string_borrow("u1"))); + + TEST_ASSERT_NULL(tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1"))); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, + tc_task_remove_uda(task, + tc_string_borrow("ns"), + tc_string_borrow("u1"))); + + udas = tc_task_get_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(1, udas.len); + TEST_ASSERT_EQUAL_STRING("", tc_string_content(udas.items[0].ns)); + TEST_ASSERT_EQUAL_STRING("leg1", tc_string_content(udas.items[0].key)); + TEST_ASSERT_EQUAL_STRING("legv", tc_string_content(udas.items[0].value)); + tc_uda_list_free(&udas); + + udas = tc_task_get_legacy_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(1, udas.len); + TEST_ASSERT_NULL(udas.items[0].ns); + TEST_ASSERT_EQUAL_STRING("leg1", tc_string_content(udas.items[0].key)); + TEST_ASSERT_EQUAL_STRING("legv", tc_string_content(udas.items[0].value)); + tc_uda_list_free(&udas); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, + tc_task_remove_legacy_uda(task, + tc_string_borrow("leg1"))); + + TEST_ASSERT_NULL(tc_task_get_legacy_uda(task, tc_string_borrow("leg1"))); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, + tc_task_remove_legacy_uda(task, + tc_string_borrow("leg1"))); + + udas = tc_task_get_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(0, udas.len); + tc_uda_list_free(&udas); + + udas = tc_task_get_legacy_udas(task); + TEST_ASSERT_NOT_NULL(udas.items); + TEST_ASSERT_EQUAL(0, udas.len); + tc_uda_list_free(&udas); + + tc_task_free(task); + tc_replica_free(rep); +} + int task_tests(void) { UNITY_BEGIN(); // each test case above should be named here, in order. @@ -401,5 +535,6 @@ int task_tests(void) { RUN_TEST(test_task_add_remove_has_tag); RUN_TEST(test_task_get_tags); RUN_TEST(test_task_annotations); + RUN_TEST(test_task_udas); return UNITY_END(); } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 41fa16005..5bc7629a3 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -12,6 +12,7 @@ pub mod result; pub mod status; pub mod string; pub mod task; +pub mod uda; pub mod uuid; pub(crate) mod types { @@ -21,5 +22,6 @@ pub(crate) mod types { pub(crate) use crate::status::TCStatus; pub(crate) use crate::string::{TCString, TCStringList}; pub(crate) use crate::task::{TCTask, TCTaskList}; + pub(crate) use crate::uda::{TCUDAList, TCUDA, UDA}; pub(crate) use crate::uuid::{TCUuid, TCUuidList}; } diff --git a/lib/src/task.rs b/lib/src/task.rs index 390386e93..a20b2049f 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -340,10 +340,89 @@ pub unsafe extern "C" fn tc_task_get_annotations<'a>(task: *mut TCTask) -> TCAnn }) } -// TODO: tc_task_get_uda -// TODO: tc_task_get_udas -// TODO: tc_task_get_legacy_uda -// TODO: tc_task_get_legacy_udas +/// Get the named UDA from the task. +/// +/// Returns NULL if the UDA does not exist. +#[no_mangle] +pub unsafe extern "C" fn tc_task_get_uda<'a>( + task: *mut TCTask, + ns: *mut TCString<'a>, + key: *mut TCString<'a>, +) -> *mut TCString<'static> { + wrap(task, |task| { + if let Ok(ns) = unsafe { TCString::take_from_arg(ns) }.as_str() { + if let Ok(key) = unsafe { TCString::take_from_arg(key) }.as_str() { + if let Some(value) = task.get_uda(ns, key) { + // SAFETY: + // - caller will free this string (caller promises) + return unsafe { TCString::return_val(value.into()) }; + } + } + } + std::ptr::null_mut() + }) +} + +/// Get the named legacy UDA from the task. +/// +/// Returns NULL if the UDA does not exist. +#[no_mangle] +pub unsafe extern "C" fn tc_task_get_legacy_uda<'a>( + task: *mut TCTask, + key: *mut TCString<'a>, +) -> *mut TCString<'static> { + wrap(task, |task| { + if let Ok(key) = unsafe { TCString::take_from_arg(key) }.as_str() { + if let Some(value) = task.get_legacy_uda(key) { + // SAFETY: + // - caller will free this string (caller promises) + return unsafe { TCString::return_val(value.into()) }; + } + } + std::ptr::null_mut() + }) +} + +/// Get all UDAs for this task. +/// +/// Legacy UDAs are represented with an empty string in the ns field. +#[no_mangle] +pub unsafe extern "C" fn tc_task_get_udas<'a>(task: *mut TCTask) -> TCUDAList { + wrap(task, |task| { + let vec: Vec = task + .get_udas() + .map(|((ns, key), value)| { + TCUDA::return_val(UDA { + ns: Some(ns.into()), + key: key.into(), + value: value.into(), + }) + }) + .collect(); + TCUDAList::return_val(vec) + }) +} + +/// Get all UDAs for this task. +/// +/// All TCUDAs in this list have a NULL ns field. The entire UDA key is +/// included in the key field. +#[no_mangle] +pub unsafe extern "C" fn tc_task_get_legacy_udas<'a>(task: *mut TCTask) -> TCUDAList { + wrap(task, |task| { + let vec: Vec = task + .get_legacy_udas() + .map(|(key, value)| { + TCUDA::return_val(UDA { + ns: None, + key: key.into(), + value: value.into(), + }) + }) + .collect(); + TCUDAList::return_val(vec) + }) +} /// Set a mutable task's status. #[no_mangle] @@ -542,10 +621,93 @@ pub unsafe extern "C" fn tc_task_remove_annotation(task: *mut TCTask, entry: i64 ) } -// TODO: tc_task_set_uda -// TODO: tc_task_remove_uda -// TODO: tc_task_set_legacy_uda -// TODO: tc_task_remove_legacy_uda +/// Set a UDA on a mutable task. +#[no_mangle] +pub unsafe extern "C" fn tc_task_set_uda<'a>( + task: *mut TCTask, + ns: *mut TCString, + key: *mut TCString, + value: *mut TCString, +) -> TCResult { + // SAFETY: see TCString docstring + let ns = unsafe { TCString::take_from_arg(ns) }; + // SAFETY: see TCString docstring + let key = unsafe { TCString::take_from_arg(key) }; + // SAFETY: see TCString docstring + let value = unsafe { TCString::take_from_arg(value) }; + wrap_mut( + task, + |task| { + task.set_uda( + ns.as_str()?.to_string(), + key.as_str()?.to_string(), + value.as_str()?.to_string(), + )?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + +/// Remove a UDA fraom a mutable task. +#[no_mangle] +pub unsafe extern "C" fn tc_task_remove_uda<'a>( + task: *mut TCTask, + ns: *mut TCString, + key: *mut TCString, +) -> TCResult { + // SAFETY: see TCString docstring + let ns = unsafe { TCString::take_from_arg(ns) }; + // SAFETY: see TCString docstring + let key = unsafe { TCString::take_from_arg(key) }; + wrap_mut( + task, + |task| { + task.remove_uda(ns.as_str()?.to_string(), key.as_str()?.to_string())?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + +/// Set a legacy UDA on a mutable task. +#[no_mangle] +pub unsafe extern "C" fn tc_task_set_legacy_uda<'a>( + task: *mut TCTask, + key: *mut TCString, + value: *mut TCString, +) -> TCResult { + // SAFETY: see TCString docstring + let key = unsafe { TCString::take_from_arg(key) }; + // SAFETY: see TCString docstring + let value = unsafe { TCString::take_from_arg(value) }; + wrap_mut( + task, + |task| { + task.set_legacy_uda(key.as_str()?.to_string(), value.as_str()?.to_string())?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + +/// Remove a UDA fraom a mutable task. +#[no_mangle] +pub unsafe extern "C" fn tc_task_remove_legacy_uda<'a>( + task: *mut TCTask, + key: *mut TCString, +) -> TCResult { + // SAFETY: see TCString docstring + let key = unsafe { TCString::take_from_arg(key) }; + wrap_mut( + task, + |task| { + task.remove_legacy_uda(key.as_str()?.to_string())?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} /// Get the latest error for a task, or NULL if the last operation succeeded. Subsequent calls /// to this function will return NULL. The task pointer must not be NULL. The caller must free the diff --git a/lib/src/uda.rs b/lib/src/uda.rs new file mode 100644 index 000000000..27134d80e --- /dev/null +++ b/lib/src/uda.rs @@ -0,0 +1,148 @@ +use crate::traits::*; +use crate::types::*; + +/// TCUDA contains the details of a UDA. +#[repr(C)] +pub struct TCUDA { + /// Namespace of the UDA. For legacy UDAs, this is NULL. + pub ns: *mut TCString<'static>, + /// UDA key. Must not be NULL. + pub key: *mut TCString<'static>, + /// Content of the UDA. Must not be NULL. + pub value: *mut TCString<'static>, +} + +pub(crate) struct UDA { + pub ns: Option>, + pub key: TCString<'static>, + pub value: TCString<'static>, +} + +impl PassByValue for TCUDA { + type RustType = UDA; + + unsafe fn from_ctype(self) -> Self::RustType { + UDA { + ns: if self.ns.is_null() { + None + } else { + // SAFETY: + // - self is owned, so we can take ownership of this TCString + // - self.ns is a valid, non-null TCString (NULL just checked) + Some(unsafe { TCString::take_from_arg(self.ns) }) + }, + // SAFETY: + // - self is owned, so we can take ownership of this TCString + // - self.key is a valid, non-null TCString (see type docstring) + key: unsafe { TCString::take_from_arg(self.key) }, + // SAFETY: + // - self is owned, so we can take ownership of this TCString + // - self.value is a valid, non-null TCString (see type docstring) + value: unsafe { TCString::take_from_arg(self.value) }, + } + } + + fn as_ctype(uda: UDA) -> Self { + TCUDA { + // SAFETY: caller assumes ownership of this value + ns: if let Some(ns) = uda.ns { + unsafe { ns.return_val() } + } else { + std::ptr::null_mut() + }, + // SAFETY: caller assumes ownership of this value + key: unsafe { uda.key.return_val() }, + // SAFETY: caller assumes ownership of this value + value: unsafe { uda.value.return_val() }, + } + } +} + +impl Default for TCUDA { + fn default() -> Self { + TCUDA { + ns: std::ptr::null_mut(), + key: std::ptr::null_mut(), + value: std::ptr::null_mut(), + } + } +} + +/// TCUDAList represents a list of UDAs. +/// +/// The content of this struct must be treated as read-only. +#[repr(C)] +pub struct TCUDAList { + /// number of UDAs in items + len: libc::size_t, + + /// total size of items (internal use only) + _capacity: libc::size_t, + + /// array of UDAs. These remain owned by the TCUDAList instance and will be freed by + /// tc_uda_list_free. This pointer is never NULL for a valid TCUDAList. + items: *const TCUDA, +} + +impl CList for TCUDAList { + type Element = TCUDA; + + unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { + TCUDAList { + len, + _capacity: cap, + items, + } + } + + fn into_raw_parts(self) -> (*const Self::Element, usize, usize) { + (self.items, self.len, self._capacity) + } +} + +/// Free a TCUDA instance. The instance, and the TCStrings it contains, must not be used +/// after this call. +#[no_mangle] +pub unsafe extern "C" fn tc_uda_free(tcuda: *mut TCUDA) { + debug_assert!(!tcuda.is_null()); + // SAFETY: + // - *tcuda is a valid TCUDA (caller promises to treat it as read-only) + let uda = unsafe { TCUDA::take_from_arg(tcuda, TCUDA::default()) }; + drop(uda); +} + +/// Free a TCUDAList instance. The instance, and all TCUDAs it contains, must not be used after +/// this call. +/// +/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCUDAList. +#[no_mangle] +pub unsafe extern "C" fn tc_uda_list_free(tcudas: *mut TCUDAList) { + // SAFETY: + // - tcudas is not NULL and points to a valid TCUDAList (caller is not allowed to + // modify the list) + // - caller promises not to use the value after return + unsafe { drop_value_list(tcudas) } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn empty_list_has_non_null_pointer() { + let tcudas = TCUDAList::return_val(Vec::new()); + assert!(!tcudas.items.is_null()); + assert_eq!(tcudas.len, 0); + assert_eq!(tcudas._capacity, 0); + } + + #[test] + fn free_sets_null_pointer() { + let mut tcudas = TCUDAList::return_val(Vec::new()); + // SAFETY: testing expected behavior + unsafe { tc_uda_list_free(&mut tcudas) }; + assert!(tcudas.items.is_null()); + assert_eq!(tcudas.len, 0); + assert_eq!(tcudas._capacity, 0); + } +} diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 4b993d8ff..bc61b1e40 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -226,6 +226,45 @@ typedef struct TCStringList { struct TCString *const *items; } TCStringList; +/** + * TCUDA contains the details of a UDA. + */ +typedef struct TCUDA { + /** + * Namespace of the UDA. For legacy UDAs, this is NULL. + */ + struct TCString *ns; + /** + * UDA key. Must not be NULL. + */ + struct TCString *key; + /** + * Content of the UDA. Must not be NULL. + */ + struct TCString *value; +} TCUDA; + +/** + * TCUDAList represents a list of UDAs. + * + * The content of this struct must be treated as read-only. + */ +typedef struct TCUDAList { + /** + * number of UDAs in items + */ + size_t len; + /** + * total size of items (internal use only) + */ + size_t _capacity; + /** + * array of UDAs. These remain owned by the TCUDAList instance and will be freed by + * tc_uda_list_free. This pointer is never NULL for a valid TCUDAList. + */ + const struct TCUDA *items; +} TCUDAList; + #ifdef __cplusplus extern "C" { #endif // __cplusplus @@ -491,6 +530,35 @@ struct TCStringList tc_task_get_tags(struct TCTask *task); */ struct TCAnnotationList tc_task_get_annotations(struct TCTask *task); +/** + * Get the named UDA from the task. + * + * Returns NULL if the UDA does not exist. + */ +struct TCString *tc_task_get_uda(struct TCTask *task, struct TCString *ns, struct TCString *key); + +/** + * Get the named legacy UDA from the task. + * + * Returns NULL if the UDA does not exist. + */ +struct TCString *tc_task_get_legacy_uda(struct TCTask *task, struct TCString *key); + +/** + * Get all UDAs for this task. + * + * Legacy UDAs are represented with an empty string in the ns field. + */ +struct TCUDAList tc_task_get_udas(struct TCTask *task); + +/** + * Get all UDAs for this task. + * + * All TCUDAs in this list have a NULL ns field. The entire UDA key is + * included in the key field. + */ +struct TCUDAList tc_task_get_legacy_udas(struct TCTask *task); + /** * Set a mutable task's status. */ @@ -557,6 +625,29 @@ TCResult tc_task_add_annotation(struct TCTask *task, struct TCAnnotation *annota */ TCResult tc_task_remove_annotation(struct TCTask *task, int64_t entry); +/** + * Set a UDA on a mutable task. + */ +TCResult tc_task_set_uda(struct TCTask *task, + struct TCString *ns, + struct TCString *key, + struct TCString *value); + +/** + * Remove a UDA fraom a mutable task. + */ +TCResult tc_task_remove_uda(struct TCTask *task, struct TCString *ns, struct TCString *key); + +/** + * Set a legacy UDA on a mutable task. + */ +TCResult tc_task_set_legacy_uda(struct TCTask *task, struct TCString *key, struct TCString *value); + +/** + * Remove a UDA fraom a mutable task. + */ +TCResult tc_task_remove_legacy_uda(struct TCTask *task, struct TCString *key); + /** * Get the latest error for a task, or NULL if the last operation succeeded. Subsequent calls * to this function will return NULL. The task pointer must not be NULL. The caller must free the @@ -580,6 +671,20 @@ void tc_task_free(struct TCTask *task); */ void tc_task_list_free(struct TCTaskList *tctasks); +/** + * Free a TCUDA instance. The instance, and the TCStrings it contains, must not be used + * after this call. + */ +void tc_uda_free(struct TCUDA *tcuda); + +/** + * Free a TCUDAList instance. The instance, and all TCUDAs it contains, must not be used after + * this call. + * + * When this call returns, the `items` pointer will be NULL, signalling an invalid TCUDAList. + */ +void tc_uda_list_free(struct TCUDAList *tcudas); + /** * Create a new, randomly-generated UUID. */ From 1488355b89e8cf44a81b8e90d8454d43b60a1a3f Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sat, 12 Feb 2022 22:19:09 +0000 Subject: [PATCH 70/95] add working-set support --- .../src/bindings_tests/replica.c | 68 ++++++++++++++- lib/src/lib.rs | 2 + lib/src/replica.rs | 16 +++- lib/src/workingset.rs | 87 +++++++++++++++++++ lib/taskchampion.h | 44 ++++++++++ taskchampion/src/workingset.rs | 20 +++++ 6 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 lib/src/workingset.rs diff --git a/integration-tests/src/bindings_tests/replica.c b/integration-tests/src/bindings_tests/replica.c index 7e598695b..423d8dc25 100644 --- a/integration-tests/src/bindings_tests/replica.c +++ b/integration-tests/src/bindings_tests/replica.c @@ -40,12 +40,74 @@ static void test_replica_add_undo_point(void) { tc_replica_free(rep); } -// rebuilding working set succeeds -static void test_replica_rebuild_working_set(void) { +// working set operations succeed +static void test_replica_working_set(void) { + TCWorkingSet *ws; + TCTask *task1, *task2, *task3; + TCUuid uuid, uuid1, uuid2, uuid3; + TCReplica *rep = tc_replica_new_in_memory(); TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_rebuild_working_set(rep, true)); TEST_ASSERT_NULL(tc_replica_error(rep)); + + ws = tc_replica_working_set(rep); + TEST_ASSERT_EQUAL(0, tc_working_set_len(ws)); + tc_working_set_free(ws); + + task1 = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("task1")); + TEST_ASSERT_NOT_NULL(task1); + uuid1 = tc_task_get_uuid(task1); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_rebuild_working_set(rep, true)); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + task2 = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("task2")); + TEST_ASSERT_NOT_NULL(task2); + uuid2 = tc_task_get_uuid(task2); + + task3 = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("task3")); + TEST_ASSERT_NOT_NULL(task3); + uuid3 = tc_task_get_uuid(task3); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_rebuild_working_set(rep, false)); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + // finish task2 to leave a "hole" + tc_task_to_mut(task2, rep); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_done(task2)); + tc_task_to_immut(task2); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_rebuild_working_set(rep, false)); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + tc_task_free(task1); + tc_task_free(task2); + tc_task_free(task3); + + // working set should now be + // 0 -> None + // 1 -> uuid1 + // 2 -> None + // 3 -> uuid3 + ws = tc_replica_working_set(rep); + TEST_ASSERT_EQUAL(2, tc_working_set_len(ws)); + TEST_ASSERT_EQUAL(3, tc_working_set_largest_index(ws)); + + TEST_ASSERT_FALSE(tc_working_set_by_index(ws, 0, &uuid)); + TEST_ASSERT_TRUE(tc_working_set_by_index(ws, 1, &uuid)); + TEST_ASSERT_EQUAL_MEMORY(uuid1.bytes, uuid.bytes, sizeof(uuid)); + TEST_ASSERT_FALSE(tc_working_set_by_index(ws, 2, &uuid)); + TEST_ASSERT_TRUE(tc_working_set_by_index(ws, 3, &uuid)); + TEST_ASSERT_EQUAL_MEMORY(uuid3.bytes, uuid.bytes, sizeof(uuid)); + + TEST_ASSERT_EQUAL(1, tc_working_set_by_uuid(ws, uuid1)); + TEST_ASSERT_EQUAL(0, tc_working_set_by_uuid(ws, uuid2)); + TEST_ASSERT_EQUAL(3, tc_working_set_by_uuid(ws, uuid3)); + + tc_working_set_free(ws); + tc_replica_free(rep); } @@ -209,7 +271,7 @@ int replica_tests(void) { RUN_TEST(test_replica_creation_disk); RUN_TEST(test_replica_undo_empty); RUN_TEST(test_replica_add_undo_point); - RUN_TEST(test_replica_rebuild_working_set); + RUN_TEST(test_replica_working_set); RUN_TEST(test_replica_undo_empty_null_undone_out); RUN_TEST(test_replica_task_creation); RUN_TEST(test_replica_all_tasks); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 5bc7629a3..b49acf77c 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -14,6 +14,7 @@ pub mod string; pub mod task; pub mod uda; pub mod uuid; +pub mod workingset; pub(crate) mod types { pub(crate) use crate::annotation::{TCAnnotation, TCAnnotationList}; @@ -24,4 +25,5 @@ pub(crate) mod types { pub(crate) use crate::task::{TCTask, TCTaskList}; pub(crate) use crate::uda::{TCUDAList, TCUDA, UDA}; pub(crate) use crate::uuid::{TCUuid, TCUuidList}; + pub(crate) use crate::workingset::TCWorkingSet; } diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 99f07639d..b89df1091 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -177,7 +177,21 @@ pub unsafe extern "C" fn tc_replica_all_task_uuids(rep: *mut TCReplica) -> TCUui ) } -// TODO: tc_replica_working_set +/// Get the current working set for this replica. +/// +/// Returns NULL on error. +#[no_mangle] +pub unsafe extern "C" fn tc_replica_working_set(rep: *mut TCReplica) -> *mut TCWorkingSet { + wrap( + rep, + |rep| { + let ws = rep.working_set()?; + // SAFETY: caller promises to free this task + Ok(unsafe { TCWorkingSet::return_val(ws.into()) }) + }, + std::ptr::null_mut(), + ) +} /// Get an existing task by its UUID. /// diff --git a/lib/src/workingset.rs b/lib/src/workingset.rs new file mode 100644 index 000000000..91f73bbd0 --- /dev/null +++ b/lib/src/workingset.rs @@ -0,0 +1,87 @@ +use crate::traits::*; +use crate::types::*; +use taskchampion::{Uuid, WorkingSet}; + +/// A TCWorkingSet represents a snapshot of the working set for a replica. It is not automatically +/// updated based on changes in the replica. Its lifetime is independent of the replica and it can +/// be freed at any time. +/// +/// To iterate over a working set, search indexes 1 through largest_index. +pub struct TCWorkingSet(WorkingSet); + +impl PassByPointer for TCWorkingSet {} + +impl From for TCWorkingSet { + fn from(ws: WorkingSet) -> TCWorkingSet { + TCWorkingSet(ws) + } +} + +/// Utility function to get a shared reference to the underlying WorkingSet. +fn wrap<'a, T, F>(ws: *mut TCWorkingSet, f: F) -> T +where + F: FnOnce(&WorkingSet) -> T, +{ + // SAFETY: + // - ws is not null (promised by caller) + // - ws outlives 'a (promised by caller) + let tcws: &'a TCWorkingSet = unsafe { TCWorkingSet::from_arg_ref(ws) }; + f(&tcws.0) +} + +/// Get the working set's length, or the number of UUIDs it contains. +#[no_mangle] +pub unsafe extern "C" fn tc_working_set_len(ws: *mut TCWorkingSet) -> usize { + wrap(ws, |ws| ws.len()) +} + +/// Get the working set's largest index. +#[no_mangle] +pub unsafe extern "C" fn tc_working_set_largest_index(ws: *mut TCWorkingSet) -> usize { + wrap(ws, |ws| ws.largest_index()) +} + +/// Get the UUID for the task at the given index. Returns true if the UUID exists in the working +/// set. If not, returns false and does not change uuid_out. +#[no_mangle] +pub unsafe extern "C" fn tc_working_set_by_index( + ws: *mut TCWorkingSet, + index: usize, + uuid_out: *mut TCUuid, +) -> bool { + debug_assert!(!uuid_out.is_null()); + wrap(ws, |ws| { + if let Some(uuid) = ws.by_index(index) { + // SAFETY: + // - uuid_out is not NULL (promised by caller) + // - alignment is not required + unsafe { TCUuid::to_arg_out(uuid, uuid_out) }; + true + } else { + false + } + }) +} + +/// Get the working set index for the task with the given UUID. Returns 0 if the task is not in +/// the working set. +#[no_mangle] +pub unsafe extern "C" fn tc_working_set_by_uuid(ws: *mut TCWorkingSet, uuid: TCUuid) -> usize { + wrap(ws, |ws| { + // SAFETY: + // - tcuuid is a valid TCUuid (all byte patterns are valid) + let uuid: Uuid = unsafe { TCUuid::from_arg(uuid) }; + ws.by_uuid(uuid).unwrap_or(0) + }) +} + +/// Free a TCWorkingSet. The given value must not be NULL. The value must not be used after this +/// function returns, and must not be freed more than once. +#[no_mangle] +pub unsafe extern "C" fn tc_working_set_free(ws: *mut TCWorkingSet) { + // SAFETY: + // - rep is not NULL (promised by caller) + // - caller will not use the TCWorkingSet after this (promised by caller) + let ws = unsafe { TCWorkingSet::take_from_arg(ws) }; + drop(ws); +} diff --git a/lib/taskchampion.h b/lib/taskchampion.h index bc61b1e40..79b25ba35 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -117,6 +117,15 @@ typedef struct TCString TCString; */ typedef struct TCTask TCTask; +/** + * A TCWorkingSet represents a snapshot of the working set for a replica. It is not automatically + * updated based on changes in the replica. Its lifetime is independent of the replica and it can + * be freed at any time. + * + * To iterate over a working set, search indexes 1 through largest_index. + */ +typedef struct TCWorkingSet TCWorkingSet; + /** * TCAnnotation contains the details of an annotation. */ @@ -309,6 +318,13 @@ struct TCTaskList tc_replica_all_tasks(struct TCReplica *rep); */ struct TCUuidList tc_replica_all_task_uuids(struct TCReplica *rep); +/** + * Get the current working set for this replica. + * + * Returns NULL on error. + */ +struct TCWorkingSet *tc_replica_working_set(struct TCReplica *rep); + /** * Get an existing task by its UUID. * @@ -721,6 +737,34 @@ TCResult tc_uuid_from_str(struct TCString *s, struct TCUuid *uuid_out); */ void tc_uuid_list_free(struct TCUuidList *tcuuids); +/** + * Get the working set's length, or the number of UUIDs it contains. + */ +size_t tc_working_set_len(struct TCWorkingSet *ws); + +/** + * Get the working set's largest index. + */ +size_t tc_working_set_largest_index(struct TCWorkingSet *ws); + +/** + * Get the UUID for the task at the given index. Returns true if the UUID exists in the working + * set. If not, returns false and does not change uuid_out. + */ +bool tc_working_set_by_index(struct TCWorkingSet *ws, size_t index, struct TCUuid *uuid_out); + +/** + * Get the working set index for the task with the given UUID. Returns 0 if the task is not in + * the working set. + */ +size_t tc_working_set_by_uuid(struct TCWorkingSet *ws, struct TCUuid uuid); + +/** + * Free a TCWorkingSet. The given value must not be NULL. The value must not be used after this + * function returns, and must not be freed more than once. + */ +void tc_working_set_free(struct TCWorkingSet *ws); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/taskchampion/src/workingset.rs b/taskchampion/src/workingset.rs index ea746a72b..8bd0cce53 100644 --- a/taskchampion/src/workingset.rs +++ b/taskchampion/src/workingset.rs @@ -38,6 +38,11 @@ impl WorkingSet { self.by_index.iter().filter(|e| e.is_some()).count() } + /// Get the largest index in the working set, or zero if the set is empty. + pub fn largest_index(&self) -> usize { + self.by_index.len().saturating_sub(1) + } + /// True if the length is zero pub fn is_empty(&self) -> bool { self.by_index.iter().all(|e| e.is_none()) @@ -103,6 +108,21 @@ mod test { assert_eq!(ws.is_empty(), true); } + #[test] + fn test_largest_index() { + let (uuid1, uuid2, ws) = make(); + assert_eq!(ws.largest_index(), 0); + + let ws = WorkingSet::new(vec![None, Some(uuid1)]); + assert_eq!(ws.largest_index(), 1); + + let ws = WorkingSet::new(vec![None, Some(uuid1), None, Some(uuid2)]); + assert_eq!(ws.largest_index(), 3); + + let ws = WorkingSet::new(vec![None, Some(uuid1), None, Some(uuid2), None]); + assert_eq!(ws.largest_index(), 4); + } + #[test] fn test_by_index() { let (uuid1, uuid2, ws) = make(); From 213da88b27d8a7a5fa33a2c21f2a21468cffda3a Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 13 Feb 2022 02:05:05 +0000 Subject: [PATCH 71/95] add tc_task_get_taskmap --- integration-tests/src/bindings_tests/task.c | 42 ++++++++ lib/src/kv.rs | 103 ++++++++++++++++++++ lib/src/lib.rs | 2 + lib/src/task.rs | 19 +++- lib/taskchampion.h | 47 +++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 lib/src/kv.rs diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 5dab6c510..7f89ddc51 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -520,6 +520,47 @@ static void test_task_udas(void) { tc_replica_free(rep); } +static void tckvlist_assert_key(TCKVList *list, char *key, char *value) { + TEST_ASSERT_NOT_NULL(list); + for (size_t i = 0; i < list->len; i++) { + if (0 == strcmp(tc_string_content(list->items[i].key), key)) { + TEST_ASSERT_EQUAL_STRING(value, tc_string_content(list->items[i].value)); + return; + } + } + TEST_FAIL_MESSAGE("key not found"); +} + +// get_tags returns the list of tags +static void test_task_taskmap(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + tc_task_to_mut(task, rep); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_tag(task, tc_string_borrow("next"))); + + TCAnnotation ann; + ann.entry = 1644623411; + ann.description = tc_string_borrow("ann1"); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_annotation(task, &ann)); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_wait(task, 3643679997)); // 2085 + + TCKVList taskmap = tc_task_get_taskmap(task); + tckvlist_assert_key(&taskmap, "annotation_1644623411", "ann1"); + tckvlist_assert_key(&taskmap, "tag_next", ""); + tckvlist_assert_key(&taskmap, "status", "pending"); + tckvlist_assert_key(&taskmap, "description", "my task"); + tc_kv_list_free(&taskmap); + + tc_task_free(task); + tc_replica_free(rep); +} + int task_tests(void) { UNITY_BEGIN(); // each test case above should be named here, in order. @@ -536,5 +577,6 @@ int task_tests(void) { RUN_TEST(test_task_get_tags); RUN_TEST(test_task_annotations); RUN_TEST(test_task_udas); + RUN_TEST(test_task_taskmap); return UNITY_END(); } diff --git a/lib/src/kv.rs b/lib/src/kv.rs new file mode 100644 index 000000000..3b24e25cf --- /dev/null +++ b/lib/src/kv.rs @@ -0,0 +1,103 @@ +use crate::traits::*; +use crate::types::*; + +/// TCKV contains a key/value pair that is part of a task. +/// +/// Neither key nor value are ever NULL. They remain owned by the TCKV and +/// will be freed when it is freed with tc_kv_list_free. +#[repr(C)] +pub struct TCKV { + pub key: *mut TCString<'static>, + pub value: *mut TCString<'static>, +} + +impl PassByValue for TCKV { + type RustType = (TCString<'static>, TCString<'static>); + + unsafe fn from_ctype(self) -> Self::RustType { + // SAFETY: + // - self is owned, so we can take ownership of this TCString + // - self.key is a valid, non-null TCString (see type docstring) + let key = unsafe { TCString::take_from_arg(self.key) }; + // SAFETY: (same) + let value = unsafe { TCString::take_from_arg(self.value) }; + (key, value) + } + + fn as_ctype((key, value): Self::RustType) -> Self { + TCKV { + // SAFETY: caller assumes ownership of this value + key: unsafe { key.return_val() }, + // SAFETY: caller assumes ownership of this value + value: unsafe { value.return_val() }, + } + } +} + +/// TCKVList represents a list of key/value pairs. +/// +/// The content of this struct must be treated as read-only. +#[repr(C)] +pub struct TCKVList { + /// number of key/value pairs in items + len: libc::size_t, + + /// total size of items (internal use only) + _capacity: libc::size_t, + + /// array of TCKV's. these remain owned by the TCKVList instance and will be freed by + /// tc_kv_list_free. This pointer is never NULL for a valid TCKVList. + items: *const TCKV, +} + +impl CList for TCKVList { + type Element = TCKV; + + unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { + TCKVList { + len, + _capacity: cap, + items, + } + } + + fn into_raw_parts(self) -> (*const Self::Element, usize, usize) { + (self.items, self.len, self._capacity) + } +} + +/// Free a TCKVList instance. The instance, and all TCKVs it contains, must not be used after +/// this call. +/// +/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCKVList. +#[no_mangle] +pub unsafe extern "C" fn tc_kv_list_free(tckvs: *mut TCKVList) { + // SAFETY: + // - tckvs is not NULL and points to a valid TCKVList (caller is not allowed to + // modify the list) + // - caller promises not to use the value after return + unsafe { drop_value_list(tckvs) } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn empty_list_has_non_null_pointer() { + let tckvs = TCKVList::return_val(Vec::new()); + assert!(!tckvs.items.is_null()); + assert_eq!(tckvs.len, 0); + assert_eq!(tckvs._capacity, 0); + } + + #[test] + fn free_sets_null_pointer() { + let mut tckvs = TCKVList::return_val(Vec::new()); + // SAFETY: testing expected behavior + unsafe { tc_kv_list_free(&mut tckvs) }; + assert!(tckvs.items.is_null()); + assert_eq!(tckvs.len, 0); + assert_eq!(tckvs._capacity, 0); + } +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index b49acf77c..4c66ec76d 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -7,6 +7,7 @@ mod util; pub mod annotation; pub mod atomic; +pub mod kv; pub mod replica; pub mod result; pub mod status; @@ -18,6 +19,7 @@ pub mod workingset; pub(crate) mod types { pub(crate) use crate::annotation::{TCAnnotation, TCAnnotationList}; + pub(crate) use crate::kv::{TCKVList, TCKV}; pub(crate) use crate::replica::TCReplica; pub(crate) use crate::result::TCResult; pub(crate) use crate::status::TCStatus; diff --git a/lib/src/task.rs b/lib/src/task.rs index a20b2049f..421072373 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -243,7 +243,24 @@ pub unsafe extern "C" fn tc_task_get_status<'a>(task: *mut TCTask) -> TCStatus { wrap(task, |task| task.get_status().into()) } -// TODO: tc_task_get_taskmap (?? then we have to wrap a map..) +/// Get the underlying key/value pairs for this task. The returned TCKVList is +/// a "snapshot" of the task and will not be updated if the task is subsequently +/// modified. +#[no_mangle] +pub unsafe extern "C" fn tc_task_get_taskmap(task: *mut TCTask) -> TCKVList { + wrap(task, |task| { + let vec: Vec = task + .get_taskmap() + .iter() + .map(|(k, v)| { + let key = TCString::from(k.as_ref()); + let value = TCString::from(v.as_ref()); + TCKV::as_ctype((key, value)) + }) + .collect(); + TCKVList::return_val(vec) + }) +} /// Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it /// contains embedded NUL characters). diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 79b25ba35..bb41d4e5a 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -161,6 +161,38 @@ typedef struct TCAnnotationList { const struct TCAnnotation *items; } TCAnnotationList; +/** + * TCKV contains a key/value pair that is part of a task. + * + * Neither key nor value are ever NULL. They remain owned by the TCKV and + * will be freed when it is freed with tc_kv_list_free. + */ +typedef struct TCKV { + struct TCString *key; + struct TCString *value; +} TCKV; + +/** + * TCKVList represents a list of key/value pairs. + * + * The content of this struct must be treated as read-only. + */ +typedef struct TCKVList { + /** + * number of key/value pairs in items + */ + size_t len; + /** + * total size of items (internal use only) + */ + size_t _capacity; + /** + * array of TCKV's. these remain owned by the TCKVList instance and will be freed by + * tc_kv_list_free. This pointer is never NULL for a valid TCKVList. + */ + const struct TCKV *items; +} TCKVList; + /** * TCTaskList represents a list of tasks. * @@ -292,6 +324,14 @@ void tc_annotation_free(struct TCAnnotation *tcann); */ void tc_annotation_list_free(struct TCAnnotationList *tcanns); +/** + * Free a TCKVList instance. The instance, and all TCKVs it contains, must not be used after + * this call. + * + * When this call returns, the `items` pointer will be NULL, signalling an invalid TCKVList. + */ +void tc_kv_list_free(struct TCKVList *tckvs); + /** * Create a new TCReplica with an in-memory database. The contents of the database will be * lost when it is freed. @@ -493,6 +533,13 @@ struct TCUuid tc_task_get_uuid(struct TCTask *task); */ enum TCStatus tc_task_get_status(struct TCTask *task); +/** + * Get the underlying key/value pairs for this task. The returned TCKVList is + * a "snapshot" of the task and will not be updated if the task is subsequently + * modified. + */ +struct TCKVList tc_task_get_taskmap(struct TCTask *task); + /** * Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it * contains embedded NUL characters). From c22182cc19d57a3d441032a1688e77cfbc31e3a5 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 13 Feb 2022 02:30:17 +0000 Subject: [PATCH 72/95] rename trait methods to avoid ambiguity --- lib/src/annotation.rs | 6 ++--- lib/src/kv.rs | 8 +++--- lib/src/replica.rs | 34 ++++++++++++------------ lib/src/string.rs | 14 +++++----- lib/src/task.rs | 60 +++++++++++++++++++++---------------------- lib/src/traits.rs | 42 +++++++++++++++--------------- lib/src/uda.rs | 14 +++++----- lib/src/uuid.rs | 10 ++++---- lib/src/workingset.rs | 8 +++--- 9 files changed, 98 insertions(+), 98 deletions(-) diff --git a/lib/src/annotation.rs b/lib/src/annotation.rs index 558f0d8c4..b05b8992a 100644 --- a/lib/src/annotation.rs +++ b/lib/src/annotation.rs @@ -24,7 +24,7 @@ impl PassByValue for TCAnnotation { // SAFETY: // - self is owned, so we can take ownership of this TCString // - self.description is a valid, non-null TCString (see type docstring) - let description = unsafe { TCString::take_from_arg(self.description) }; + let description = unsafe { TCString::take_from_ptr_arg(self.description) }; (entry, description) } @@ -32,7 +32,7 @@ impl PassByValue for TCAnnotation { TCAnnotation { entry: libc::time_t::as_ctype(Some(entry)), // SAFETY: caller assumes ownership of this value - description: unsafe { description.return_val() }, + description: unsafe { description.return_ptr() }, } } } @@ -85,7 +85,7 @@ pub unsafe extern "C" fn tc_annotation_free(tcann: *mut TCAnnotation) { debug_assert!(!tcann.is_null()); // SAFETY: // - *tcann is a valid TCAnnotation (caller promises to treat it as read-only) - let annotation = unsafe { TCAnnotation::take_from_arg(tcann, TCAnnotation::default()) }; + let annotation = unsafe { TCAnnotation::take_val_from_arg(tcann, TCAnnotation::default()) }; drop(annotation); } diff --git a/lib/src/kv.rs b/lib/src/kv.rs index 3b24e25cf..ce79a0df9 100644 --- a/lib/src/kv.rs +++ b/lib/src/kv.rs @@ -18,18 +18,18 @@ impl PassByValue for TCKV { // SAFETY: // - self is owned, so we can take ownership of this TCString // - self.key is a valid, non-null TCString (see type docstring) - let key = unsafe { TCString::take_from_arg(self.key) }; + let key = unsafe { TCString::take_from_ptr_arg(self.key) }; // SAFETY: (same) - let value = unsafe { TCString::take_from_arg(self.value) }; + let value = unsafe { TCString::take_from_ptr_arg(self.value) }; (key, value) } fn as_ctype((key, value): Self::RustType) -> Self { TCKV { // SAFETY: caller assumes ownership of this value - key: unsafe { key.return_val() }, + key: unsafe { key.return_ptr() }, // SAFETY: caller assumes ownership of this value - value: unsafe { value.return_val() }, + value: unsafe { value.return_ptr() }, } } } diff --git a/lib/src/replica.rs b/lib/src/replica.rs index b89df1091..655120669 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -75,7 +75,7 @@ where F: FnOnce(&mut Replica) -> anyhow::Result, { // SAFETY: see type docstring - let rep: &mut TCReplica = unsafe { TCReplica::from_arg_ref_mut(rep) }; + let rep: &mut TCReplica = unsafe { TCReplica::from_ptr_arg_ref_mut(rep) }; if rep.mut_borrowed { panic!("replica is borrowed and cannot be used"); } @@ -97,7 +97,7 @@ pub unsafe extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica { .into_storage() .expect("in-memory always succeeds"); // SAFETY: see type docstring - unsafe { TCReplica::from(Replica::new(storage)).return_val() } + unsafe { TCReplica::from(Replica::new(storage)).return_ptr() } } /// Create a new TCReplica with an on-disk database having the given filename. On error, a string @@ -108,7 +108,7 @@ pub unsafe extern "C" fn tc_replica_new_on_disk<'a>( error_out: *mut *mut TCString, ) -> *mut TCReplica { // SAFETY: see TCString docstring - let path = unsafe { TCString::take_from_arg(path) }; + let path = unsafe { TCString::take_from_ptr_arg(path) }; let storage_res = StorageConfig::OnDisk { taskdb_dir: path.to_path_buf(), } @@ -119,7 +119,7 @@ pub unsafe extern "C" fn tc_replica_new_on_disk<'a>( Err(e) => { if !error_out.is_null() { unsafe { - *error_out = err_to_tcstring(e).return_val(); + *error_out = err_to_tcstring(e).return_ptr(); } } return std::ptr::null_mut(); @@ -127,7 +127,7 @@ pub unsafe extern "C" fn tc_replica_new_on_disk<'a>( }; // SAFETY: see type docstring - unsafe { TCReplica::from(Replica::new(storage)).return_val() } + unsafe { TCReplica::from(Replica::new(storage)).return_ptr() } } /// Get a list of all tasks in the replica. @@ -147,9 +147,9 @@ pub unsafe extern "C" fn tc_replica_all_tasks(rep: *mut TCReplica) -> TCTaskList .map(|(_uuid, t)| { NonNull::new( // SAFETY: see TCTask docstring - unsafe { TCTask::from(t).return_val() }, + unsafe { TCTask::from(t).return_ptr() }, ) - .expect("TCTask::return_val returned NULL") + .expect("TCTask::return_ptr returned NULL") }) .collect(); Ok(TCTaskList::return_val(tasks)) @@ -187,7 +187,7 @@ pub unsafe extern "C" fn tc_replica_working_set(rep: *mut TCReplica) -> *mut TCW |rep| { let ws = rep.working_set()?; // SAFETY: caller promises to free this task - Ok(unsafe { TCWorkingSet::return_val(ws.into()) }) + Ok(unsafe { TCWorkingSet::return_ptr(ws.into()) }) }, std::ptr::null_mut(), ) @@ -203,10 +203,10 @@ pub unsafe extern "C" fn tc_replica_get_task(rep: *mut TCReplica, tcuuid: TCUuid rep, |rep| { // SAFETY: see TCUuid docstring - let uuid = unsafe { TCUuid::from_arg(tcuuid) }; + let uuid = unsafe { TCUuid::val_from_arg(tcuuid) }; if let Some(task) = rep.get_task(uuid)? { // SAFETY: caller promises to free this task - Ok(unsafe { TCTask::from(task).return_val() }) + Ok(unsafe { TCTask::from(task).return_ptr() }) } else { Ok(std::ptr::null_mut()) } @@ -225,13 +225,13 @@ pub unsafe extern "C" fn tc_replica_new_task( description: *mut TCString, ) -> *mut TCTask { // SAFETY: see TCString docstring - let description = unsafe { TCString::take_from_arg(description) }; + let description = unsafe { TCString::take_from_ptr_arg(description) }; wrap( rep, |rep| { let task = rep.new_task(status.into(), description.as_str()?.to_string())?; // SAFETY: caller promises to free this task - Ok(unsafe { TCTask::from(task).return_val() }) + Ok(unsafe { TCTask::from(task).return_ptr() }) }, std::ptr::null_mut(), ) @@ -249,10 +249,10 @@ pub unsafe extern "C" fn tc_replica_import_task_with_uuid( rep, |rep| { // SAFETY: see TCUuid docstring - let uuid = unsafe { TCUuid::from_arg(tcuuid) }; + let uuid = unsafe { TCUuid::val_from_arg(tcuuid) }; let task = rep.import_task_with_uuid(uuid)?; // SAFETY: caller promises to free this task - Ok(unsafe { TCTask::from(task).return_val() }) + Ok(unsafe { TCTask::from(task).return_ptr() }) }, std::ptr::null_mut(), ) @@ -325,10 +325,10 @@ pub unsafe extern "C" fn tc_replica_rebuild_working_set( #[no_mangle] pub unsafe extern "C" fn tc_replica_error<'a>(rep: *mut TCReplica) -> *mut TCString<'static> { // SAFETY: see type docstring - let rep: &'a mut TCReplica = unsafe { TCReplica::from_arg_ref_mut(rep) }; + let rep: &'a mut TCReplica = unsafe { TCReplica::from_ptr_arg_ref_mut(rep) }; if let Some(tcstring) = rep.error.take() { // SAFETY: see TCString docstring - unsafe { tcstring.return_val() } + unsafe { tcstring.return_ptr() } } else { std::ptr::null_mut() } @@ -339,7 +339,7 @@ pub unsafe extern "C" fn tc_replica_error<'a>(rep: *mut TCReplica) -> *mut TCStr #[no_mangle] pub unsafe extern "C" fn tc_replica_free(rep: *mut TCReplica) { // SAFETY: see type docstring - let replica = unsafe { TCReplica::take_from_arg(rep) }; + let replica = unsafe { TCReplica::take_from_ptr_arg(rep) }; if replica.mut_borrowed { panic!("replica is borrowed and cannot be freed"); } diff --git a/lib/src/string.rs b/lib/src/string.rs index af40714d9..442ffd0e9 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -194,7 +194,7 @@ pub unsafe extern "C" fn tc_string_borrow(cstr: *const libc::c_char) -> *mut TCS // - cstr's content will not change before it is destroyed (promised by caller) let cstr: &CStr = unsafe { CStr::from_ptr(cstr) }; // SAFETY: see docstring - unsafe { TCString::CStr(cstr).return_val() } + unsafe { TCString::CStr(cstr).return_ptr() } } /// Create a new TCString by cloning the content of the given C string. The resulting TCString @@ -209,7 +209,7 @@ pub unsafe extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> *mut TCSt // - cstr's content will not change before it is destroyed (by C convention) let cstr: &CStr = unsafe { CStr::from_ptr(cstr) }; // SAFETY: see docstring - unsafe { TCString::CString(cstr.into()).return_val() } + unsafe { TCString::CString(cstr.into()).return_ptr() } } /// Create a new TCString containing the given string with the given length. This allows creation @@ -246,7 +246,7 @@ pub unsafe extern "C" fn tc_string_clone_with_len( }; // SAFETY: see docstring - unsafe { tcstring.return_val() } + unsafe { tcstring.return_ptr() } } /// Get the content of the string as a regular C string. The given string must not be NULL. The @@ -263,7 +263,7 @@ pub unsafe extern "C" fn tc_string_content(tcstring: *mut TCString) -> *const li // - tcstring is not NULL (promised by caller) // - lifetime of tcstring outlives the lifetime of this function // - lifetime of tcstring outlives the lifetime of the returned pointer (promised by caller) - let tcstring = unsafe { TCString::from_arg_ref_mut(tcstring) }; + let tcstring = unsafe { TCString::from_ptr_arg_ref_mut(tcstring) }; // if we have a String, we need to consume it and turn it into // a CString. @@ -293,7 +293,7 @@ pub unsafe extern "C" fn tc_string_content_with_len( // - tcstring is not NULL (promised by caller) // - lifetime of tcstring outlives the lifetime of this function // - lifetime of tcstring outlives the lifetime of the returned pointer (promised by caller) - let tcstring = unsafe { TCString::from_arg_ref(tcstring) }; + let tcstring = unsafe { TCString::from_ptr_arg_ref(tcstring) }; let bytes = tcstring.as_bytes(); @@ -301,7 +301,7 @@ pub unsafe extern "C" fn tc_string_content_with_len( // - len_out is not NULL (promised by caller) // - len_out points to valid memory (promised by caller) // - len_out is properly aligned (C convention) - unsafe { usize::to_arg_out(bytes.len(), len_out) }; + unsafe { usize::val_to_arg_out(bytes.len(), len_out) }; bytes.as_ptr() as *const libc::c_char } @@ -312,7 +312,7 @@ pub unsafe extern "C" fn tc_string_free(tcstring: *mut TCString) { // SAFETY: // - tcstring is not NULL (promised by caller) // - caller is exclusive owner of tcstring (promised by caller) - drop(unsafe { TCString::take_from_arg(tcstring) }); + drop(unsafe { TCString::take_from_ptr_arg(tcstring) }); } /// Free a TCStringList instance. The instance, and all TCStringList it contains, must not be used after diff --git a/lib/src/task.rs b/lib/src/task.rs index 421072373..24dc1fcfc 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -63,7 +63,7 @@ impl TCTask { // - tcreplica is not null (promised by caller) // - tcreplica outlives the pointer in this variant (promised by caller) let tcreplica_ref: &mut TCReplica = - unsafe { TCReplica::from_arg_ref_mut(tcreplica) }; + unsafe { TCReplica::from_ptr_arg_ref_mut(tcreplica) }; let rep_ref = tcreplica_ref.borrow_mut(); Inner::Mutable(task.into_mut(rep_ref), tcreplica) } @@ -83,7 +83,7 @@ impl TCTask { // variant) // - tcreplica is still alive (promised by caller of to_mut) let tcreplica_ref: &mut TCReplica = - unsafe { TCReplica::from_arg_ref_mut(tcreplica) }; + unsafe { TCReplica::from_ptr_arg_ref_mut(tcreplica) }; tcreplica_ref.release_borrow(); Inner::Immutable(task.into_immut()) } @@ -110,7 +110,7 @@ where // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) - let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; + let tctask: &'a mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; let task: &'a Task = match &tctask.inner { Inner::Immutable(t) => t, Inner::Mutable(t, _) => t.deref(), @@ -130,7 +130,7 @@ where // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) - let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; + let tctask: &'a mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; let task: &'a mut TaskMut = match tctask.inner { Inner::Immutable(_) => panic!("Task is immutable"), Inner::Mutable(ref mut t, _) => t, @@ -209,7 +209,7 @@ pub unsafe extern "C" fn tc_task_to_mut<'a>(task: *mut TCTask, tcreplica: *mut T // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) - let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; + let tctask: &'a mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; // SAFETY: // - tcreplica is not NULL (promised by caller) // - tcreplica lives until later call to to_immut via tc_task_to_immut (promised by caller, @@ -227,7 +227,7 @@ pub unsafe extern "C" fn tc_task_to_immut<'a>(task: *mut TCTask) { // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) - let tctask: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; + let tctask: &'a mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; tctask.to_immut(); } @@ -269,7 +269,7 @@ pub unsafe extern "C" fn tc_task_get_description<'a>(task: *mut TCTask) -> *mut wrap(task, |task| { let descr: TCString = task.get_description().into(); // SAFETY: see TCString docstring - unsafe { descr.return_val() } + unsafe { descr.return_ptr() } }) } @@ -308,7 +308,7 @@ pub unsafe extern "C" fn tc_task_is_active(task: *mut TCTask) -> bool { #[no_mangle] pub unsafe extern "C" fn tc_task_has_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> bool { // SAFETY: see TCString docstring - let tcstring = unsafe { TCString::take_from_arg(tag) }; + let tcstring = unsafe { TCString::take_from_ptr_arg(tag) }; wrap(task, |task| { if let Ok(tag) = Tag::try_from(tcstring) { task.has_tag(&tag) @@ -330,9 +330,9 @@ pub unsafe extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCStringList .map(|t| { NonNull::new( // SAFETY: see TCString docstring - unsafe { TCString::from(t.as_ref()).return_val() }, + unsafe { TCString::from(t.as_ref()).return_ptr() }, ) - .expect("TCString::return_val() returned NULL") + .expect("TCString::return_ptr() returned NULL") }) .collect(); TCStringList::return_val(vec) @@ -367,12 +367,12 @@ pub unsafe extern "C" fn tc_task_get_uda<'a>( key: *mut TCString<'a>, ) -> *mut TCString<'static> { wrap(task, |task| { - if let Ok(ns) = unsafe { TCString::take_from_arg(ns) }.as_str() { - if let Ok(key) = unsafe { TCString::take_from_arg(key) }.as_str() { + if let Ok(ns) = unsafe { TCString::take_from_ptr_arg(ns) }.as_str() { + if let Ok(key) = unsafe { TCString::take_from_ptr_arg(key) }.as_str() { if let Some(value) = task.get_uda(ns, key) { // SAFETY: // - caller will free this string (caller promises) - return unsafe { TCString::return_val(value.into()) }; + return unsafe { TCString::return_ptr(value.into()) }; } } } @@ -389,11 +389,11 @@ pub unsafe extern "C" fn tc_task_get_legacy_uda<'a>( key: *mut TCString<'a>, ) -> *mut TCString<'static> { wrap(task, |task| { - if let Ok(key) = unsafe { TCString::take_from_arg(key) }.as_str() { + if let Ok(key) = unsafe { TCString::take_from_ptr_arg(key) }.as_str() { if let Some(value) = task.get_legacy_uda(key) { // SAFETY: // - caller will free this string (caller promises) - return unsafe { TCString::return_val(value.into()) }; + return unsafe { TCString::return_ptr(value.into()) }; } } std::ptr::null_mut() @@ -461,7 +461,7 @@ pub unsafe extern "C" fn tc_task_set_description<'a>( description: *mut TCString, ) -> TCResult { // SAFETY: see TCString docstring - let description = unsafe { TCString::take_from_arg(description) }; + let description = unsafe { TCString::take_from_ptr_arg(description) }; wrap_mut( task, |task| { @@ -577,7 +577,7 @@ pub unsafe extern "C" fn tc_task_delete(task: *mut TCTask) -> TCResult { #[no_mangle] pub unsafe extern "C" fn tc_task_add_tag(task: *mut TCTask, tag: *mut TCString) -> TCResult { // SAFETY: see TCString docstring - let tcstring = unsafe { TCString::take_from_arg(tag) }; + let tcstring = unsafe { TCString::take_from_ptr_arg(tag) }; wrap_mut( task, |task| { @@ -593,7 +593,7 @@ pub unsafe extern "C" fn tc_task_add_tag(task: *mut TCTask, tag: *mut TCString) #[no_mangle] pub unsafe extern "C" fn tc_task_remove_tag(task: *mut TCTask, tag: *mut TCString) -> TCResult { // SAFETY: see TCString docstring - let tcstring = unsafe { TCString::take_from_arg(tag) }; + let tcstring = unsafe { TCString::take_from_ptr_arg(tag) }; wrap_mut( task, |task| { @@ -613,7 +613,7 @@ pub unsafe extern "C" fn tc_task_add_annotation( ) -> TCResult { // SAFETY: see TCAnnotation docstring let (entry, description) = - unsafe { TCAnnotation::take_from_arg(annotation, TCAnnotation::default()) }; + unsafe { TCAnnotation::take_val_from_arg(annotation, TCAnnotation::default()) }; wrap_mut( task, |task| { @@ -647,11 +647,11 @@ pub unsafe extern "C" fn tc_task_set_uda<'a>( value: *mut TCString, ) -> TCResult { // SAFETY: see TCString docstring - let ns = unsafe { TCString::take_from_arg(ns) }; + let ns = unsafe { TCString::take_from_ptr_arg(ns) }; // SAFETY: see TCString docstring - let key = unsafe { TCString::take_from_arg(key) }; + let key = unsafe { TCString::take_from_ptr_arg(key) }; // SAFETY: see TCString docstring - let value = unsafe { TCString::take_from_arg(value) }; + let value = unsafe { TCString::take_from_ptr_arg(value) }; wrap_mut( task, |task| { @@ -674,9 +674,9 @@ pub unsafe extern "C" fn tc_task_remove_uda<'a>( key: *mut TCString, ) -> TCResult { // SAFETY: see TCString docstring - let ns = unsafe { TCString::take_from_arg(ns) }; + let ns = unsafe { TCString::take_from_ptr_arg(ns) }; // SAFETY: see TCString docstring - let key = unsafe { TCString::take_from_arg(key) }; + let key = unsafe { TCString::take_from_ptr_arg(key) }; wrap_mut( task, |task| { @@ -695,9 +695,9 @@ pub unsafe extern "C" fn tc_task_set_legacy_uda<'a>( value: *mut TCString, ) -> TCResult { // SAFETY: see TCString docstring - let key = unsafe { TCString::take_from_arg(key) }; + let key = unsafe { TCString::take_from_ptr_arg(key) }; // SAFETY: see TCString docstring - let value = unsafe { TCString::take_from_arg(value) }; + let value = unsafe { TCString::take_from_ptr_arg(value) }; wrap_mut( task, |task| { @@ -715,7 +715,7 @@ pub unsafe extern "C" fn tc_task_remove_legacy_uda<'a>( key: *mut TCString, ) -> TCResult { // SAFETY: see TCString docstring - let key = unsafe { TCString::take_from_arg(key) }; + let key = unsafe { TCString::take_from_ptr_arg(key) }; wrap_mut( task, |task| { @@ -734,9 +734,9 @@ pub unsafe extern "C" fn tc_task_error<'a>(task: *mut TCTask) -> *mut TCString<' // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) - let task: &'a mut TCTask = unsafe { TCTask::from_arg_ref_mut(task) }; + let task: &'a mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; if let Some(tcstring) = task.error.take() { - unsafe { tcstring.return_val() } + unsafe { tcstring.return_ptr() } } else { std::ptr::null_mut() } @@ -751,7 +751,7 @@ pub unsafe extern "C" fn tc_task_free<'a>(task: *mut TCTask) { // SAFETY: // - rep is not NULL (promised by caller) // - caller will not use the TCTask after this (promised by caller) - let mut tctask = unsafe { TCTask::take_from_arg(task) }; + let mut tctask = unsafe { TCTask::take_from_ptr_arg(task) }; // convert to immut if it was mutable tctask.to_immut(); diff --git a/lib/src/traits.rs b/lib/src/traits.rs index 6a4ad2735..5cd5746ff 100644 --- a/lib/src/traits.rs +++ b/lib/src/traits.rs @@ -27,7 +27,7 @@ pub(crate) trait PassByValue: Sized { /// /// `self` must be a valid instance of Self. This is typically ensured either by requiring /// that C code not modify it, or by defining the valid values in C comments. - unsafe fn from_arg(arg: Self) -> Self::RustType { + unsafe fn val_from_arg(arg: Self) -> Self::RustType { // SAFETY: // - arg is a valid CType (promised by caller) unsafe { arg.from_ctype() } @@ -38,15 +38,15 @@ pub(crate) trait PassByValue: Sized { /// /// # Safety /// - /// `*arg` must be a valid CType, as with [`from_arg`]. - unsafe fn take_from_arg(arg: *mut Self, mut replacement: Self) -> Self::RustType { + /// `*arg` must be a valid CType, as with [`val_from_arg`]. + unsafe fn take_val_from_arg(arg: *mut Self, mut replacement: Self) -> Self::RustType { // SAFETY: // - arg is valid (promised by caller) // - replacement is valid (guaranteed by Rust) unsafe { std::ptr::swap(arg, &mut replacement) }; // SAFETY: // - replacement (formerly *arg) is a valid CType (promised by caller) - unsafe { PassByValue::from_arg(replacement) } + unsafe { PassByValue::val_from_arg(replacement) } } /// Return a value to C @@ -60,7 +60,7 @@ pub(crate) trait PassByValue: Sized { /// /// `arg_out` must not be NULL and must be properly aligned and pointing to valid memory /// of the size of CType. - unsafe fn to_arg_out(val: Self::RustType, arg_out: *mut Self) { + unsafe fn val_to_arg_out(val: Self::RustType, arg_out: *mut Self) { debug_assert!(!arg_out.is_null()); // SAFETY: // - arg_out is not NULL (promised by caller, asserted) @@ -81,13 +81,13 @@ pub(crate) trait PassByValue: Sized { /// - the pointer must not be NULL; /// - the pointer must be one previously returned from Rust; and /// - the memory addressed by the pointer must never be modified by C code. -/// - For `from_arg_ref`, the value must not be modified during the call to the Rust function -/// - For `from_arg_ref_mut`, the value must not be accessed (read or write) during the call +/// - For `from_ptr_arg_ref`, the value must not be modified during the call to the Rust function +/// - For `from_ptr_arg_ref_mut`, the value must not be accessed (read or write) during the call /// (these last two points are trivially ensured by all TC… types being non-threadsafe) -/// - For `take_from_arg`, the pointer becomes invalid and must not be used in _any way_ after it +/// - For `take_from_ptr_arg`, the pointer becomes invalid and must not be used in _any way_ after it /// is passed to the Rust function. -/// - For `return_val` and `to_arg_out`, it is the C caller's responsibility to later free the value. -/// - For `to_arg_out`, `arg_out` must not be NULL and must be properly aligned and pointing to +/// - For `return_ptr` and `ptr_to_arg_out`, it is the C caller's responsibility to later free the value. +/// - For `ptr_to_arg_out`, `arg_out` must not be NULL and must be properly aligned and pointing to /// valid memory. /// /// These requirements should be expressed in the C documentation for the type implementing this @@ -98,7 +98,7 @@ pub(crate) trait PassByPointer: Sized { /// # Safety /// /// See trait documentation. - unsafe fn take_from_arg(arg: *mut Self) -> Self { + unsafe fn take_from_ptr_arg(arg: *mut Self) -> Self { debug_assert!(!arg.is_null()); // SAFETY: see trait documentation unsafe { *(Box::from_raw(arg)) } @@ -109,7 +109,7 @@ pub(crate) trait PassByPointer: Sized { /// # Safety /// /// See trait documentation. - unsafe fn from_arg_ref<'a>(arg: *const Self) -> &'a Self { + unsafe fn from_ptr_arg_ref<'a>(arg: *const Self) -> &'a Self { debug_assert!(!arg.is_null()); // SAFETY: see trait documentation unsafe { &*arg } @@ -120,7 +120,7 @@ pub(crate) trait PassByPointer: Sized { /// # Safety /// /// See trait documentation. - unsafe fn from_arg_ref_mut<'a>(arg: *mut Self) -> &'a mut Self { + unsafe fn from_ptr_arg_ref_mut<'a>(arg: *mut Self) -> &'a mut Self { debug_assert!(!arg.is_null()); // SAFETY: see trait documentation unsafe { &mut *arg } @@ -131,7 +131,7 @@ pub(crate) trait PassByPointer: Sized { /// # Safety /// /// See trait documentation. - unsafe fn return_val(self) -> *mut Self { + unsafe fn return_ptr(self) -> *mut Self { Box::into_raw(Box::new(self)) } @@ -140,10 +140,10 @@ pub(crate) trait PassByPointer: Sized { /// # Safety /// /// See trait documentation. - unsafe fn to_arg_out(self, arg_out: *mut *mut Self) { + unsafe fn ptr_to_arg_out(self, arg_out: *mut *mut Self) { // SAFETY: see trait documentation unsafe { - *arg_out = self.return_val(); + *arg_out = self.return_ptr(); } } } @@ -154,7 +154,7 @@ pub(crate) trait PassByPointer: Sized { /// required trait functions just fetch and set these fields. The PassByValue trait will be /// implemented automatically, converting between the C type and `Vec`. For most cases, /// it is only necessary to implement `tc_.._free` that first calls -/// `PassByValue::take_from_arg(arg, CList::null_value())` to take the existing value and +/// `PassByValue::take_val_from_arg(arg, CList::null_value())` to take the existing value and /// replace it with the null value; then one of hte `drop_.._list(..)` functions to drop the resulting /// vector and all of the objects it points to. /// @@ -210,14 +210,14 @@ where // SAFETY: // - *list is a valid CL (caller promises to treat it as read-only) - let mut vec = unsafe { CL::take_from_arg(list, CL::null_value()) }; + let mut vec = unsafe { CL::take_val_from_arg(list, CL::null_value()) }; // first, drop each of the elements in turn for e in vec.drain(..) { // SAFETY: // - e is a valid Element (caller promisd not to change it) // - Vec::drain has invalidated this entry (value is owned) - drop(unsafe { PassByValue::from_arg(e) }); + drop(unsafe { PassByValue::val_from_arg(e) }); } // then drop the vector drop(vec); @@ -240,14 +240,14 @@ where debug_assert!(!list.is_null()); // SAFETY: // - *list is a valid CL (caller promises to treat it as read-only) - let mut vec = unsafe { CL::take_from_arg(list, CL::null_value()) }; + let mut vec = unsafe { CL::take_val_from_arg(list, CL::null_value()) }; // first, drop each of the elements in turn for e in vec.drain(..) { // SAFETY: // - e is a valid Element (caller promised not to change it) // - Vec::drain has invalidated this entry (value is owned) - drop(unsafe { PassByPointer::take_from_arg(e.as_ptr()) }); + drop(unsafe { PassByPointer::take_from_ptr_arg(e.as_ptr()) }); } // then drop the vector drop(vec); diff --git a/lib/src/uda.rs b/lib/src/uda.rs index 27134d80e..4c0fe03f2 100644 --- a/lib/src/uda.rs +++ b/lib/src/uda.rs @@ -29,16 +29,16 @@ impl PassByValue for TCUDA { // SAFETY: // - self is owned, so we can take ownership of this TCString // - self.ns is a valid, non-null TCString (NULL just checked) - Some(unsafe { TCString::take_from_arg(self.ns) }) + Some(unsafe { TCString::take_from_ptr_arg(self.ns) }) }, // SAFETY: // - self is owned, so we can take ownership of this TCString // - self.key is a valid, non-null TCString (see type docstring) - key: unsafe { TCString::take_from_arg(self.key) }, + key: unsafe { TCString::take_from_ptr_arg(self.key) }, // SAFETY: // - self is owned, so we can take ownership of this TCString // - self.value is a valid, non-null TCString (see type docstring) - value: unsafe { TCString::take_from_arg(self.value) }, + value: unsafe { TCString::take_from_ptr_arg(self.value) }, } } @@ -46,14 +46,14 @@ impl PassByValue for TCUDA { TCUDA { // SAFETY: caller assumes ownership of this value ns: if let Some(ns) = uda.ns { - unsafe { ns.return_val() } + unsafe { ns.return_ptr() } } else { std::ptr::null_mut() }, // SAFETY: caller assumes ownership of this value - key: unsafe { uda.key.return_val() }, + key: unsafe { uda.key.return_ptr() }, // SAFETY: caller assumes ownership of this value - value: unsafe { uda.value.return_val() }, + value: unsafe { uda.value.return_ptr() }, } } } @@ -107,7 +107,7 @@ pub unsafe extern "C" fn tc_uda_free(tcuda: *mut TCUDA) { debug_assert!(!tcuda.is_null()); // SAFETY: // - *tcuda is a valid TCUDA (caller promises to treat it as read-only) - let uda = unsafe { TCUDA::take_from_arg(tcuda, TCUDA::default()) }; + let uda = unsafe { TCUDA::take_val_from_arg(tcuda, TCUDA::default()) }; drop(uda); } diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index 8a26e4d11..f3d0528b3 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -88,7 +88,7 @@ pub unsafe extern "C" fn tc_uuid_to_buf<'a>(tcuuid: TCUuid, buf: *mut libc::c_ch }; // SAFETY: // - tcuuid is a valid TCUuid (all byte patterns are valid) - let uuid: Uuid = unsafe { TCUuid::from_arg(tcuuid) }; + let uuid: Uuid = unsafe { TCUuid::val_from_arg(tcuuid) }; uuid.to_hyphenated().encode_lower(buf); } @@ -98,10 +98,10 @@ pub unsafe extern "C" fn tc_uuid_to_buf<'a>(tcuuid: TCUuid, buf: *mut libc::c_ch pub unsafe extern "C" fn tc_uuid_to_str(tcuuid: TCUuid) -> *mut TCString<'static> { // SAFETY: // - tcuuid is a valid TCUuid (all byte patterns are valid) - let uuid: Uuid = unsafe { TCUuid::from_arg(tcuuid) }; + let uuid: Uuid = unsafe { TCUuid::val_from_arg(tcuuid) }; let s = uuid.to_string(); // SAFETY: see TCString docstring - unsafe { TCString::from(s).return_val() } + unsafe { TCString::from(s).return_ptr() } } /// Parse the given string as a UUID. Returns TC_RESULT_ERROR on parse failure or if the given @@ -111,13 +111,13 @@ pub unsafe extern "C" fn tc_uuid_from_str<'a>(s: *mut TCString, uuid_out: *mut T debug_assert!(!s.is_null()); debug_assert!(!uuid_out.is_null()); // SAFETY: see TCString docstring - let s = unsafe { TCString::take_from_arg(s) }; + let s = unsafe { TCString::take_from_ptr_arg(s) }; if let Ok(s) = s.as_str() { if let Ok(u) = Uuid::parse_str(s) { // SAFETY: // - uuid_out is not NULL (promised by caller) // - alignment is not required - unsafe { TCUuid::to_arg_out(u, uuid_out) }; + unsafe { TCUuid::val_to_arg_out(u, uuid_out) }; return TCResult::Ok; } } diff --git a/lib/src/workingset.rs b/lib/src/workingset.rs index 91f73bbd0..02780e50f 100644 --- a/lib/src/workingset.rs +++ b/lib/src/workingset.rs @@ -25,7 +25,7 @@ where // SAFETY: // - ws is not null (promised by caller) // - ws outlives 'a (promised by caller) - let tcws: &'a TCWorkingSet = unsafe { TCWorkingSet::from_arg_ref(ws) }; + let tcws: &'a TCWorkingSet = unsafe { TCWorkingSet::from_ptr_arg_ref(ws) }; f(&tcws.0) } @@ -55,7 +55,7 @@ pub unsafe extern "C" fn tc_working_set_by_index( // SAFETY: // - uuid_out is not NULL (promised by caller) // - alignment is not required - unsafe { TCUuid::to_arg_out(uuid, uuid_out) }; + unsafe { TCUuid::val_to_arg_out(uuid, uuid_out) }; true } else { false @@ -70,7 +70,7 @@ pub unsafe extern "C" fn tc_working_set_by_uuid(ws: *mut TCWorkingSet, uuid: TCU wrap(ws, |ws| { // SAFETY: // - tcuuid is a valid TCUuid (all byte patterns are valid) - let uuid: Uuid = unsafe { TCUuid::from_arg(uuid) }; + let uuid: Uuid = unsafe { TCUuid::val_from_arg(uuid) }; ws.by_uuid(uuid).unwrap_or(0) }) } @@ -82,6 +82,6 @@ pub unsafe extern "C" fn tc_working_set_free(ws: *mut TCWorkingSet) { // SAFETY: // - rep is not NULL (promised by caller) // - caller will not use the TCWorkingSet after this (promised by caller) - let ws = unsafe { TCWorkingSet::take_from_arg(ws) }; + let ws = unsafe { TCWorkingSet::take_from_ptr_arg(ws) }; drop(ws); } From bbb7b64842eff9dbb6bb13af6f0d94ee2224756f Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 13 Feb 2022 03:19:11 +0000 Subject: [PATCH 73/95] review safety comments --- lib/src/annotation.rs | 13 +++--- lib/src/kv.rs | 9 ++-- lib/src/traits.rs | 96 +++++++++++++++++++------------------------ 3 files changed, 57 insertions(+), 61 deletions(-) diff --git a/lib/src/annotation.rs b/lib/src/annotation.rs index b05b8992a..a1fe78da5 100644 --- a/lib/src/annotation.rs +++ b/lib/src/annotation.rs @@ -19,11 +19,12 @@ impl PassByValue for TCAnnotation { unsafe fn from_ctype(self) -> Self::RustType { // SAFETY: // - any time_t value is valid - // - time_t is not zero, so unwrap is safe (see type docstring) - let entry = unsafe { self.entry.from_ctype() }.unwrap(); + // - time_t is copy, so ownership is not important + let entry = unsafe { self.entry.val_from_arg() }.unwrap(); // SAFETY: + // - self.description is not NULL (field docstring) + // - self.description came from return_ptr in as_ctype // - self is owned, so we can take ownership of this TCString - // - self.description is a valid, non-null TCString (see type docstring) let description = unsafe { TCString::take_from_ptr_arg(self.description) }; (entry, description) } @@ -31,7 +32,8 @@ impl PassByValue for TCAnnotation { fn as_ctype((entry, description): Self::RustType) -> Self { TCAnnotation { entry: libc::time_t::as_ctype(Some(entry)), - // SAFETY: caller assumes ownership of this value + // SAFETY: + // - ownership of the TCString tied to ownership of Self description: unsafe { description.return_ptr() }, } } @@ -84,7 +86,8 @@ impl CList for TCAnnotationList { pub unsafe extern "C" fn tc_annotation_free(tcann: *mut TCAnnotation) { debug_assert!(!tcann.is_null()); // SAFETY: - // - *tcann is a valid TCAnnotation (caller promises to treat it as read-only) + // - tcann is not NULL + // - *tcann is a valid TCAnnotation (caller promised to treat it as read-only) let annotation = unsafe { TCAnnotation::take_val_from_arg(tcann, TCAnnotation::default()) }; drop(annotation); } diff --git a/lib/src/kv.rs b/lib/src/kv.rs index ce79a0df9..1a6f61c00 100644 --- a/lib/src/kv.rs +++ b/lib/src/kv.rs @@ -16,8 +16,9 @@ impl PassByValue for TCKV { unsafe fn from_ctype(self) -> Self::RustType { // SAFETY: + // - self.key is not NULL (field docstring) + // - self.key came from return_ptr in as_ctype // - self is owned, so we can take ownership of this TCString - // - self.key is a valid, non-null TCString (see type docstring) let key = unsafe { TCString::take_from_ptr_arg(self.key) }; // SAFETY: (same) let value = unsafe { TCString::take_from_ptr_arg(self.value) }; @@ -26,9 +27,11 @@ impl PassByValue for TCKV { fn as_ctype((key, value): Self::RustType) -> Self { TCKV { - // SAFETY: caller assumes ownership of this value + // SAFETY: + // - ownership of the TCString tied to ownership of Self key: unsafe { key.return_ptr() }, - // SAFETY: caller assumes ownership of this value + // SAFETY: + // - ownership of the TCString tied to ownership of Self value: unsafe { value.return_ptr() }, } } diff --git a/lib/src/traits.rs b/lib/src/traits.rs index 5cd5746ff..57537d212 100644 --- a/lib/src/traits.rs +++ b/lib/src/traits.rs @@ -25,8 +25,10 @@ pub(crate) trait PassByValue: Sized { /// /// # Safety /// - /// `self` must be a valid instance of Self. This is typically ensured either by requiring - /// that C code not modify it, or by defining the valid values in C comments. + /// - `self` must be a valid instance of the C type. This is typically ensured either by + /// requiring that C code not modify it, or by defining the valid values in C comments. + /// - if RustType is not Copy, then arg must not be used by the caller after calling this + /// function unsafe fn val_from_arg(arg: Self) -> Self::RustType { // SAFETY: // - arg is a valid CType (promised by caller) @@ -38,11 +40,12 @@ pub(crate) trait PassByValue: Sized { /// /// # Safety /// - /// `*arg` must be a valid CType, as with [`val_from_arg`]. + /// - arg must not be NULL + /// - *arg must be a valid, properly aligned instance of the C type unsafe fn take_val_from_arg(arg: *mut Self, mut replacement: Self) -> Self::RustType { // SAFETY: // - arg is valid (promised by caller) - // - replacement is valid (guaranteed by Rust) + // - replacement is valid and aligned (guaranteed by Rust) unsafe { std::ptr::swap(arg, &mut replacement) }; // SAFETY: // - replacement (formerly *arg) is a valid CType (promised by caller) @@ -58,8 +61,8 @@ pub(crate) trait PassByValue: Sized { /// /// # Safety /// - /// `arg_out` must not be NULL and must be properly aligned and pointing to valid memory - /// of the size of CType. + /// - `arg_out` must not be NULL and must be properly aligned and pointing to valid memory + /// of the size of CType. unsafe fn val_to_arg_out(val: Self::RustType, arg_out: *mut Self) { debug_assert!(!arg_out.is_null()); // SAFETY: @@ -71,36 +74,17 @@ pub(crate) trait PassByValue: Sized { /// Support for values passed to Rust by pointer. These are represented as opaque structs in C, /// and always handled as pointers. -/// -/// # Safety -/// -/// The functions provided by this trait are used directly in C interface functions, and make the -/// following expectations of the C code: -/// -/// - When passing a value to Rust (via the `…arg…` functions), -/// - the pointer must not be NULL; -/// - the pointer must be one previously returned from Rust; and -/// - the memory addressed by the pointer must never be modified by C code. -/// - For `from_ptr_arg_ref`, the value must not be modified during the call to the Rust function -/// - For `from_ptr_arg_ref_mut`, the value must not be accessed (read or write) during the call -/// (these last two points are trivially ensured by all TC… types being non-threadsafe) -/// - For `take_from_ptr_arg`, the pointer becomes invalid and must not be used in _any way_ after it -/// is passed to the Rust function. -/// - For `return_ptr` and `ptr_to_arg_out`, it is the C caller's responsibility to later free the value. -/// - For `ptr_to_arg_out`, `arg_out` must not be NULL and must be properly aligned and pointing to -/// valid memory. -/// -/// These requirements should be expressed in the C documentation for the type implementing this -/// trait. pub(crate) trait PassByPointer: Sized { /// Take a value from C as an argument. /// /// # Safety /// - /// See trait documentation. + /// - arg must not be NULL + /// - arg must be a value returned from Box::into_raw (via return_ptr or ptr_to_arg_out) + /// - arg becomes invalid and must not be used after this call unsafe fn take_from_ptr_arg(arg: *mut Self) -> Self { debug_assert!(!arg.is_null()); - // SAFETY: see trait documentation + // SAFETY: see docstring unsafe { *(Box::from_raw(arg)) } } @@ -108,10 +92,13 @@ pub(crate) trait PassByPointer: Sized { /// /// # Safety /// - /// See trait documentation. + /// - arg must not be NULL + /// - *arg must be a valid instance of Self + /// - arg must be valid for the lifetime assigned by the caller + /// - arg must not be modified by anything else during that lifetime unsafe fn from_ptr_arg_ref<'a>(arg: *const Self) -> &'a Self { debug_assert!(!arg.is_null()); - // SAFETY: see trait documentation + // SAFETY: see docstring unsafe { &*arg } } @@ -119,10 +106,13 @@ pub(crate) trait PassByPointer: Sized { /// /// # Safety /// - /// See trait documentation. + /// - arg must not be NULL + /// - *arg must be a valid instance of Self + /// - arg must be valid for the lifetime assigned by the caller + /// - arg must not be accessed by anything else during that lifetime unsafe fn from_ptr_arg_ref_mut<'a>(arg: *mut Self) -> &'a mut Self { debug_assert!(!arg.is_null()); - // SAFETY: see trait documentation + // SAFETY: see docstring unsafe { &mut *arg } } @@ -130,7 +120,7 @@ pub(crate) trait PassByPointer: Sized { /// /// # Safety /// - /// See trait documentation. + /// - the caller must ensure that the value is eventually freed unsafe fn return_ptr(self) -> *mut Self { Box::into_raw(Box::new(self)) } @@ -139,31 +129,31 @@ pub(crate) trait PassByPointer: Sized { /// /// # Safety /// - /// See trait documentation. + /// - the caller must ensure that the value is eventually freed + /// - arg_out must not be NULL + /// - arg_out must point to valid, properly aligned memory for a pointer value unsafe fn ptr_to_arg_out(self, arg_out: *mut *mut Self) { - // SAFETY: see trait documentation - unsafe { - *arg_out = self.return_ptr(); - } + debug_assert!(!arg_out.is_null()); + // SAFETY: see docstring + unsafe { *arg_out = self.return_ptr() }; } } /// Support for C lists of objects referenced by value. /// /// The underlying C type should have three fields, containing items, length, and capacity. The -/// required trait functions just fetch and set these fields. The PassByValue trait will be -/// implemented automatically, converting between the C type and `Vec`. For most cases, -/// it is only necessary to implement `tc_.._free` that first calls -/// `PassByValue::take_val_from_arg(arg, CList::null_value())` to take the existing value and -/// replace it with the null value; then one of hte `drop_.._list(..)` functions to drop the resulting -/// vector and all of the objects it points to. +/// required trait functions just fetch and set these fields. /// -/// This can be used for objects referenced by pointer, too, with an Element type of `NonNull` +/// The PassByValue trait will be implemented automatically, converting between the C type and +/// `Vec`. +/// +/// For most cases, it is only necessary to implement `tc_.._free` that calls either +/// drop_value_list (if Element is PassByValue) or drop_pointer_list (if element is PassByPointer). /// /// # Safety /// /// The C type must be documented as read-only. None of the fields may be modified, nor anything -/// in the `items` array. +/// accessible via the `items` array. /// /// This class guarantees that the items pointer is non-NULL for any valid list (even when len=0). pub(crate) trait CList: Sized { @@ -209,14 +199,14 @@ where debug_assert!(!list.is_null()); // SAFETY: - // - *list is a valid CL (caller promises to treat it as read-only) + // - *list is a valid CL (promised by caller) let mut vec = unsafe { CL::take_val_from_arg(list, CL::null_value()) }; // first, drop each of the elements in turn for e in vec.drain(..) { // SAFETY: - // - e is a valid Element (caller promisd not to change it) - // - Vec::drain has invalidated this entry (value is owned) + // - e is a valid Element (promised by caller) + // - e is owned drop(unsafe { PassByValue::val_from_arg(e) }); } // then drop the vector @@ -239,14 +229,14 @@ where { debug_assert!(!list.is_null()); // SAFETY: - // - *list is a valid CL (caller promises to treat it as read-only) + // - *list is a valid CL (promised by caller) let mut vec = unsafe { CL::take_val_from_arg(list, CL::null_value()) }; // first, drop each of the elements in turn for e in vec.drain(..) { // SAFETY: - // - e is a valid Element (caller promised not to change it) - // - Vec::drain has invalidated this entry (value is owned) + // - e is a valid Element (promised by caller) + // - e is owned drop(unsafe { PassByPointer::take_from_ptr_arg(e.as_ptr()) }); } // then drop the vector From 51a854cfeff998c4a6dd4dd54687bf9caf476384 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 13 Feb 2022 03:30:02 +0000 Subject: [PATCH 74/95] address some clippy lints --- lib/src/annotation.rs | 2 +- lib/src/lib.rs | 4 +++ lib/src/replica.rs | 13 ++++----- lib/src/task.rs | 62 +++++++++++++++++++++---------------------- lib/src/uuid.rs | 6 ++--- lib/src/workingset.rs | 4 +-- 6 files changed, 46 insertions(+), 45 deletions(-) diff --git a/lib/src/annotation.rs b/lib/src/annotation.rs index a1fe78da5..dede87431 100644 --- a/lib/src/annotation.rs +++ b/lib/src/annotation.rs @@ -20,7 +20,7 @@ impl PassByValue for TCAnnotation { // SAFETY: // - any time_t value is valid // - time_t is copy, so ownership is not important - let entry = unsafe { self.entry.val_from_arg() }.unwrap(); + let entry = unsafe { libc::time_t::val_from_arg(self.entry) }.unwrap(); // SAFETY: // - self.description is not NULL (field docstring) // - self.description came from return_ptr in as_ctype diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 4c66ec76d..b6a8f2eec 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -2,6 +2,10 @@ // Not working yet in stable - https://github.com/rust-lang/rust-clippy/issues/8020 // #![warn(clippy::undocumented_unsafe_blocks)] +// docstrings for extern "C" functions are reflected into C, and do not benefit +// from safety docs. +#![allow(clippy::missing_safety_doc)] + mod traits; mod util; diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 655120669..314f8d375 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -103,7 +103,7 @@ pub unsafe extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica { /// Create a new TCReplica with an on-disk database having the given filename. On error, a string /// is written to the `error_out` parameter (if it is not NULL) and NULL is returned. #[no_mangle] -pub unsafe extern "C" fn tc_replica_new_on_disk<'a>( +pub unsafe extern "C" fn tc_replica_new_on_disk( path: *mut TCString, error_out: *mut *mut TCString, ) -> *mut TCReplica { @@ -169,7 +169,7 @@ pub unsafe extern "C" fn tc_replica_all_task_uuids(rep: *mut TCReplica) -> TCUui let uuids: Vec<_> = rep .all_task_uuids()? .drain(..) - .map(|uuid| TCUuid::return_val(uuid)) + .map(TCUuid::return_val) .collect(); Ok(TCUuidList::return_val(uuids)) }, @@ -265,10 +265,7 @@ pub unsafe extern "C" fn tc_replica_import_task_with_uuid( /// If undone_out is not NULL, then on success it is set to 1 if operations were undone, or 0 if /// there are no operations that can be done. #[no_mangle] -pub unsafe extern "C" fn tc_replica_undo<'a>( - rep: *mut TCReplica, - undone_out: *mut i32, -) -> TCResult { +pub unsafe extern "C" fn tc_replica_undo(rep: *mut TCReplica, undone_out: *mut i32) -> TCResult { wrap( rep, |rep| { @@ -323,9 +320,9 @@ pub unsafe extern "C" fn tc_replica_rebuild_working_set( /// to this function will return NULL. The rep pointer must not be NULL. The caller must free the /// returned string. #[no_mangle] -pub unsafe extern "C" fn tc_replica_error<'a>(rep: *mut TCReplica) -> *mut TCString<'static> { +pub unsafe extern "C" fn tc_replica_error(rep: *mut TCReplica) -> *mut TCString<'static> { // SAFETY: see type docstring - let rep: &'a mut TCReplica = unsafe { TCReplica::from_ptr_arg_ref_mut(rep) }; + let rep: &mut TCReplica = unsafe { TCReplica::from_ptr_arg_ref_mut(rep) }; if let Some(tcstring) = rep.error.take() { // SAFETY: see TCString docstring unsafe { tcstring.return_ptr() } diff --git a/lib/src/task.rs b/lib/src/task.rs index 24dc1fcfc..5a7a446fc 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -103,15 +103,15 @@ impl From for TCTask { /// Utility function to get a shared reference to the underlying Task. All Task getters /// are error-free, so this does not handle errors. -fn wrap<'a, T, F>(task: *mut TCTask, f: F) -> T +fn wrap(task: *mut TCTask, f: F) -> T where F: FnOnce(&Task) -> T, { // SAFETY: // - task is not null (promised by caller) - // - task outlives 'a (promised by caller) - let tctask: &'a mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; - let task: &'a Task = match &tctask.inner { + // - task outlives this function (promised by caller) + let tctask: &mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; + let task: &Task = match &tctask.inner { Inner::Immutable(t) => t, Inner::Mutable(t, _) => t.deref(), Inner::Invalid => unreachable!(), @@ -123,15 +123,15 @@ where /// Utility function to get a mutable reference to the underlying Task. The /// TCTask must be mutable. The inner function may use `?` syntax to return an /// error, which will be represented with the `err_value` returned to C. -fn wrap_mut<'a, T, F>(task: *mut TCTask, f: F, err_value: T) -> T +fn wrap_mut(task: *mut TCTask, f: F, err_value: T) -> T where F: FnOnce(&mut TaskMut) -> anyhow::Result, { // SAFETY: // - task is not null (promised by caller) - // - task outlives 'a (promised by caller) - let tctask: &'a mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; - let task: &'a mut TaskMut = match tctask.inner { + // - task outlives this function (promised by caller) + let tctask: &mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; + let task: &mut TaskMut = match tctask.inner { Inner::Immutable(_) => panic!("Task is immutable"), Inner::Mutable(ref mut t, _) => t, Inner::Invalid => unreachable!(), @@ -205,11 +205,11 @@ impl CList for TCTaskList { /// if (!success) { ... } /// ``` #[no_mangle] -pub unsafe extern "C" fn tc_task_to_mut<'a>(task: *mut TCTask, tcreplica: *mut TCReplica) { +pub unsafe extern "C" fn tc_task_to_mut(task: *mut TCTask, tcreplica: *mut TCReplica) { // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) - let tctask: &'a mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; + let tctask: &mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; // SAFETY: // - tcreplica is not NULL (promised by caller) // - tcreplica lives until later call to to_immut via tc_task_to_immut (promised by caller, @@ -223,11 +223,11 @@ pub unsafe extern "C" fn tc_task_to_mut<'a>(task: *mut TCTask, tcreplica: *mut T /// /// The replica passed to `tc_task_to_mut` may be used freely after this call. #[no_mangle] -pub unsafe extern "C" fn tc_task_to_immut<'a>(task: *mut TCTask) { +pub unsafe extern "C" fn tc_task_to_immut(task: *mut TCTask) { // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) - let tctask: &'a mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; + let tctask: &mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; tctask.to_immut(); } @@ -239,7 +239,7 @@ pub unsafe extern "C" fn tc_task_get_uuid(task: *mut TCTask) -> TCUuid { /// Get a task's status. #[no_mangle] -pub unsafe extern "C" fn tc_task_get_status<'a>(task: *mut TCTask) -> TCStatus { +pub unsafe extern "C" fn tc_task_get_status(task: *mut TCTask) -> TCStatus { wrap(task, |task| task.get_status().into()) } @@ -265,7 +265,7 @@ pub unsafe extern "C" fn tc_task_get_taskmap(task: *mut TCTask) -> TCKVList { /// Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it /// contains embedded NUL characters). #[no_mangle] -pub unsafe extern "C" fn tc_task_get_description<'a>(task: *mut TCTask) -> *mut TCString<'static> { +pub unsafe extern "C" fn tc_task_get_description(task: *mut TCTask) -> *mut TCString<'static> { wrap(task, |task| { let descr: TCString = task.get_description().into(); // SAFETY: see TCString docstring @@ -275,19 +275,19 @@ pub unsafe extern "C" fn tc_task_get_description<'a>(task: *mut TCTask) -> *mut /// Get the entry timestamp for a task (when it was created), or 0 if not set. #[no_mangle] -pub unsafe extern "C" fn tc_task_get_entry<'a>(task: *mut TCTask) -> libc::time_t { +pub unsafe extern "C" fn tc_task_get_entry(task: *mut TCTask) -> libc::time_t { wrap(task, |task| libc::time_t::as_ctype(task.get_entry())) } /// Get the wait timestamp for a task, or 0 if not set. #[no_mangle] -pub unsafe extern "C" fn tc_task_get_wait<'a>(task: *mut TCTask) -> libc::time_t { +pub unsafe extern "C" fn tc_task_get_wait(task: *mut TCTask) -> libc::time_t { wrap(task, |task| libc::time_t::as_ctype(task.get_wait())) } /// Get the modified timestamp for a task, or 0 if not set. #[no_mangle] -pub unsafe extern "C" fn tc_task_get_modified<'a>(task: *mut TCTask) -> libc::time_t { +pub unsafe extern "C" fn tc_task_get_modified(task: *mut TCTask) -> libc::time_t { wrap(task, |task| libc::time_t::as_ctype(task.get_modified())) } @@ -306,7 +306,7 @@ pub unsafe extern "C" fn tc_task_is_active(task: *mut TCTask) -> bool { /// Check if a task has the given tag. If the tag is invalid, this function will return false, as /// that (invalid) tag is not present. No error will be reported via `tc_task_error`. #[no_mangle] -pub unsafe extern "C" fn tc_task_has_tag<'a>(task: *mut TCTask, tag: *mut TCString) -> bool { +pub unsafe extern "C" fn tc_task_has_tag(task: *mut TCTask, tag: *mut TCString) -> bool { // SAFETY: see TCString docstring let tcstring = unsafe { TCString::take_from_ptr_arg(tag) }; wrap(task, |task| { @@ -323,7 +323,7 @@ pub unsafe extern "C" fn tc_task_has_tag<'a>(task: *mut TCTask, tag: *mut TCStri /// The caller must free the returned TCStringList instance. The TCStringList instance does not /// reference the task and the two may be freed in any order. #[no_mangle] -pub unsafe extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCStringList { +pub unsafe extern "C" fn tc_task_get_tags(task: *mut TCTask) -> TCStringList { wrap(task, |task| { let vec: Vec>> = task .get_tags() @@ -344,7 +344,7 @@ pub unsafe extern "C" fn tc_task_get_tags<'a>(task: *mut TCTask) -> TCStringList /// The caller must free the returned TCAnnotationList instance. The TCStringList instance does not /// reference the task and the two may be freed in any order. #[no_mangle] -pub unsafe extern "C" fn tc_task_get_annotations<'a>(task: *mut TCTask) -> TCAnnotationList { +pub unsafe extern "C" fn tc_task_get_annotations(task: *mut TCTask) -> TCAnnotationList { wrap(task, |task| { let vec: Vec = task .get_annotations() @@ -404,7 +404,7 @@ pub unsafe extern "C" fn tc_task_get_legacy_uda<'a>( /// /// Legacy UDAs are represented with an empty string in the ns field. #[no_mangle] -pub unsafe extern "C" fn tc_task_get_udas<'a>(task: *mut TCTask) -> TCUDAList { +pub unsafe extern "C" fn tc_task_get_udas(task: *mut TCTask) -> TCUDAList { wrap(task, |task| { let vec: Vec = task .get_udas() @@ -425,7 +425,7 @@ pub unsafe extern "C" fn tc_task_get_udas<'a>(task: *mut TCTask) -> TCUDAList { /// All TCUDAs in this list have a NULL ns field. The entire UDA key is /// included in the key field. #[no_mangle] -pub unsafe extern "C" fn tc_task_get_legacy_udas<'a>(task: *mut TCTask) -> TCUDAList { +pub unsafe extern "C" fn tc_task_get_legacy_udas(task: *mut TCTask) -> TCUDAList { wrap(task, |task| { let vec: Vec = task .get_legacy_udas() @@ -443,7 +443,7 @@ pub unsafe extern "C" fn tc_task_get_legacy_udas<'a>(task: *mut TCTask) -> TCUDA /// Set a mutable task's status. #[no_mangle] -pub unsafe extern "C" fn tc_task_set_status<'a>(task: *mut TCTask, status: TCStatus) -> TCResult { +pub unsafe extern "C" fn tc_task_set_status(task: *mut TCTask, status: TCStatus) -> TCResult { wrap_mut( task, |task| { @@ -456,7 +456,7 @@ pub unsafe extern "C" fn tc_task_set_status<'a>(task: *mut TCTask, status: TCSta /// Set a mutable task's description. #[no_mangle] -pub unsafe extern "C" fn tc_task_set_description<'a>( +pub unsafe extern "C" fn tc_task_set_description( task: *mut TCTask, description: *mut TCString, ) -> TCResult { @@ -640,7 +640,7 @@ pub unsafe extern "C" fn tc_task_remove_annotation(task: *mut TCTask, entry: i64 /// Set a UDA on a mutable task. #[no_mangle] -pub unsafe extern "C" fn tc_task_set_uda<'a>( +pub unsafe extern "C" fn tc_task_set_uda( task: *mut TCTask, ns: *mut TCString, key: *mut TCString, @@ -668,7 +668,7 @@ pub unsafe extern "C" fn tc_task_set_uda<'a>( /// Remove a UDA fraom a mutable task. #[no_mangle] -pub unsafe extern "C" fn tc_task_remove_uda<'a>( +pub unsafe extern "C" fn tc_task_remove_uda( task: *mut TCTask, ns: *mut TCString, key: *mut TCString, @@ -689,7 +689,7 @@ pub unsafe extern "C" fn tc_task_remove_uda<'a>( /// Set a legacy UDA on a mutable task. #[no_mangle] -pub unsafe extern "C" fn tc_task_set_legacy_uda<'a>( +pub unsafe extern "C" fn tc_task_set_legacy_uda( task: *mut TCTask, key: *mut TCString, value: *mut TCString, @@ -710,7 +710,7 @@ pub unsafe extern "C" fn tc_task_set_legacy_uda<'a>( /// Remove a UDA fraom a mutable task. #[no_mangle] -pub unsafe extern "C" fn tc_task_remove_legacy_uda<'a>( +pub unsafe extern "C" fn tc_task_remove_legacy_uda( task: *mut TCTask, key: *mut TCString, ) -> TCResult { @@ -730,11 +730,11 @@ pub unsafe extern "C" fn tc_task_remove_legacy_uda<'a>( /// to this function will return NULL. The task pointer must not be NULL. The caller must free the /// returned string. #[no_mangle] -pub unsafe extern "C" fn tc_task_error<'a>(task: *mut TCTask) -> *mut TCString<'static> { +pub unsafe extern "C" fn tc_task_error(task: *mut TCTask) -> *mut TCString<'static> { // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) - let task: &'a mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; + let task: &mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; if let Some(tcstring) = task.error.take() { unsafe { tcstring.return_ptr() } } else { @@ -747,7 +747,7 @@ pub unsafe extern "C" fn tc_task_error<'a>(task: *mut TCTask) -> *mut TCString<' /// /// If the task is currently mutable, it will first be made immutable. #[no_mangle] -pub unsafe extern "C" fn tc_task_free<'a>(task: *mut TCTask) { +pub unsafe extern "C" fn tc_task_free(task: *mut TCTask) { // SAFETY: // - rep is not NULL (promised by caller) // - caller will not use the TCTask after this (promised by caller) diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index f3d0528b3..9496e8b0b 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -75,7 +75,7 @@ impl CList for TCUuidList { /// Write the string representation of a TCUuid into the given buffer, which must be /// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. #[no_mangle] -pub unsafe extern "C" fn tc_uuid_to_buf<'a>(tcuuid: TCUuid, buf: *mut libc::c_char) { +pub unsafe extern "C" fn tc_uuid_to_buf(tcuuid: TCUuid, buf: *mut libc::c_char) { debug_assert!(!buf.is_null()); // SAFETY: // - buf is valid for len bytes (by C convention) @@ -83,7 +83,7 @@ pub unsafe extern "C" fn tc_uuid_to_buf<'a>(tcuuid: TCUuid, buf: *mut libc::c_ch // - content of buf will not be mutated during the lifetime of this slice (lifetime // does not outlive this function call) // - the length of the buffer is less than isize::MAX (promised by caller) - let buf: &'a mut [u8] = unsafe { + let buf: &mut [u8] = unsafe { std::slice::from_raw_parts_mut(buf as *mut u8, ::uuid::adapter::Hyphenated::LENGTH) }; // SAFETY: @@ -107,7 +107,7 @@ pub unsafe extern "C" fn tc_uuid_to_str(tcuuid: TCUuid) -> *mut TCString<'static /// Parse the given string as a UUID. Returns TC_RESULT_ERROR on parse failure or if the given /// string is not valid. #[no_mangle] -pub unsafe extern "C" fn tc_uuid_from_str<'a>(s: *mut TCString, uuid_out: *mut TCUuid) -> TCResult { +pub unsafe extern "C" fn tc_uuid_from_str(s: *mut TCString, uuid_out: *mut TCUuid) -> TCResult { debug_assert!(!s.is_null()); debug_assert!(!uuid_out.is_null()); // SAFETY: see TCString docstring diff --git a/lib/src/workingset.rs b/lib/src/workingset.rs index 02780e50f..6e1e15b0f 100644 --- a/lib/src/workingset.rs +++ b/lib/src/workingset.rs @@ -18,14 +18,14 @@ impl From for TCWorkingSet { } /// Utility function to get a shared reference to the underlying WorkingSet. -fn wrap<'a, T, F>(ws: *mut TCWorkingSet, f: F) -> T +fn wrap(ws: *mut TCWorkingSet, f: F) -> T where F: FnOnce(&WorkingSet) -> T, { // SAFETY: // - ws is not null (promised by caller) // - ws outlives 'a (promised by caller) - let tcws: &'a TCWorkingSet = unsafe { TCWorkingSet::from_ptr_arg_ref(ws) }; + let tcws: &TCWorkingSet = unsafe { TCWorkingSet::from_ptr_arg_ref(ws) }; f(&tcws.0) } From ad464c4779b54c07931b9d57b335db9bc4f9c1e9 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 13 Feb 2022 03:33:43 +0000 Subject: [PATCH 75/95] use Uda instead of UDA --- integration-tests/src/bindings_tests/task.c | 2 +- lib/src/lib.rs | 2 +- lib/src/task.rs | 18 +++---- lib/src/uda.rs | 56 ++++++++++----------- lib/taskchampion.h | 34 ++++++------- 5 files changed, 56 insertions(+), 56 deletions(-) diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 7f89ddc51..b2412f3c3 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -400,7 +400,7 @@ static void test_task_udas(void) { tc_task_to_mut(task, rep); TCString *value; - TCUDAList udas; + TCUdaList udas; TEST_ASSERT_NULL(tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1"))); TEST_ASSERT_NULL(tc_task_get_legacy_uda(task, tc_string_borrow("leg1"))); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index b6a8f2eec..39fab8534 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -29,7 +29,7 @@ pub(crate) mod types { pub(crate) use crate::status::TCStatus; pub(crate) use crate::string::{TCString, TCStringList}; pub(crate) use crate::task::{TCTask, TCTaskList}; - pub(crate) use crate::uda::{TCUDAList, TCUDA, UDA}; + pub(crate) use crate::uda::{TCUda, TCUdaList, Uda}; pub(crate) use crate::uuid::{TCUuid, TCUuidList}; pub(crate) use crate::workingset::TCWorkingSet; } diff --git a/lib/src/task.rs b/lib/src/task.rs index 5a7a446fc..1a4b66157 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -404,40 +404,40 @@ pub unsafe extern "C" fn tc_task_get_legacy_uda<'a>( /// /// Legacy UDAs are represented with an empty string in the ns field. #[no_mangle] -pub unsafe extern "C" fn tc_task_get_udas(task: *mut TCTask) -> TCUDAList { +pub unsafe extern "C" fn tc_task_get_udas(task: *mut TCTask) -> TCUdaList { wrap(task, |task| { - let vec: Vec = task + let vec: Vec = task .get_udas() .map(|((ns, key), value)| { - TCUDA::return_val(UDA { + TCUda::return_val(Uda { ns: Some(ns.into()), key: key.into(), value: value.into(), }) }) .collect(); - TCUDAList::return_val(vec) + TCUdaList::return_val(vec) }) } /// Get all UDAs for this task. /// -/// All TCUDAs in this list have a NULL ns field. The entire UDA key is +/// All TCUdas in this list have a NULL ns field. The entire UDA key is /// included in the key field. #[no_mangle] -pub unsafe extern "C" fn tc_task_get_legacy_udas(task: *mut TCTask) -> TCUDAList { +pub unsafe extern "C" fn tc_task_get_legacy_udas(task: *mut TCTask) -> TCUdaList { wrap(task, |task| { - let vec: Vec = task + let vec: Vec = task .get_legacy_udas() .map(|(key, value)| { - TCUDA::return_val(UDA { + TCUda::return_val(Uda { ns: None, key: key.into(), value: value.into(), }) }) .collect(); - TCUDAList::return_val(vec) + TCUdaList::return_val(vec) }) } diff --git a/lib/src/uda.rs b/lib/src/uda.rs index 4c0fe03f2..3712932ad 100644 --- a/lib/src/uda.rs +++ b/lib/src/uda.rs @@ -1,9 +1,9 @@ use crate::traits::*; use crate::types::*; -/// TCUDA contains the details of a UDA. +/// TCUda contains the details of a UDA. #[repr(C)] -pub struct TCUDA { +pub struct TCUda { /// Namespace of the UDA. For legacy UDAs, this is NULL. pub ns: *mut TCString<'static>, /// UDA key. Must not be NULL. @@ -12,17 +12,17 @@ pub struct TCUDA { pub value: *mut TCString<'static>, } -pub(crate) struct UDA { +pub(crate) struct Uda { pub ns: Option>, pub key: TCString<'static>, pub value: TCString<'static>, } -impl PassByValue for TCUDA { - type RustType = UDA; +impl PassByValue for TCUda { + type RustType = Uda; unsafe fn from_ctype(self) -> Self::RustType { - UDA { + Uda { ns: if self.ns.is_null() { None } else { @@ -42,8 +42,8 @@ impl PassByValue for TCUDA { } } - fn as_ctype(uda: UDA) -> Self { - TCUDA { + fn as_ctype(uda: Uda) -> Self { + TCUda { // SAFETY: caller assumes ownership of this value ns: if let Some(ns) = uda.ns { unsafe { ns.return_ptr() } @@ -58,9 +58,9 @@ impl PassByValue for TCUDA { } } -impl Default for TCUDA { +impl Default for TCUda { fn default() -> Self { - TCUDA { + TCUda { ns: std::ptr::null_mut(), key: std::ptr::null_mut(), value: std::ptr::null_mut(), @@ -68,27 +68,27 @@ impl Default for TCUDA { } } -/// TCUDAList represents a list of UDAs. +/// TCUdaList represents a list of UDAs. /// /// The content of this struct must be treated as read-only. #[repr(C)] -pub struct TCUDAList { +pub struct TCUdaList { /// number of UDAs in items len: libc::size_t, /// total size of items (internal use only) _capacity: libc::size_t, - /// array of UDAs. These remain owned by the TCUDAList instance and will be freed by - /// tc_uda_list_free. This pointer is never NULL for a valid TCUDAList. - items: *const TCUDA, + /// array of UDAs. These remain owned by the TCUdaList instance and will be freed by + /// tc_uda_list_free. This pointer is never NULL for a valid TCUdaList. + items: *const TCUda, } -impl CList for TCUDAList { - type Element = TCUDA; +impl CList for TCUdaList { + type Element = TCUda; unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { - TCUDAList { + TCUdaList { len, _capacity: cap, items, @@ -100,25 +100,25 @@ impl CList for TCUDAList { } } -/// Free a TCUDA instance. The instance, and the TCStrings it contains, must not be used +/// Free a TCUda instance. The instance, and the TCStrings it contains, must not be used /// after this call. #[no_mangle] -pub unsafe extern "C" fn tc_uda_free(tcuda: *mut TCUDA) { +pub unsafe extern "C" fn tc_uda_free(tcuda: *mut TCUda) { debug_assert!(!tcuda.is_null()); // SAFETY: - // - *tcuda is a valid TCUDA (caller promises to treat it as read-only) - let uda = unsafe { TCUDA::take_val_from_arg(tcuda, TCUDA::default()) }; + // - *tcuda is a valid TCUda (caller promises to treat it as read-only) + let uda = unsafe { TCUda::take_val_from_arg(tcuda, TCUda::default()) }; drop(uda); } -/// Free a TCUDAList instance. The instance, and all TCUDAs it contains, must not be used after +/// Free a TCUdaList instance. The instance, and all TCUdas it contains, must not be used after /// this call. /// -/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCUDAList. +/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCUdaList. #[no_mangle] -pub unsafe extern "C" fn tc_uda_list_free(tcudas: *mut TCUDAList) { +pub unsafe extern "C" fn tc_uda_list_free(tcudas: *mut TCUdaList) { // SAFETY: - // - tcudas is not NULL and points to a valid TCUDAList (caller is not allowed to + // - tcudas is not NULL and points to a valid TCUdaList (caller is not allowed to // modify the list) // - caller promises not to use the value after return unsafe { drop_value_list(tcudas) } @@ -130,7 +130,7 @@ mod test { #[test] fn empty_list_has_non_null_pointer() { - let tcudas = TCUDAList::return_val(Vec::new()); + let tcudas = TCUdaList::return_val(Vec::new()); assert!(!tcudas.items.is_null()); assert_eq!(tcudas.len, 0); assert_eq!(tcudas._capacity, 0); @@ -138,7 +138,7 @@ mod test { #[test] fn free_sets_null_pointer() { - let mut tcudas = TCUDAList::return_val(Vec::new()); + let mut tcudas = TCUdaList::return_val(Vec::new()); // SAFETY: testing expected behavior unsafe { tc_uda_list_free(&mut tcudas) }; assert!(tcudas.items.is_null()); diff --git a/lib/taskchampion.h b/lib/taskchampion.h index bb41d4e5a..d5f787a88 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -268,9 +268,9 @@ typedef struct TCStringList { } TCStringList; /** - * TCUDA contains the details of a UDA. + * TCUda contains the details of a UDA. */ -typedef struct TCUDA { +typedef struct TCUda { /** * Namespace of the UDA. For legacy UDAs, this is NULL. */ @@ -283,14 +283,14 @@ typedef struct TCUDA { * Content of the UDA. Must not be NULL. */ struct TCString *value; -} TCUDA; +} TCUda; /** - * TCUDAList represents a list of UDAs. + * TCUdaList represents a list of UDAs. * * The content of this struct must be treated as read-only. */ -typedef struct TCUDAList { +typedef struct TCUdaList { /** * number of UDAs in items */ @@ -300,11 +300,11 @@ typedef struct TCUDAList { */ size_t _capacity; /** - * array of UDAs. These remain owned by the TCUDAList instance and will be freed by - * tc_uda_list_free. This pointer is never NULL for a valid TCUDAList. + * array of UDAs. These remain owned by the TCUdaList instance and will be freed by + * tc_uda_list_free. This pointer is never NULL for a valid TCUdaList. */ - const struct TCUDA *items; -} TCUDAList; + const struct TCUda *items; +} TCUdaList; #ifdef __cplusplus extern "C" { @@ -612,15 +612,15 @@ struct TCString *tc_task_get_legacy_uda(struct TCTask *task, struct TCString *ke * * Legacy UDAs are represented with an empty string in the ns field. */ -struct TCUDAList tc_task_get_udas(struct TCTask *task); +struct TCUdaList tc_task_get_udas(struct TCTask *task); /** * Get all UDAs for this task. * - * All TCUDAs in this list have a NULL ns field. The entire UDA key is + * All TCUdas in this list have a NULL ns field. The entire UDA key is * included in the key field. */ -struct TCUDAList tc_task_get_legacy_udas(struct TCTask *task); +struct TCUdaList tc_task_get_legacy_udas(struct TCTask *task); /** * Set a mutable task's status. @@ -735,18 +735,18 @@ void tc_task_free(struct TCTask *task); void tc_task_list_free(struct TCTaskList *tctasks); /** - * Free a TCUDA instance. The instance, and the TCStrings it contains, must not be used + * Free a TCUda instance. The instance, and the TCStrings it contains, must not be used * after this call. */ -void tc_uda_free(struct TCUDA *tcuda); +void tc_uda_free(struct TCUda *tcuda); /** - * Free a TCUDAList instance. The instance, and all TCUDAs it contains, must not be used after + * Free a TCUdaList instance. The instance, and all TCUdas it contains, must not be used after * this call. * - * When this call returns, the `items` pointer will be NULL, signalling an invalid TCUDAList. + * When this call returns, the `items` pointer will be NULL, signalling an invalid TCUdaList. */ -void tc_uda_list_free(struct TCUDAList *tcudas); +void tc_uda_list_free(struct TCUdaList *tcudas); /** * Create a new, randomly-generated UUID. From fc73911cde7c1562316a4abe48c1e1bee19edb81 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 13 Feb 2022 03:37:50 +0000 Subject: [PATCH 76/95] fix some clippy::wrong_self_convention --- lib/src/string.rs | 26 +++++++++++++------------- lib/src/task.rs | 1 + lib/src/traits.rs | 1 + 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/src/string.rs b/lib/src/string.rs index 442ffd0e9..758f1d6e0 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -95,7 +95,7 @@ impl<'a> TCString<'a> { /// Convert the TCString, in place, into one of the C variants. If this is not /// possible, such as if the string contains an embedded NUL, then the string /// remains unchanged. - fn to_c_string(&mut self) { + fn to_c_string_mut(&mut self) { if matches!(self, TCString::String(_)) { // we must take ownership of the String in order to try converting it, // leaving the underlying TCString as its default (None) @@ -267,11 +267,11 @@ pub unsafe extern "C" fn tc_string_content(tcstring: *mut TCString) -> *const li // if we have a String, we need to consume it and turn it into // a CString. - tcstring.to_c_string(); + tcstring.to_c_string_mut(); match tcstring { TCString::CString(cstring) => cstring.as_ptr(), - TCString::String(_) => std::ptr::null(), // to_c_string failed + TCString::String(_) => std::ptr::null(), // to_c_string_mut failed TCString::CStr(cstr) => cstr.as_ptr(), TCString::InvalidUtf8(_, _) => std::ptr::null(), TCString::None => unreachable!(), @@ -427,37 +427,37 @@ mod test { } #[test] - fn cstring_to_c_string() { + fn cstring_to_c_string_mut() { let mut tcstring = make_cstring(); - tcstring.to_c_string(); + tcstring.to_c_string_mut(); assert_eq!(tcstring, make_cstring()); // unchanged } #[test] - fn cstr_to_c_string() { + fn cstr_to_c_string_mut() { let mut tcstring = make_cstr(); - tcstring.to_c_string(); + tcstring.to_c_string_mut(); assert_eq!(tcstring, make_cstr()); // unchanged } #[test] - fn string_to_c_string() { + fn string_to_c_string_mut() { let mut tcstring = make_string(); - tcstring.to_c_string(); + tcstring.to_c_string_mut(); assert_eq!(tcstring, make_cstring()); // converted to CString, same content } #[test] - fn string_with_nul_to_c_string() { + fn string_with_nul_to_c_string_mut() { let mut tcstring = make_string_with_nul(); - tcstring.to_c_string(); + tcstring.to_c_string_mut(); assert_eq!(tcstring, make_string_with_nul()); // unchanged } #[test] - fn invalid_to_c_string() { + fn invalid_to_c_string_mut() { let mut tcstring = make_invalid(); - tcstring.to_c_string(); + tcstring.to_c_string_mut(); assert_eq!(tcstring, make_invalid()); // unchanged } } diff --git a/lib/src/task.rs b/lib/src/task.rs index 1a4b66157..47dbbf3d1 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -74,6 +74,7 @@ impl TCTask { /// Make an mutable TCTask into a immutable TCTask. Does nothing if the task /// is already immutable. + #[allow(clippy::wrong_self_convention)] // to_immut_mut is not better! fn to_immut(&mut self) { self.inner = match std::mem::replace(&mut self.inner, Inner::Invalid) { Inner::Immutable(task) => Inner::Immutable(task), diff --git a/lib/src/traits.rs b/lib/src/traits.rs index 57537d212..e71ab0b9f 100644 --- a/lib/src/traits.rs +++ b/lib/src/traits.rs @@ -16,6 +16,7 @@ pub(crate) trait PassByValue: Sized { /// # Safety /// /// `self` must be a valid CType. + #[allow(clippy::wrong_self_convention)] unsafe fn from_ctype(self) -> Self::RustType; /// Convert a Rust value to a C value. From c0403f3f3823cbf67980e9dafd98abd10d3cdf8e Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 13 Feb 2022 16:18:17 +0000 Subject: [PATCH 77/95] fix bad test --- taskchampion/src/workingset.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/taskchampion/src/workingset.rs b/taskchampion/src/workingset.rs index 8bd0cce53..15a509753 100644 --- a/taskchampion/src/workingset.rs +++ b/taskchampion/src/workingset.rs @@ -110,7 +110,10 @@ mod test { #[test] fn test_largest_index() { - let (uuid1, uuid2, ws) = make(); + let uuid1 = Uuid::new_v4(); + let uuid2 = Uuid::new_v4(); + + let ws = WorkingSet::new(vec![]); assert_eq!(ws.largest_index(), 0); let ws = WorkingSet::new(vec![None, Some(uuid1)]); From 41a578ab2bf2554f940029d3eba847019342373a Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 13 Feb 2022 20:18:07 +0000 Subject: [PATCH 78/95] add server support --- integration-tests/.gitignore | 1 + .../src/bindings_tests/replica.c | 45 ++++++ lib/src/lib.rs | 2 + lib/src/replica.rs | 31 +++- lib/src/server.rs | 139 ++++++++++++++++++ lib/src/string.rs | 2 +- lib/taskchampion.h | 53 ++++++- 7 files changed, 267 insertions(+), 6 deletions(-) create mode 100644 lib/src/server.rs diff --git a/integration-tests/.gitignore b/integration-tests/.gitignore index e525de89a..140a35ffe 100644 --- a/integration-tests/.gitignore +++ b/integration-tests/.gitignore @@ -1 +1,2 @@ test-db +test-sync-server diff --git a/integration-tests/src/bindings_tests/replica.c b/integration-tests/src/bindings_tests/replica.c index 423d8dc25..bd15adfa9 100644 --- a/integration-tests/src/bindings_tests/replica.c +++ b/integration-tests/src/bindings_tests/replica.c @@ -1,4 +1,5 @@ #include +#include #include #include "unity.h" #include "taskchampion.h" @@ -153,6 +154,48 @@ static void test_replica_task_creation(void) { tc_replica_free(rep); } +// When tc_replica_undo is passed NULL for undone_out, it still succeeds +static void test_replica_sync_local(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + mkdir("test-sync-server", 0755); // ignore error, if dir already exists + + TCString *err = NULL; + TCServer *server = tc_server_new_local(tc_string_borrow("test-sync-server"), &err); + TEST_ASSERT_NOT_NULL(server); + TEST_ASSERT_NULL(err); + + int rv = tc_replica_sync(rep, server, false); + TEST_ASSERT_EQUAL(TC_RESULT_OK, rv); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + tc_server_free(server); + tc_replica_free(rep); + + // test error handling + server = tc_server_new_local(tc_string_borrow("/no/such/directory"), &err); + TEST_ASSERT_NULL(server); + TEST_ASSERT_NOT_NULL(err); + tc_string_free(err); +} + +// When tc_replica_undo is passed NULL for undone_out, it still succeeds +static void test_replica_remote_server(void) { + TCString *err = NULL; + TCServer *server = tc_server_new_remote( + tc_string_borrow("tc.freecinc.com"), + tc_uuid_new_v4(), + tc_string_borrow("\xf0\x28\x8c\x28"), // NOTE: not utf-8 + &err); + TEST_ASSERT_NOT_NULL(server); + TEST_ASSERT_NULL(err); + + // can't actually do anything with this server! + + tc_server_free(server); +} + // a replica with tasks in it returns an appropriate list of tasks and list of uuids static void test_replica_all_tasks(void) { TCReplica *rep = tc_replica_new_in_memory(); @@ -274,6 +317,8 @@ int replica_tests(void) { RUN_TEST(test_replica_working_set); RUN_TEST(test_replica_undo_empty_null_undone_out); RUN_TEST(test_replica_task_creation); + RUN_TEST(test_replica_sync_local); + RUN_TEST(test_replica_remote_server); RUN_TEST(test_replica_all_tasks); RUN_TEST(test_replica_task_import); RUN_TEST(test_replica_get_task_not_found); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 39fab8534..614d57f7b 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -14,6 +14,7 @@ pub mod atomic; pub mod kv; pub mod replica; pub mod result; +pub mod server; pub mod status; pub mod string; pub mod task; @@ -26,6 +27,7 @@ pub(crate) mod types { pub(crate) use crate::kv::{TCKVList, TCKV}; pub(crate) use crate::replica::TCReplica; pub(crate) use crate::result::TCResult; + pub(crate) use crate::server::TCServer; pub(crate) use crate::status::TCStatus; pub(crate) use crate::string::{TCString, TCStringList}; pub(crate) use crate::task::{TCTask, TCTaskList}; diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 314f8d375..423810645 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -100,8 +100,9 @@ pub unsafe extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica { unsafe { TCReplica::from(Replica::new(storage)).return_ptr() } } -/// Create a new TCReplica with an on-disk database having the given filename. On error, a string -/// is written to the `error_out` parameter (if it is not NULL) and NULL is returned. +/// Create a new TCReplica with an on-disk database having the given filename. On error, a string +/// is written to the error_out parameter (if it is not NULL) and NULL is returned. The caller +/// must free this string. #[no_mangle] pub unsafe extern "C" fn tc_replica_new_on_disk( path: *mut TCString, @@ -258,7 +259,31 @@ pub unsafe extern "C" fn tc_replica_import_task_with_uuid( ) } -// TODO: tc_replica_sync +/// Synchronize this replica with a server. +/// +/// The `server` argument remains owned by the caller, and must be freed explicitly. +#[no_mangle] +pub unsafe extern "C" fn tc_replica_sync( + rep: *mut TCReplica, + server: *mut TCServer, + avoid_snapshots: bool, +) -> TCResult { + wrap( + rep, + |rep| { + debug_assert!(!server.is_null()); + // SAFETY: + // - server is not NULL + // - *server is a valid TCServer (promised by caller) + // - server is valid for the lifetime of tc_replica_sync (not threadsafe) + // - server will not be accessed simultaneously (not threadsafe) + let server = unsafe { TCServer::from_ptr_arg_ref_mut(server) }; + rep.sync(server.as_mut(), avoid_snapshots)?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} /// Undo local operations until the most recent UndoPoint. /// diff --git a/lib/src/server.rs b/lib/src/server.rs new file mode 100644 index 000000000..d50650ec9 --- /dev/null +++ b/lib/src/server.rs @@ -0,0 +1,139 @@ +use crate::traits::*; +use crate::types::*; +use crate::util::err_to_tcstring; +use taskchampion::{Server, ServerConfig}; + +/// TCServer represents an interface to a sync server. Aside from new and free, a server +/// has no C-accessible API, but is designed to be passed to `tc_replica_sync`. +/// +/// ## Safety +/// +/// TCServer are not threadsafe, and must not be used with multiple replicas simultaneously. +pub struct TCServer(Box); + +impl PassByPointer for TCServer {} + +impl From> for TCServer { + fn from(server: Box) -> TCServer { + TCServer(server) + } +} + +impl AsMut> for TCServer { + fn as_mut(&mut self) -> &mut Box { + &mut self.0 + } +} + +/// Utility function to allow using `?` notation to return an error value. +fn wrap(f: F, error_out: *mut *mut TCString, err_value: T) -> T +where + F: FnOnce() -> anyhow::Result, +{ + match f() { + Ok(v) => v, + Err(e) => { + if !error_out.is_null() { + // SAFETY: + // - error_out is not NULL (checked) + // - ..and points to a valid pointer (promised by caller) + // - caller will free this string (promised by caller) + unsafe { + *error_out = err_to_tcstring(e).return_ptr(); + } + } + err_value + } + } +} + +/// Create a new TCServer that operates locally (on-disk). See the TaskChampion docs for the +/// description of the arguments. +/// +/// On error, a string is written to the error_out parameter (if it is not NULL) and NULL is +/// returned. The caller must free this string. +/// +/// The server must be freed after it is used - tc_replica_sync does not automatically free it. +#[no_mangle] +pub unsafe extern "C" fn tc_server_new_local( + server_dir: *mut TCString, + error_out: *mut *mut TCString, +) -> *mut TCServer { + wrap( + || { + // SAFETY: see TCString docstring + let server_dir = unsafe { TCString::take_from_ptr_arg(server_dir) }; + let server_config = ServerConfig::Local { + server_dir: server_dir.to_path_buf(), + }; + let server = server_config.into_server()?; + // SAFETY: caller promises to free this server. + Ok(unsafe { TCServer::return_ptr(server.into()) }) + }, + error_out, + std::ptr::null_mut(), + ) +} + +/// Create a new TCServer that connects to a remote server. See the TaskChampion docs for the +/// description of the arguments. +/// +/// On error, a string is written to the error_out parameter (if it is not NULL) and NULL is +/// returned. The caller must free this string. +/// +/// The server must be freed after it is used - tc_replica_sync does not automatically free it. +#[no_mangle] +pub unsafe extern "C" fn tc_server_new_remote( + origin: *mut TCString, + client_key: TCUuid, + encryption_secret: *mut TCString, + error_out: *mut *mut TCString, +) -> *mut TCServer { + wrap( + || { + debug_assert!(!origin.is_null()); + debug_assert!(!encryption_secret.is_null()); + // SAFETY: + // - origin is not NULL + // - origin is valid (promised by caller) + // - origin ownership is transferred to this function + let origin = unsafe { TCString::take_from_ptr_arg(origin) }.into_string()?; + + // SAFETY: + // - client_key is a valid Uuid (any 8-byte sequence counts) + + let client_key = unsafe { TCUuid::val_from_arg(client_key) }; + // SAFETY: + // - encryption_secret is not NULL + // - encryption_secret is valid (promised by caller) + // - encryption_secret ownership is transferred to this function + let encryption_secret = unsafe { TCString::take_from_ptr_arg(encryption_secret) } + .as_bytes() + .to_vec(); + + let server_config = ServerConfig::Remote { + origin, + client_key, + encryption_secret, + }; + let server = server_config.into_server()?; + // SAFETY: caller promises to free this server. + Ok(unsafe { TCServer::return_ptr(server.into()) }) + }, + error_out, + std::ptr::null_mut(), + ) +} + +/// Free a server. The server may not be used after this function returns and must not be freed +/// more than once. +#[no_mangle] +pub unsafe extern "C" fn tc_server_free(server: *mut TCServer) { + debug_assert!(!server.is_null()); + // SAFETY: + // - server is not NULL + // - server came from tc_server_new_.., which used return_ptr + // - server will not be used after (promised by caller) + let server = unsafe { TCServer::take_from_ptr_arg(server) }; + drop(server); +} diff --git a/lib/src/string.rs b/lib/src/string.rs index 758f1d6e0..f21f05c95 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -82,7 +82,7 @@ impl<'a> TCString<'a> { } } - fn as_bytes(&self) -> &[u8] { + pub(crate) fn as_bytes(&self) -> &[u8] { match self { TCString::CString(cstring) => cstring.as_bytes(), TCString::CStr(cstr) => cstr.to_bytes(), diff --git a/lib/taskchampion.h b/lib/taskchampion.h index d5f787a88..41edeedff 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -63,6 +63,16 @@ typedef enum TCStatus { */ typedef struct TCReplica TCReplica; +/** + * TCServer represents an interface to a sync server. Aside from new and free, a server + * has no C-accessible API, but is designed to be passed to `tc_replica_sync`. + * + * ## Safety + * + * TCServer are not threadsafe, and must not be used with multiple replicas simultaneously. + */ +typedef struct TCServer TCServer; + /** * TCString supports passing strings into and out of the TaskChampion API. * @@ -339,8 +349,9 @@ void tc_kv_list_free(struct TCKVList *tckvs); struct TCReplica *tc_replica_new_in_memory(void); /** - * Create a new TCReplica with an on-disk database having the given filename. On error, a string - * is written to the `error_out` parameter (if it is not NULL) and NULL is returned. + * Create a new TCReplica with an on-disk database having the given filename. On error, a string + * is written to the error_out parameter (if it is not NULL) and NULL is returned. The caller + * must free this string. */ struct TCReplica *tc_replica_new_on_disk(struct TCString *path, struct TCString **error_out); @@ -389,6 +400,13 @@ struct TCTask *tc_replica_new_task(struct TCReplica *rep, */ struct TCTask *tc_replica_import_task_with_uuid(struct TCReplica *rep, struct TCUuid tcuuid); +/** + * Synchronize this replica with a server. + * + * The `server` argument remains owned by the caller, and must be freed explicitly. + */ +TCResult tc_replica_sync(struct TCReplica *rep, struct TCServer *server, bool avoid_snapshots); + /** * Undo local operations until the most recent UndoPoint. * @@ -425,6 +443,37 @@ struct TCString *tc_replica_error(struct TCReplica *rep); */ void tc_replica_free(struct TCReplica *rep); +/** + * Create a new TCServer that operates locally (on-disk). See the TaskChampion docs for the + * description of the arguments. + * + * On error, a string is written to the error_out parameter (if it is not NULL) and NULL is + * returned. The caller must free this string. + * + * The server must be freed after it is used - tc_replica_sync does not automatically free it. + */ +struct TCServer *tc_server_new_local(struct TCString *server_dir, struct TCString **error_out); + +/** + * Create a new TCServer that connects to a remote server. See the TaskChampion docs for the + * description of the arguments. + * + * On error, a string is written to the error_out parameter (if it is not NULL) and NULL is + * returned. The caller must free this string. + * + * The server must be freed after it is used - tc_replica_sync does not automatically free it. + */ +struct TCServer *tc_server_new_remote(struct TCString *origin, + struct TCUuid client_key, + struct TCString *encryption_secret, + struct TCString **error_out); + +/** + * Free a server. The server may not be used after this function returns and must not be freed + * more than once. + */ +void tc_server_free(struct TCServer *server); + /** * Create a new TCString referencing the given C string. The C string must remain valid and * unchanged until after the TCString is freed. It's typically easiest to ensure this by using a From ca904d6288ed49e748b02b8981d079370b7a20e3 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 13 Feb 2022 21:02:18 +0000 Subject: [PATCH 79/95] improve output of C tests --- integration-tests/build.rs | 5 ++++ integration-tests/src/bindings_tests/mod.rs | 18 ++++++++++++-- .../src/bindings_tests/replica.c | 2 +- integration-tests/src/bindings_tests/test.c | 24 +++++++++++++++++++ integration-tests/tests/bindings.rs | 15 ++++++++++-- 5 files changed, 59 insertions(+), 5 deletions(-) diff --git a/integration-tests/build.rs b/integration-tests/build.rs index 5182784b3..e1c5c2fbe 100644 --- a/integration-tests/build.rs +++ b/integration-tests/build.rs @@ -18,6 +18,11 @@ fn build_libtaskchampion(suites: &[&'static str]) { build.object(libtaskchampion); build.include("../lib"); build.include("src/bindings_tests/unity"); + build.define("UNITY_OUTPUT_CHAR", "test_output"); + build.define( + "UNITY_OUTPUT_CHAR_HEADER_DECLARATION", + "test_output(char c)", + ); build.file("src/bindings_tests/unity/unity.c"); let mut files = vec!["src/bindings_tests/test.c".to_string()]; diff --git a/integration-tests/src/bindings_tests/mod.rs b/integration-tests/src/bindings_tests/mod.rs index 4eba5b421..327ca83fe 100644 --- a/integration-tests/src/bindings_tests/mod.rs +++ b/integration-tests/src/bindings_tests/mod.rs @@ -1,14 +1,28 @@ +use std::fs; + +extern "C" { + // set up to send test output to TEST-OUTPUT + fn setup_output(); + // close the output file + fn finish_output(); +} + // Each suite is represented by a _tests C function in .c. // All of these C files are built into a library that is linked to the crate -- but not to test // crates. So, this macro produces a "glue function" that calls the C function, and that can be // called from test crates. macro_rules! suite( { $s:ident } => { - pub fn $s() -> i32 { + pub fn $s() -> (i32, String) { extern "C" { fn $s() -> i32; } - unsafe { $s() } + unsafe { setup_output() }; + let res = unsafe { $s() }; + unsafe { finish_output() }; + let output = fs::read_to_string("TEST-OUTPUT") + .unwrap_or_else(|e| format!("could not open TEST-OUTPUT: {}", e)); + (res, output) } }; ); diff --git a/integration-tests/src/bindings_tests/replica.c b/integration-tests/src/bindings_tests/replica.c index bd15adfa9..282d2948b 100644 --- a/integration-tests/src/bindings_tests/replica.c +++ b/integration-tests/src/bindings_tests/replica.c @@ -1,8 +1,8 @@ #include #include #include -#include "unity.h" #include "taskchampion.h" +#include "unity.h" // creating an in-memory replica does not crash static void test_replica_creation(void) { diff --git a/integration-tests/src/bindings_tests/test.c b/integration-tests/src/bindings_tests/test.c index 609943fc4..5afa236ca 100644 --- a/integration-tests/src/bindings_tests/test.c +++ b/integration-tests/src/bindings_tests/test.c @@ -1,6 +1,30 @@ +#include #include "unity.h" // these functions are shared between all test "suites" // and cannot be customized per-suite. void setUp(void) { } void tearDown(void) { } + +static FILE *output = NULL; + +// Set up for test_output, writing output to "TEST-OUTPUT" in the +// current directory. The Rust test harness reads this file to get +// the output and display it only on failure. This is called by +// the Rust test harness +void setup_output(void) { + output = fopen("TEST-OUTPUT", "w"); +} + +// Close the output file. Called by the Rust test harness. +void finish_output(void) { + fclose(output); + output = NULL; +} + +// this replaces UNITY_OUTPUT_CHAR, and writes output to +// TEST-OUTPUT in the current directory; the Rust test harness +// will read this data if the test fails. +void test_output(char c) { + fputc(c, output); +} diff --git a/integration-tests/tests/bindings.rs b/integration-tests/tests/bindings.rs index 9f14ec47f..a121dd721 100644 --- a/integration-tests/tests/bindings.rs +++ b/integration-tests/tests/bindings.rs @@ -1,5 +1,6 @@ use lazy_static::lazy_static; use std::sync::Mutex; +use tempfile::TempDir; lazy_static! { // the C library running the tests is not reentrant, so we use a mutex to ensure that only one @@ -11,8 +12,18 @@ macro_rules! suite( { $s:ident } => { #[test] fn $s() { - let _guard = MUTEX.lock().unwrap(); - assert_eq!(integration_tests::bindings_tests::$s(), 0); + let tmp_dir = TempDir::new().expect("TempDir failed"); + let (res, output) = { + let _guard = MUTEX.lock().unwrap(); + // run the tests in the temp dir (NOTE: this must be inside + // the mutex guard!) + std::env::set_current_dir(tmp_dir.as_ref()).unwrap(); + integration_tests::bindings_tests::$s() + }; + println!("{}", output); + if res != 0 { + assert!(false, "test failed"); + } } }; ); From 8e34c107d5cdd934fe61b0516a40c67376bb17ee Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 13 Feb 2022 22:21:07 +0000 Subject: [PATCH 80/95] update safety comments --- lib/src/annotation.rs | 14 +++++++ lib/src/replica.rs | 72 ++++++++++++++++++++++++++--------- lib/src/server.rs | 5 ++- lib/src/string.rs | 13 ++++--- lib/src/task.rs | 88 ++++++++++++++++++++++++++++++++++--------- lib/src/uuid.rs | 12 ++++-- lib/taskchampion.h | 45 ++++++++++++++++++---- 7 files changed, 196 insertions(+), 53 deletions(-) diff --git a/lib/src/annotation.rs b/lib/src/annotation.rs index dede87431..7142eff62 100644 --- a/lib/src/annotation.rs +++ b/lib/src/annotation.rs @@ -3,6 +3,20 @@ use crate::types::*; use chrono::prelude::*; /// TCAnnotation contains the details of an annotation. +/// +/// # Safety +/// +/// An annotation must be initialized from a tc_.. function, and later freed +/// with `tc_annotation_free` or `tc_annotation_list_free`. +/// +/// Any function taking a `*TCAnnotation` requires: +/// - the pointer must not be NUL; +/// - the pointer must be one previously returned from a tc_… function; +/// - the memory referenced by the pointer must never be modified by C code; and +/// - ownership transfers to the called function, and the value must not be used +/// after the call returns. In fact, the value will be zeroed out to ensure this. +/// +/// TCAnnotations are not threadsafe. #[repr(C)] pub struct TCAnnotation { /// Time the annotation was made. Must be nonzero. diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 423810645..a35260b51 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -23,7 +23,7 @@ use taskchampion::{Replica, StorageConfig}; /// - the memory referenced by the pointer must never be modified by C code; and /// - except for `tc_replica_free`, ownership of a `*TCReplica` remains with the caller. /// -/// Once passed to `tc_replica_free`, a `*TCReplica` becmes invalid and must not be used again. +/// Once passed to `tc_replica_free`, a `*TCReplica` becomes invalid and must not be used again. /// /// TCReplicas are not threadsafe. pub struct TCReplica { @@ -74,7 +74,12 @@ fn wrap(rep: *mut TCReplica, f: F, err_value: T) -> T where F: FnOnce(&mut Replica) -> anyhow::Result, { - // SAFETY: see type docstring + debug_assert!(!rep.is_null()); + // SAFETY: + // - rep is not NULL (promised by caller) + // - *rep is a valid TCReplica (promised by caller) + // - rep is valid for the duration of this function + // - rep is not modified by anything else (not threadsafe) let rep: &mut TCReplica = unsafe { TCReplica::from_ptr_arg_ref_mut(rep) }; if rep.mut_borrowed { panic!("replica is borrowed and cannot be used"); @@ -90,13 +95,14 @@ where } /// Create a new TCReplica with an in-memory database. The contents of the database will be -/// lost when it is freed. +/// lost when it is freed with tc_replica_free. #[no_mangle] pub unsafe extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica { let storage = StorageConfig::InMemory .into_storage() .expect("in-memory always succeeds"); - // SAFETY: see type docstring + // SAFETY: + // - caller promises to free this value unsafe { TCReplica::from(Replica::new(storage)).return_ptr() } } @@ -108,7 +114,10 @@ pub unsafe extern "C" fn tc_replica_new_on_disk( path: *mut TCString, error_out: *mut *mut TCString, ) -> *mut TCReplica { - // SAFETY: see TCString docstring + // SAFETY: + // - path is not NULL (promised by caller) + // - path is return from a tc_string_.. so is valid + // - caller will not use path after this call (convention) let path = unsafe { TCString::take_from_ptr_arg(path) }; let storage_res = StorageConfig::OnDisk { taskdb_dir: path.to_path_buf(), @@ -120,6 +129,11 @@ pub unsafe extern "C" fn tc_replica_new_on_disk( Err(e) => { if !error_out.is_null() { unsafe { + // SAFETY: + // - return_ptr: caller promises to free this string + // - *error_out: + // - error_out is not NULL (checked) + // - error_out points to a valid place for a pointer (caller promises) *error_out = err_to_tcstring(e).return_ptr(); } } @@ -127,7 +141,8 @@ pub unsafe extern "C" fn tc_replica_new_on_disk( } }; - // SAFETY: see type docstring + // SAFETY: + // - caller promises to free this value unsafe { TCReplica::from(Replica::new(storage)).return_ptr() } } @@ -147,7 +162,8 @@ pub unsafe extern "C" fn tc_replica_all_tasks(rep: *mut TCReplica) -> TCTaskList .drain() .map(|(_uuid, t)| { NonNull::new( - // SAFETY: see TCTask docstring + // SAFETY: + // - caller promises to free this value (via freeing the list) unsafe { TCTask::from(t).return_ptr() }, ) .expect("TCTask::return_ptr returned NULL") @@ -178,7 +194,8 @@ pub unsafe extern "C" fn tc_replica_all_task_uuids(rep: *mut TCReplica) -> TCUui ) } -/// Get the current working set for this replica. +/// Get the current working set for this replica. The resulting value must be freed +/// with tc_working_set_free. /// /// Returns NULL on error. #[no_mangle] @@ -187,7 +204,8 @@ pub unsafe extern "C" fn tc_replica_working_set(rep: *mut TCReplica) -> *mut TCW rep, |rep| { let ws = rep.working_set()?; - // SAFETY: caller promises to free this task + // SAFETY: + // - caller promises to free this value Ok(unsafe { TCWorkingSet::return_ptr(ws.into()) }) }, std::ptr::null_mut(), @@ -203,10 +221,13 @@ pub unsafe extern "C" fn tc_replica_get_task(rep: *mut TCReplica, tcuuid: TCUuid wrap( rep, |rep| { - // SAFETY: see TCUuid docstring + // SAFETY: + // - tcuuid is a valid TCUuid (all bytes are valid) + // - tcuuid is Copy so ownership doesn't matter let uuid = unsafe { TCUuid::val_from_arg(tcuuid) }; if let Some(task) = rep.get_task(uuid)? { - // SAFETY: caller promises to free this task + // SAFETY: + // - caller promises to free this task Ok(unsafe { TCTask::from(task).return_ptr() }) } else { Ok(std::ptr::null_mut()) @@ -225,13 +246,17 @@ pub unsafe extern "C" fn tc_replica_new_task( status: TCStatus, description: *mut TCString, ) -> *mut TCTask { - // SAFETY: see TCString docstring + // SAFETY: + // - description is not NULL (promised by caller) + // - description is return from a tc_string_.. so is valid + // - caller will not use description after this call (convention) let description = unsafe { TCString::take_from_ptr_arg(description) }; wrap( rep, |rep| { let task = rep.new_task(status.into(), description.as_str()?.to_string())?; - // SAFETY: caller promises to free this task + // SAFETY: + // - caller promises to free this task Ok(unsafe { TCTask::from(task).return_ptr() }) }, std::ptr::null_mut(), @@ -249,10 +274,13 @@ pub unsafe extern "C" fn tc_replica_import_task_with_uuid( wrap( rep, |rep| { - // SAFETY: see TCUuid docstring + // SAFETY: + // - tcuuid is a valid TCUuid (all bytes are valid) + // - tcuuid is Copy so ownership doesn't matter let uuid = unsafe { TCUuid::val_from_arg(tcuuid) }; let task = rep.import_task_with_uuid(uuid)?; - // SAFETY: caller promises to free this task + // SAFETY: + // - caller promises to free this task Ok(unsafe { TCTask::from(task).return_ptr() }) }, std::ptr::null_mut(), @@ -346,10 +374,15 @@ pub unsafe extern "C" fn tc_replica_rebuild_working_set( /// returned string. #[no_mangle] pub unsafe extern "C" fn tc_replica_error(rep: *mut TCReplica) -> *mut TCString<'static> { - // SAFETY: see type docstring + // SAFETY: + // - rep is not NULL (promised by caller) + // - *rep is a valid TCReplica (promised by caller) + // - rep is valid for the duration of this function + // - rep is not modified by anything else (not threadsafe) let rep: &mut TCReplica = unsafe { TCReplica::from_ptr_arg_ref_mut(rep) }; if let Some(tcstring) = rep.error.take() { - // SAFETY: see TCString docstring + // SAFETY: + // - caller promises to free this string unsafe { tcstring.return_ptr() } } else { std::ptr::null_mut() @@ -360,7 +393,10 @@ pub unsafe extern "C" fn tc_replica_error(rep: *mut TCReplica) -> *mut TCString< /// more than once. #[no_mangle] pub unsafe extern "C" fn tc_replica_free(rep: *mut TCReplica) { - // SAFETY: see type docstring + // SAFETY: + // - replica is not NULL (promised by caller) + // - replica is valid (promised by caller) + // - caller will not use description after this call (promised by caller) let replica = unsafe { TCReplica::take_from_ptr_arg(rep) }; if replica.mut_borrowed { panic!("replica is borrowed and cannot be freed"); diff --git a/lib/src/server.rs b/lib/src/server.rs index d50650ec9..74feff34f 100644 --- a/lib/src/server.rs +++ b/lib/src/server.rs @@ -61,7 +61,10 @@ pub unsafe extern "C" fn tc_server_new_local( ) -> *mut TCServer { wrap( || { - // SAFETY: see TCString docstring + // SAFETY: + // - server_dir is not NULL (promised by caller) + // - server_dir is return from a tc_string_.. so is valid + // - caller will not use server_dir after this call (convention) let server_dir = unsafe { TCString::take_from_ptr_arg(server_dir) }; let server_config = ServerConfig::Local { server_dir: server_dir.to_path_buf(), diff --git a/lib/src/string.rs b/lib/src/string.rs index f21f05c95..3e0d4069e 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -23,7 +23,7 @@ use std::str::Utf8Error; /// # Safety /// /// When a `*TCString` appears as a return value or output argument, ownership is passed to the -/// caller. The caller must pass that ownerhsip back to another function or free the string. +/// caller. The caller must pass that ownership back to another function or free the string. /// /// Any function taking a `*TCReplica` requires: /// - the pointer must not be NUL; @@ -32,7 +32,7 @@ use std::str::Utf8Error; /// /// Unless specified otherwise, TaskChampion functions take ownership of a `*TCString` when it is /// given as a function argument, and the pointer is invalid when the function returns. Callers -/// must not use or free TCStringList after passing them to such API functions. +/// must not use or free TCStrings after passing them to such API functions. /// /// TCString is not threadsafe. #[derive(PartialEq, Debug)] @@ -193,7 +193,8 @@ pub unsafe extern "C" fn tc_string_borrow(cstr: *const libc::c_char) -> *mut TCS // - cstr contains a valid NUL terminator (promised by caller) // - cstr's content will not change before it is destroyed (promised by caller) let cstr: &CStr = unsafe { CStr::from_ptr(cstr) }; - // SAFETY: see docstring + // SAFETY: + // - caller promises to free this string unsafe { TCString::CStr(cstr).return_ptr() } } @@ -208,7 +209,8 @@ pub unsafe extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> *mut TCSt // - cstr contains a valid NUL terminator (promised by caller) // - cstr's content will not change before it is destroyed (by C convention) let cstr: &CStr = unsafe { CStr::from_ptr(cstr) }; - // SAFETY: see docstring + // SAFETY: + // - caller promises to free this string unsafe { TCString::CString(cstr.into()).return_ptr() } } @@ -245,7 +247,8 @@ pub unsafe extern "C" fn tc_string_clone_with_len( } }; - // SAFETY: see docstring + // SAFETY: + // - caller promises to free this string unsafe { tcstring.return_ptr() } } diff --git a/lib/src/task.rs b/lib/src/task.rs index 47dbbf3d1..ccea9fa20 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -23,6 +23,19 @@ use taskchampion::{Annotation, Tag, Task, TaskMut}; /// When a `tc_task_..` function that returns a TCResult returns TC_RESULT_ERROR, then /// `tc_task_error` will return the error message. /// +/// # Safety +/// +/// A task is an owned object, and must be freed with tc_task_free (or, if part of a list, +/// with tc_task_list_free). +/// +/// Any function taking a `*TCTask` requires: +/// - the pointer must not be NUL; +/// - the pointer must be one previously returned from a tc_… function; +/// - the memory referenced by the pointer must never be modified by C code; and +/// - except for `tc_{task,task_list}_free`, ownership of a `*TCTask` remains with the caller. +/// +/// Once passed to tc_task_free, a `*TCTask` becomes invalid and must not be used again. +/// /// TCTasks are not threadsafe. pub struct TCTask { /// The wrapped Task or TaskMut @@ -269,7 +282,8 @@ pub unsafe extern "C" fn tc_task_get_taskmap(task: *mut TCTask) -> TCKVList { pub unsafe extern "C" fn tc_task_get_description(task: *mut TCTask) -> *mut TCString<'static> { wrap(task, |task| { let descr: TCString = task.get_description().into(); - // SAFETY: see TCString docstring + // SAFETY: + // - caller promises to free this string unsafe { descr.return_ptr() } }) } @@ -308,7 +322,10 @@ pub unsafe extern "C" fn tc_task_is_active(task: *mut TCTask) -> bool { /// that (invalid) tag is not present. No error will be reported via `tc_task_error`. #[no_mangle] pub unsafe extern "C" fn tc_task_has_tag(task: *mut TCTask, tag: *mut TCString) -> bool { - // SAFETY: see TCString docstring + // SAFETY: + // - tag is not NULL (promised by caller) + // - tag is return from a tc_string_.. so is valid + // - caller will not use tag after this call (convention) let tcstring = unsafe { TCString::take_from_ptr_arg(tag) }; wrap(task, |task| { if let Ok(tag) = Tag::try_from(tcstring) { @@ -330,7 +347,8 @@ pub unsafe extern "C" fn tc_task_get_tags(task: *mut TCTask) -> TCStringList { .get_tags() .map(|t| { NonNull::new( - // SAFETY: see TCString docstring + // SAFETY: + // - this TCString will be freed via tc_string_list_free. unsafe { TCString::from(t.as_ref()).return_ptr() }, ) .expect("TCString::return_ptr() returned NULL") @@ -368,7 +386,12 @@ pub unsafe extern "C" fn tc_task_get_uda<'a>( key: *mut TCString<'a>, ) -> *mut TCString<'static> { wrap(task, |task| { + // SAFETY: + // - ns is not NULL (promised by caller) + // - ns is return from a tc_string_.. so is valid + // - caller will not use ns after this call (convention) if let Ok(ns) = unsafe { TCString::take_from_ptr_arg(ns) }.as_str() { + // SAFETY: same if let Ok(key) = unsafe { TCString::take_from_ptr_arg(key) }.as_str() { if let Some(value) = task.get_uda(ns, key) { // SAFETY: @@ -390,6 +413,10 @@ pub unsafe extern "C" fn tc_task_get_legacy_uda<'a>( key: *mut TCString<'a>, ) -> *mut TCString<'static> { wrap(task, |task| { + // SAFETY: + // - key is not NULL (promised by caller) + // - key is return from a tc_string_.. so is valid + // - caller will not use key after this call (convention) if let Ok(key) = unsafe { TCString::take_from_ptr_arg(key) }.as_str() { if let Some(value) = task.get_legacy_uda(key) { // SAFETY: @@ -461,7 +488,10 @@ pub unsafe extern "C" fn tc_task_set_description( task: *mut TCTask, description: *mut TCString, ) -> TCResult { - // SAFETY: see TCString docstring + // SAFETY: + // - description is not NULL (promised by caller) + // - description is return from a tc_string_.. so is valid + // - caller will not use description after this call (convention) let description = unsafe { TCString::take_from_ptr_arg(description) }; wrap_mut( task, @@ -577,7 +607,10 @@ pub unsafe extern "C" fn tc_task_delete(task: *mut TCTask) -> TCResult { /// Add a tag to a mutable task. #[no_mangle] pub unsafe extern "C" fn tc_task_add_tag(task: *mut TCTask, tag: *mut TCString) -> TCResult { - // SAFETY: see TCString docstring + // SAFETY: + // - tag is not NULL (promised by caller) + // - tag is return from a tc_string_.. so is valid + // - caller will not use tag after this call (convention) let tcstring = unsafe { TCString::take_from_ptr_arg(tag) }; wrap_mut( task, @@ -593,7 +626,10 @@ pub unsafe extern "C" fn tc_task_add_tag(task: *mut TCTask, tag: *mut TCString) /// Remove a tag from a mutable task. #[no_mangle] pub unsafe extern "C" fn tc_task_remove_tag(task: *mut TCTask, tag: *mut TCString) -> TCResult { - // SAFETY: see TCString docstring + // SAFETY: + // - tag is not NULL (promised by caller) + // - tag is return from a tc_string_.. so is valid + // - caller will not use tag after this call (convention) let tcstring = unsafe { TCString::take_from_ptr_arg(tag) }; wrap_mut( task, @@ -606,13 +642,17 @@ pub unsafe extern "C" fn tc_task_remove_tag(task: *mut TCTask, tag: *mut TCStrin ) } -/// Add an annotation to a mutable task. +/// Add an annotation to a mutable task. This call takes ownership of the +/// passed annotation, which must not be used after the call returns. #[no_mangle] pub unsafe extern "C" fn tc_task_add_annotation( task: *mut TCTask, annotation: *mut TCAnnotation, ) -> TCResult { - // SAFETY: see TCAnnotation docstring + // SAFETY: + // - annotation is not NULL (promised by caller) + // - annotation is return from a tc_string_.. so is valid + // - caller will not use annotation after this call let (entry, description) = unsafe { TCAnnotation::take_val_from_arg(annotation, TCAnnotation::default()) }; wrap_mut( @@ -647,11 +687,14 @@ pub unsafe extern "C" fn tc_task_set_uda( key: *mut TCString, value: *mut TCString, ) -> TCResult { - // SAFETY: see TCString docstring + // safety: + // - ns is not null (promised by caller) + // - ns is return from a tc_string_.. so is valid + // - caller will not use ns after this call (convention) let ns = unsafe { TCString::take_from_ptr_arg(ns) }; - // SAFETY: see TCString docstring + // SAFETY: same let key = unsafe { TCString::take_from_ptr_arg(key) }; - // SAFETY: see TCString docstring + // SAFETY: same let value = unsafe { TCString::take_from_ptr_arg(value) }; wrap_mut( task, @@ -674,9 +717,12 @@ pub unsafe extern "C" fn tc_task_remove_uda( ns: *mut TCString, key: *mut TCString, ) -> TCResult { - // SAFETY: see TCString docstring + // safety: + // - ns is not null (promised by caller) + // - ns is return from a tc_string_.. so is valid + // - caller will not use ns after this call (convention) let ns = unsafe { TCString::take_from_ptr_arg(ns) }; - // SAFETY: see TCString docstring + // SAFETY: same let key = unsafe { TCString::take_from_ptr_arg(key) }; wrap_mut( task, @@ -695,9 +741,12 @@ pub unsafe extern "C" fn tc_task_set_legacy_uda( key: *mut TCString, value: *mut TCString, ) -> TCResult { - // SAFETY: see TCString docstring + // safety: + // - key is not null (promised by caller) + // - key is return from a tc_string_.. so is valid + // - caller will not use key after this call (convention) let key = unsafe { TCString::take_from_ptr_arg(key) }; - // SAFETY: see TCString docstring + // SAFETY: same let value = unsafe { TCString::take_from_ptr_arg(value) }; wrap_mut( task, @@ -715,7 +764,10 @@ pub unsafe extern "C" fn tc_task_remove_legacy_uda( task: *mut TCTask, key: *mut TCString, ) -> TCResult { - // SAFETY: see TCString docstring + // safety: + // - key is not null (promised by caller) + // - key is return from a tc_string_.. so is valid + // - caller will not use key after this call (convention) let key = unsafe { TCString::take_from_ptr_arg(key) }; wrap_mut( task, @@ -737,6 +789,8 @@ pub unsafe extern "C" fn tc_task_error(task: *mut TCTask) -> *mut TCString<'stat // - task outlives 'a (promised by caller) let task: &mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; if let Some(tcstring) = task.error.take() { + // SAFETY: + // - caller promises to free this value unsafe { tcstring.return_ptr() } } else { std::ptr::null_mut() @@ -750,7 +804,7 @@ pub unsafe extern "C" fn tc_task_error(task: *mut TCTask) -> *mut TCString<'stat #[no_mangle] pub unsafe extern "C" fn tc_task_free(task: *mut TCTask) { // SAFETY: - // - rep is not NULL (promised by caller) + // - task is not NULL (promised by caller) // - caller will not use the TCTask after this (promised by caller) let mut tctask = unsafe { TCTask::take_from_ptr_arg(task) }; diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index 9496e8b0b..1bc300cf1 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -92,15 +92,16 @@ pub unsafe extern "C" fn tc_uuid_to_buf(tcuuid: TCUuid, buf: *mut libc::c_char) uuid.to_hyphenated().encode_lower(buf); } -/// Write the string representation of a TCUuid into the given buffer, which must be -/// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. +/// Return the hyphenated string representation of a TCUuid. The returned string +/// must be freed with tc_string_free. #[no_mangle] pub unsafe extern "C" fn tc_uuid_to_str(tcuuid: TCUuid) -> *mut TCString<'static> { // SAFETY: // - tcuuid is a valid TCUuid (all byte patterns are valid) let uuid: Uuid = unsafe { TCUuid::val_from_arg(tcuuid) }; let s = uuid.to_string(); - // SAFETY: see TCString docstring + // SAFETY: + // - caller promises to free this value. unsafe { TCString::from(s).return_ptr() } } @@ -110,7 +111,10 @@ pub unsafe extern "C" fn tc_uuid_to_str(tcuuid: TCUuid) -> *mut TCString<'static pub unsafe extern "C" fn tc_uuid_from_str(s: *mut TCString, uuid_out: *mut TCUuid) -> TCResult { debug_assert!(!s.is_null()); debug_assert!(!uuid_out.is_null()); - // SAFETY: see TCString docstring + // SAFETY: + // - s is not NULL (promised by caller) + // - s is return from a tc_string_.. so is valid + // - caller will not use s after this call (convention) let s = unsafe { TCString::take_from_ptr_arg(s) }; if let Ok(s) = s.as_str() { if let Ok(u) = Uuid::parse_str(s) { diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 41edeedff..0fd61ad9a 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -57,7 +57,7 @@ typedef enum TCStatus { * - the memory referenced by the pointer must never be modified by C code; and * - except for `tc_replica_free`, ownership of a `*TCReplica` remains with the caller. * - * Once passed to `tc_replica_free`, a `*TCReplica` becmes invalid and must not be used again. + * Once passed to `tc_replica_free`, a `*TCReplica` becomes invalid and must not be used again. * * TCReplicas are not threadsafe. */ @@ -92,7 +92,7 @@ typedef struct TCServer TCServer; * # Safety * * When a `*TCString` appears as a return value or output argument, ownership is passed to the - * caller. The caller must pass that ownerhsip back to another function or free the string. + * caller. The caller must pass that ownership back to another function or free the string. * * Any function taking a `*TCReplica` requires: * - the pointer must not be NUL; @@ -101,7 +101,7 @@ typedef struct TCServer TCServer; * * Unless specified otherwise, TaskChampion functions take ownership of a `*TCString` when it is * given as a function argument, and the pointer is invalid when the function returns. Callers - * must not use or free TCStringList after passing them to such API functions. + * must not use or free TCStrings after passing them to such API functions. * * TCString is not threadsafe. */ @@ -123,6 +123,19 @@ typedef struct TCString TCString; * When a `tc_task_..` function that returns a TCResult returns TC_RESULT_ERROR, then * `tc_task_error` will return the error message. * + * # Safety + * + * A task is an owned object, and must be freed with tc_task_free (or, if part of a list, + * with tc_task_list_free). + * + * Any function taking a `*TCTask` requires: + * - the pointer must not be NUL; + * - the pointer must be one previously returned from a tc_… function; + * - the memory referenced by the pointer must never be modified by C code; and + * - except for `tc_{task,task_list}_free`, ownership of a `*TCTask` remains with the caller. + * + * Once passed to tc_task_free, a `*TCTask` becomes invalid and must not be used again. + * * TCTasks are not threadsafe. */ typedef struct TCTask TCTask; @@ -138,6 +151,20 @@ typedef struct TCWorkingSet TCWorkingSet; /** * TCAnnotation contains the details of an annotation. + * + * # Safety + * + * An annotation must be initialized from a tc_.. function, and later freed + * with `tc_annotation_free` or `tc_annotation_list_free`. + * + * Any function taking a `*TCAnnotation` requires: + * - the pointer must not be NUL; + * - the pointer must be one previously returned from a tc_… function; + * - the memory referenced by the pointer must never be modified by C code; and + * - ownership transfers to the called function, and the value must not be used + * after the call returns. In fact, the value will be zeroed out to ensure this. + * + * TCAnnotations are not threadsafe. */ typedef struct TCAnnotation { /** @@ -344,7 +371,7 @@ void tc_kv_list_free(struct TCKVList *tckvs); /** * Create a new TCReplica with an in-memory database. The contents of the database will be - * lost when it is freed. + * lost when it is freed with tc_replica_free. */ struct TCReplica *tc_replica_new_in_memory(void); @@ -370,7 +397,8 @@ struct TCTaskList tc_replica_all_tasks(struct TCReplica *rep); struct TCUuidList tc_replica_all_task_uuids(struct TCReplica *rep); /** - * Get the current working set for this replica. + * Get the current working set for this replica. The resulting value must be freed + * with tc_working_set_free. * * Returns NULL on error. */ @@ -728,7 +756,8 @@ TCResult tc_task_add_tag(struct TCTask *task, struct TCString *tag); TCResult tc_task_remove_tag(struct TCTask *task, struct TCString *tag); /** - * Add an annotation to a mutable task. + * Add an annotation to a mutable task. This call takes ownership of the + * passed annotation, which must not be used after the call returns. */ TCResult tc_task_add_annotation(struct TCTask *task, struct TCAnnotation *annotation); @@ -814,8 +843,8 @@ struct TCUuid tc_uuid_nil(void); void tc_uuid_to_buf(struct TCUuid tcuuid, char *buf); /** - * Write the string representation of a TCUuid into the given buffer, which must be - * at least TC_UUID_STRING_BYTES long. No NUL terminator is added. + * Return the hyphenated string representation of a TCUuid. The returned string + * must be freed with tc_string_free. */ struct TCString *tc_uuid_to_str(struct TCUuid tcuuid); From 02055b122ea3bd935ded5aa8128eb9a5b83303c9 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 16 Feb 2022 00:07:30 +0000 Subject: [PATCH 81/95] find shared library on macos as well --- integration-tests/build.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/integration-tests/build.rs b/integration-tests/build.rs index e1c5c2fbe..b5217f5a0 100644 --- a/integration-tests/build.rs +++ b/integration-tests/build.rs @@ -11,7 +11,11 @@ fn build_libtaskchampion(suites: &[&'static str]) { libtaskchampion.push("target"); libtaskchampion.push(env::var("PROFILE").unwrap()); libtaskchampion.push("deps"); - libtaskchampion.push("libtaskchampion.so"); + libtaskchampion.push(if cfg!(target_os = "macos") { + "libtaskchampion.dylib" + } else { + "libtaskchampion.so" + }); let mut build = cc::Build::new(); build.shared_flag(true); From b1d537ac8703b5340b435e27a6c8d76ec29103a6 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 16 Feb 2022 00:28:07 +0000 Subject: [PATCH 82/95] use codegen, instead of build.rs, to build header file --- .cargo/config | 2 ++ Cargo.lock | 11 ++++++++--- Cargo.toml | 1 + README.md | 10 ++++++++-- lib/Cargo.toml | 4 ---- lib/build.rs | 26 -------------------------- xtask/Cargo.toml | 10 ++++++++++ xtask/src/main.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 76 insertions(+), 35 deletions(-) create mode 100644 .cargo/config delete mode 100644 lib/build.rs create mode 100644 xtask/Cargo.toml create mode 100644 xtask/src/main.rs diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 000000000..35049cbcb --- /dev/null +++ b/.cargo/config @@ -0,0 +1,2 @@ +[alias] +xtask = "run --package xtask --" diff --git a/Cargo.lock b/Cargo.lock index ca9160c27..3bdb7dc0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3035,12 +3035,9 @@ name = "taskchampion-lib" version = "0.1.0" dependencies = [ "anyhow", - "cbindgen", "chrono", "libc", - "pretty_assertions", "taskchampion", - "uuid", ] [[package]] @@ -3797,6 +3794,14 @@ dependencies = [ "time 0.1.43", ] +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "anyhow", + "cbindgen", +] + [[package]] name = "zeroize" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 18399678b..701f3839e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,5 @@ members = [ "sync-server", "lib", "integration-tests", + "xtask", ] diff --git a/README.md b/README.md index 10e119460..6bf44be76 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,18 @@ But, if you just want to get some practice with Rust, we'd be happy to have you. ## Structure -There are four crates here: +There are five crates here: * [taskchampion](./taskchampion) - the core of the tool * [taskchampion-cli](./cli) - the command-line binary * [taskchampion-sync-server](./sync-server) - the server against which `task sync` operates - * [replica-server-tests](./replica-server-tests) - integration tests covering both _taskchampion-cli_ and _taskchampion-sync-server_ + * [taskchampion-lib](./lib) - glue code to use _taskchampion_ from C + * [integration-tests](./integration-tests) - integration tests covering _taskchampion-cli_, _taskchampion-sync-server_, and _taskchampion-lib_. + +## Code Generation + +The _taskchampion_lib_ crate uses a bit of code generation to create the `lib/taskchampion.h` header file. +To regenerate this file, run `cargo xtask codegen`. ## Documentation Generation diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 86a2ec735..564f28590 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -2,7 +2,6 @@ name = "taskchampion-lib" version = "0.1.0" edition = "2018" -build = "build.rs" [lib] name = "taskchampion" @@ -17,6 +16,3 @@ anyhow = "1.0" [dev-dependencies] pretty_assertions = "1" - -[build-dependencies] -cbindgen = "0.20.0" diff --git a/lib/build.rs b/lib/build.rs deleted file mode 100644 index f8f75d3b5..000000000 --- a/lib/build.rs +++ /dev/null @@ -1,26 +0,0 @@ -use cbindgen::*; - -use std::env; - -fn main() { - let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); - - Builder::new() - .with_crate(crate_dir) - .with_config(Config { - language: Language::C, - cpp_compat: true, - sys_includes: vec!["stdbool.h".into(), "stdint.h".into(), "time.h".into()], - usize_is_size_t: true, - no_includes: true, - enumeration: EnumConfig { - // this appears to still default to true for C - enum_class: false, - ..Default::default() - }, - ..Default::default() - }) - .generate() - .expect("Unable to generate bindings") - .write_to_file("taskchampion.h"); -} diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 000000000..fe6c3dda6 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0" +cbindgen = "0.20.0" diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 000000000..036802e9a --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,47 @@ +//! This executable defines the `cargo xtask` subcommands. +//! +//! At the moment it is very simple, but if this grows more subcommands then +//! it will be sensible to use `clap` or another similar library. + +use cbindgen::*; +use std::env; +use std::path::PathBuf; + +pub fn main() -> anyhow::Result<()> { + let arg = env::args().nth(1); + match arg.as_ref().map(|arg| arg.as_str()) { + Some("codegen") => codegen(), + Some(arg) => anyhow::bail!("unknown xtask {}", arg), + _ => anyhow::bail!("unknown xtask"), + } +} + +/// `cargo xtask codegen` +/// +/// This uses cbindgen to generate `lib/taskchampion.h`. +fn codegen() -> anyhow::Result<()> { + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let workspace_dir = manifest_dir.parent().unwrap(); + let lib_crate_dir = workspace_dir.join("lib"); + + Builder::new() + .with_crate(&lib_crate_dir) + .with_config(Config { + language: Language::C, + cpp_compat: true, + sys_includes: vec!["stdbool.h".into(), "stdint.h".into(), "time.h".into()], + usize_is_size_t: true, + no_includes: true, + enumeration: EnumConfig { + // this appears to still default to true for C + enum_class: false, + ..Default::default() + }, + ..Default::default() + }) + .generate() + .expect("Unable to generate bindings") + .write_to_file(lib_crate_dir.join("taskchampion.h")); + + Ok(()) +} From 741cb84430d0ff0e288879ed1fb863eef566a2c4 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 16 Feb 2022 01:01:35 +0000 Subject: [PATCH 83/95] better docs for C --- Cargo.lock | 2 + lib/header-intro.h | 76 ++++++++++++++++++++++++++++++++++ lib/src/string.rs | 2 +- lib/src/workingset.rs | 16 ++++++++ lib/taskchampion.h | 96 ++++++++++++++++++++++++++++++++++++++++++- xtask/src/main.rs | 1 + 6 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 lib/header-intro.h diff --git a/Cargo.lock b/Cargo.lock index 3bdb7dc0f..256a68b3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3037,7 +3037,9 @@ dependencies = [ "anyhow", "chrono", "libc", + "pretty_assertions", "taskchampion", + "uuid", ] [[package]] diff --git a/lib/header-intro.h b/lib/header-intro.h new file mode 100644 index 000000000..c0dd01153 --- /dev/null +++ b/lib/header-intro.h @@ -0,0 +1,76 @@ +/** + * TaskChampion + * + * This file defines the C interface to libtaskchampion. This is a thin + * wrapper around the Rust `taskchampion` crate. Refer to the documentation + * for that crate at https://docs.rs/taskchampion/latest/taskchampion/ for API + * details. The comments in this file focus mostly on the low-level details of + * passing values to and from TaskChampion. + * + * # Overview + * + * This library defines two major types used to interact with the API, that map directly + * to Rust types. + * + * * TCReplica - see https://docs.rs/taskchampion/latest/taskchampion/struct.Replica.html + * * TCTask - see https://docs.rs/taskchampion/latest/taskchampion/struct.Task.html + * * TCServer - see https://docs.rs/taskchampion/latest/taskchampion/trait.Server.html + * * TCWorkingSet - see https://docs.rs/taskchampion/latest/taskchampion/struct.WorkingSet.html + * + * It also defines a few utility types: + * + * * TCString - a wrapper around both C (NUL-terminated) and Rust (always utf-8) strings. + * * TC…List - a list of objects represented as a C array + * * see below for the remainder + * + * # Safety + * + * Each type contains specific instructions to ensure memory safety. + * The general rules are as follows. + * + * No types in this library are threadsafe. All values should be used in only + * one thread for their entire lifetime. It is safe to use unrelated values in + * different threads (for example, different threads may use different + * TCReplica values concurrently). + * + * ## Pass by Pointer + * + * Several types such as TCReplica and TCString are "opaque" types and always + * handled as pointers in C. The bytes these pointers address are private to + * the Rust implemetation and must not be accessed from C. + * + * Pass-by-pointer values have exactly one owner, and that owner is responsible + * for freeing the value (using a `tc_…_free` function), or transferring + * ownership elsewhere. Except where documented otherwise, when a value is + * passed to C, ownership passes to C as well. When a value is passed to Rust, + * ownership stays with the C code. The exception is TCString, ownership of + * which passes to Rust when it is used as a function argument. + * + * The limited circumstances where one value must not outlive another, due to + * pointer references between them, are documented below. + * + * ## Pass by Value + * + * Types such as TCUuid and TC…List are passed by value, and contain fields + * that are accessible from C. C code is free to access the content of these + * types in a _read_only_ fashion. + * + * Pass-by-value values that contain pointers also have exactly one owner, + * responsible for freeing the value or transferring ownership. The tc_…_free + * functions for these types will replace the pointers with NULL to guard + * against use-after-free errors. The interior pointers in such values should + * never be freed directly (for example, `tc_string_free(tcuda.value)` is an + * error). + * + * TCUuid is a special case, because it does not contain pointers. It can be + * freely copied and need not be freed. + * + * ## Lists + * + * Lists are a special kind of pass-by-value type. Each contains `len` and + * `items`, where `items` is an array of length `len`. Lists, and the values + * in the `items` array, must be treated as read-only. On return from an API + * function, a list's ownership is with the C caller, which must eventually + * free the list. List data must be freed with the `tc_…_list_free` function. + * It is an error to free any value in the `items` array of a list. + */ diff --git a/lib/src/string.rs b/lib/src/string.rs index 3e0d4069e..70e4795ee 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -25,7 +25,7 @@ use std::str::Utf8Error; /// When a `*TCString` appears as a return value or output argument, ownership is passed to the /// caller. The caller must pass that ownership back to another function or free the string. /// -/// Any function taking a `*TCReplica` requires: +/// Any function taking a `*TCString` requires: /// - the pointer must not be NUL; /// - the pointer must be one previously returned from a tc_… function; and /// - the memory referenced by the pointer must never be modified by C code. diff --git a/lib/src/workingset.rs b/lib/src/workingset.rs index 6e1e15b0f..672194886 100644 --- a/lib/src/workingset.rs +++ b/lib/src/workingset.rs @@ -7,6 +7,22 @@ use taskchampion::{Uuid, WorkingSet}; /// be freed at any time. /// /// To iterate over a working set, search indexes 1 through largest_index. +/// +/// # Safety +/// +/// The `*TCWorkingSet` returned from `tc_replica_working_set` is owned by the caller and +/// must later be freed to avoid a memory leak. Its lifetime is independent of the replica +/// from which it was generated. +/// +/// Any function taking a `*TCWorkingSet` requires: +/// - the pointer must not be NUL; +/// - the pointer must be one previously returned from `tc_replica_working_set` +/// - the memory referenced by the pointer must never be accessed by C code; and +/// - except for `tc_replica_free`, ownership of a `*TCWorkingSet` remains with the caller. +/// +/// Once passed to `tc_replica_free`, a `*TCWorkingSet` becomes invalid and must not be used again. +/// +/// TCWorkingSet is not threadsafe. pub struct TCWorkingSet(WorkingSet); impl PassByPointer for TCWorkingSet {} diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 0fd61ad9a..702e24446 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -1,3 +1,81 @@ +/** + * TaskChampion + * + * This file defines the C interface to libtaskchampion. This is a thin + * wrapper around the Rust `taskchampion` crate. Refer to the documentation + * for that crate at https://docs.rs/taskchampion/latest/taskchampion/ for API + * details. The comments in this file focus mostly on the low-level details of + * passing values to and from TaskChampion. + * + * # Overview + * + * This library defines two major types used to interact with the API, that map directly + * to Rust types. + * + * * TCReplica - see https://docs.rs/taskchampion/latest/taskchampion/struct.Replica.html + * * TCTask - see https://docs.rs/taskchampion/latest/taskchampion/struct.Task.html + * * TCServer - see https://docs.rs/taskchampion/latest/taskchampion/trait.Server.html + * * TCWorkingSet - see https://docs.rs/taskchampion/latest/taskchampion/struct.WorkingSet.html + * + * It also defines a few utility types: + * + * * TCString - a wrapper around both C (NUL-terminated) and Rust (always utf-8) strings. + * * TC…List - a list of objects represented as a C array + * * see below for the remainder + * + * # Safety + * + * Each type contains specific instructions to ensure memory safety. + * The general rules are as follows. + * + * No types in this library are threadsafe. All values should be used in only + * one thread for their entire lifetime. It is safe to use unrelated values in + * different threads (for example, different threads may use different + * TCReplica values concurrently). + * + * ## Pass by Pointer + * + * Several types such as TCReplica and TCString are "opaque" types and always + * handled as pointers in C. The bytes these pointers address are private to + * the Rust implemetation and must not be accessed from C. + * + * Pass-by-pointer values have exactly one owner, and that owner is responsible + * for freeing the value (using a `tc_…_free` function), or transferring + * ownership elsewhere. Except where documented otherwise, when a value is + * passed to C, ownership passes to C as well. When a value is passed to Rust, + * ownership stays with the C code. The exception is TCString, ownership of + * which passes to Rust when it is used as a function argument. + * + * The limited circumstances where one value must not outlive another, due to + * pointer references between them, are documented below. + * + * ## Pass by Value + * + * Types such as TCUuid and TC…List are passed by value, and contain fields + * that are accessible from C. C code is free to access the content of these + * types in a _read_only_ fashion. + * + * Pass-by-value values that contain pointers also have exactly one owner, + * responsible for freeing the value or transferring ownership. The tc_…_free + * functions for these types will replace the pointers with NULL to guard + * against use-after-free errors. The interior pointers in such values should + * never be freed directly (for example, `tc_string_free(tcuda.value)` is an + * error). + * + * TCUuid is a special case, because it does not contain pointers. It can be + * freely copied and need not be freed. + * + * ## Lists + * + * Lists are a special kind of pass-by-value type. Each contains `len` and + * `items`, where `items` is an array of length `len`. Lists, and the values + * in the `items` array, must be treated as read-only. On return from an API + * function, a list's ownership is with the C caller, which must eventually + * free the list. List data must be freed with the `tc_…_list_free` function. + * It is an error to free any value in the `items` array of a list. + */ + + #include #include #include @@ -94,7 +172,7 @@ typedef struct TCServer TCServer; * When a `*TCString` appears as a return value or output argument, ownership is passed to the * caller. The caller must pass that ownership back to another function or free the string. * - * Any function taking a `*TCReplica` requires: + * Any function taking a `*TCString` requires: * - the pointer must not be NUL; * - the pointer must be one previously returned from a tc_… function; and * - the memory referenced by the pointer must never be modified by C code. @@ -146,6 +224,22 @@ typedef struct TCTask TCTask; * be freed at any time. * * To iterate over a working set, search indexes 1 through largest_index. + * + * # Safety + * + * The `*TCWorkingSet` returned from `tc_replica_working_set` is owned by the caller and + * must later be freed to avoid a memory leak. Its lifetime is independent of the replica + * from which it was generated. + * + * Any function taking a `*TCWorkingSet` requires: + * - the pointer must not be NUL; + * - the pointer must be one previously returned from `tc_replica_working_set` + * - the memory referenced by the pointer must never be accessed by C code; and + * - except for `tc_replica_free`, ownership of a `*TCWorkingSet` remains with the caller. + * + * Once passed to `tc_replica_free`, a `*TCWorkingSet` becomes invalid and must not be used again. + * + * TCWorkingSet is not threadsafe. */ typedef struct TCWorkingSet TCWorkingSet; diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 036802e9a..3990a4f39 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -27,6 +27,7 @@ fn codegen() -> anyhow::Result<()> { Builder::new() .with_crate(&lib_crate_dir) .with_config(Config { + header: Some(include_str!("../../lib/header-intro.h").into()), language: Language::C, cpp_compat: true, sys_includes: vec!["stdbool.h".into(), "stdint.h".into(), "time.h".into()], From 2eee7616448593ad749df97b61b522e65c3642e3 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 16 Feb 2022 01:03:46 +0000 Subject: [PATCH 84/95] fix xtask Cargo.toml --- xtask/Cargo.toml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index fe6c3dda6..6deb10d58 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -1,9 +1,7 @@ [package] name = "xtask" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +version = "0.4.1" +edition = "2018" [dependencies] anyhow = "1.0" From 471119dbdf4d6a3c0870c5309e12df13517f8e06 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 17 Feb 2022 04:11:06 +0000 Subject: [PATCH 85/95] TCString as PassByValue --- Cargo.lock | 2 +- .../src/bindings_tests/replica.c | 76 +-- integration-tests/src/bindings_tests/string.c | 67 +-- integration-tests/src/bindings_tests/task.c | 140 ++--- integration-tests/src/bindings_tests/uuid.c | 6 +- lib/src/annotation.rs | 22 +- lib/src/kv.rs | 18 +- lib/src/lib.rs | 2 +- lib/src/replica.rs | 65 ++- lib/src/server.rs | 41 +- lib/src/string.rs | 498 +++++++++++++----- lib/src/task.rs | 219 ++++---- lib/src/traits.rs | 8 +- lib/src/uda.rs | 38 +- lib/src/util.rs | 15 +- lib/src/uuid.rs | 23 +- lib/taskchampion.h | 182 ++++--- 17 files changed, 853 insertions(+), 569 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 256a68b3e..fed2a869a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3798,7 +3798,7 @@ dependencies = [ [[package]] name = "xtask" -version = "0.1.0" +version = "0.4.1" dependencies = [ "anyhow", "cbindgen", diff --git a/integration-tests/src/bindings_tests/replica.c b/integration-tests/src/bindings_tests/replica.c index 282d2948b..cd02d3a30 100644 --- a/integration-tests/src/bindings_tests/replica.c +++ b/integration-tests/src/bindings_tests/replica.c @@ -8,7 +8,7 @@ static void test_replica_creation(void) { TCReplica *rep = tc_replica_new_in_memory(); TEST_ASSERT_NOT_NULL(rep); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); tc_replica_free(rep); } @@ -16,28 +16,28 @@ static void test_replica_creation(void) { static void test_replica_creation_disk(void) { TCReplica *rep = tc_replica_new_on_disk(tc_string_borrow("test-db"), NULL); TEST_ASSERT_NOT_NULL(rep); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); tc_replica_free(rep); } // undo on an empty in-memory TCReplica does nothing static void test_replica_undo_empty(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); int undone; int rv = tc_replica_undo(rep, &undone); TEST_ASSERT_EQUAL(TC_RESULT_OK, rv); TEST_ASSERT_EQUAL(0, undone); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); tc_replica_free(rep); } // adding an undo point succeeds static void test_replica_add_undo_point(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_add_undo_point(rep, true)); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); tc_replica_free(rep); } @@ -48,10 +48,10 @@ static void test_replica_working_set(void) { TCUuid uuid, uuid1, uuid2, uuid3; TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_rebuild_working_set(rep, true)); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); ws = tc_replica_working_set(rep); TEST_ASSERT_EQUAL(0, tc_working_set_len(ws)); @@ -62,7 +62,7 @@ static void test_replica_working_set(void) { uuid1 = tc_task_get_uuid(task1); TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_rebuild_working_set(rep, true)); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); task2 = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("task2")); TEST_ASSERT_NOT_NULL(task2); @@ -73,7 +73,7 @@ static void test_replica_working_set(void) { uuid3 = tc_task_get_uuid(task3); TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_rebuild_working_set(rep, false)); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); // finish task2 to leave a "hole" tc_task_to_mut(task2, rep); @@ -81,7 +81,7 @@ static void test_replica_working_set(void) { tc_task_to_immut(task2); TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_rebuild_working_set(rep, false)); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); tc_task_free(task1); tc_task_free(task2); @@ -115,17 +115,17 @@ static void test_replica_working_set(void) { // When tc_replica_undo is passed NULL for undone_out, it still succeeds static void test_replica_undo_empty_null_undone_out(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); int rv = tc_replica_undo(rep, NULL); TEST_ASSERT_EQUAL(TC_RESULT_OK, rv); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); tc_replica_free(rep); } // creating a task succeeds and the resulting task looks good static void test_replica_task_creation(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCTask *task = tc_replica_new_task( rep, @@ -136,10 +136,10 @@ static void test_replica_task_creation(void) { TCUuid uuid = tc_task_get_uuid(task); TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task)); - TCString *desc = tc_task_get_description(task); - TEST_ASSERT_NOT_NULL(desc); - TEST_ASSERT_EQUAL_STRING("my task", tc_string_content(desc)); - tc_string_free(desc); + TCString desc = tc_task_get_description(task); + TEST_ASSERT_NOT_NULL(desc.ptr); + TEST_ASSERT_EQUAL_STRING("my task", tc_string_content(&desc)); + tc_string_free(&desc); tc_task_free(task); @@ -157,18 +157,18 @@ static void test_replica_task_creation(void) { // When tc_replica_undo is passed NULL for undone_out, it still succeeds static void test_replica_sync_local(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); mkdir("test-sync-server", 0755); // ignore error, if dir already exists - TCString *err = NULL; + TCString err; TCServer *server = tc_server_new_local(tc_string_borrow("test-sync-server"), &err); TEST_ASSERT_NOT_NULL(server); - TEST_ASSERT_NULL(err); + TEST_ASSERT_NULL(err.ptr); int rv = tc_replica_sync(rep, server, false); TEST_ASSERT_EQUAL(TC_RESULT_OK, rv); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); tc_server_free(server); tc_replica_free(rep); @@ -176,20 +176,20 @@ static void test_replica_sync_local(void) { // test error handling server = tc_server_new_local(tc_string_borrow("/no/such/directory"), &err); TEST_ASSERT_NULL(server); - TEST_ASSERT_NOT_NULL(err); - tc_string_free(err); + TEST_ASSERT_NOT_NULL(err.ptr); + tc_string_free(&err); } // When tc_replica_undo is passed NULL for undone_out, it still succeeds static void test_replica_remote_server(void) { - TCString *err = NULL; + TCString err; TCServer *server = tc_server_new_remote( tc_string_borrow("tc.freecinc.com"), tc_uuid_new_v4(), tc_string_borrow("\xf0\x28\x8c\x28"), // NOTE: not utf-8 &err); TEST_ASSERT_NOT_NULL(server); - TEST_ASSERT_NULL(err); + TEST_ASSERT_NULL(err.ptr); // can't actually do anything with this server! @@ -199,7 +199,7 @@ static void test_replica_remote_server(void) { // a replica with tasks in it returns an appropriate list of tasks and list of uuids static void test_replica_all_tasks(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCTask *task1 = tc_replica_new_task( rep, @@ -225,13 +225,13 @@ static void test_replica_all_tasks(void) { bool seen1, seen2 = false; for (size_t i = 0; i < tasks.len; i++) { TCTask *task = tasks.items[i]; - TCString *descr = tc_task_get_description(task); - if (0 == strcmp(tc_string_content(descr), "task1")) { + TCString descr = tc_task_get_description(task); + if (0 == strcmp(tc_string_content(&descr), "task1")) { seen1 = true; - } else if (0 == strcmp(tc_string_content(descr), "task2")) { + } else if (0 == strcmp(tc_string_content(&descr), "task2")) { seen2 = true; } - tc_string_free(descr); + tc_string_free(&descr); } TEST_ASSERT_TRUE(seen1); TEST_ASSERT_TRUE(seen2); @@ -267,7 +267,7 @@ static void test_replica_all_tasks(void) { // importing a task succeeds and the resulting task looks good static void test_replica_task_import(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCUuid uuid; TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_uuid_from_str(tc_string_borrow("23cb25e0-5d1a-4932-8131-594ac6d3a843"), &uuid)); @@ -277,10 +277,10 @@ static void test_replica_task_import(void) { TEST_ASSERT_EQUAL_MEMORY(uuid.bytes, tc_task_get_uuid(task).bytes, sizeof(uuid.bytes)); TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task)); - TCString *desc = tc_task_get_description(task); - TEST_ASSERT_NOT_NULL(desc); - TEST_ASSERT_EQUAL_STRING("", tc_string_content(desc)); // default value - tc_string_free(desc); + TCString desc = tc_task_get_description(task); + TEST_ASSERT_NOT_NULL(desc.ptr); + TEST_ASSERT_EQUAL_STRING("", tc_string_content(&desc)); // default value + tc_string_free(&desc); tc_task_free(task); @@ -298,13 +298,13 @@ static void test_replica_task_import(void) { // importing a task succeeds and the resulting task looks good static void test_replica_get_task_not_found(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCUuid uuid; TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_uuid_from_str(tc_string_borrow("23cb25e0-5d1a-4932-8131-594ac6d3a843"), &uuid)); TCTask *task = tc_replica_get_task(rep, uuid); TEST_ASSERT_NULL(task); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); } int replica_tests(void) { diff --git a/integration-tests/src/bindings_tests/string.c b/integration-tests/src/bindings_tests/string.c index 0eebec8f7..2bd2749c0 100644 --- a/integration-tests/src/bindings_tests/string.c +++ b/integration-tests/src/bindings_tests/string.c @@ -5,103 +5,110 @@ // creating strings does not crash static void test_string_creation(void) { - TCString *s = tc_string_borrow("abcdef"); - tc_string_free(s); + TCString s = tc_string_borrow("abcdef"); + tc_string_free(&s); + TEST_ASSERT_NULL(s.ptr); } // creating cloned strings does not crash static void test_string_cloning(void) { char *abcdef = strdup("abcdef"); - TCString *s = tc_string_clone(abcdef); - TEST_ASSERT_NOT_NULL(s); + TCString s = tc_string_clone(abcdef); + TEST_ASSERT_NOT_NULL(s.ptr); free(abcdef); - TEST_ASSERT_EQUAL_STRING("abcdef", tc_string_content(s)); - tc_string_free(s); + TEST_ASSERT_EQUAL_STRING("abcdef", tc_string_content(&s)); + tc_string_free(&s); + TEST_ASSERT_NULL(s.ptr); } // creating cloned strings with invalid utf-8 does not crash // ..but content is NULL and content_and_len returns the value static void test_string_cloning_invalid_utf8(void) { - TCString *s = tc_string_clone("\xf0\x28\x8c\x28"); - TEST_ASSERT_NOT_NULL(s); + TCString s = tc_string_clone("\xf0\x28\x8c\x28"); + TEST_ASSERT_NOT_NULL(s.ptr); // NOTE: this is not one of the cases where invalid UTF-8 results in NULL, // but that may change. size_t len; - const char *buf = tc_string_content_with_len(s, &len); + const char *buf = tc_string_content_with_len(&s, &len); TEST_ASSERT_NOT_NULL(buf); TEST_ASSERT_EQUAL(4, len); TEST_ASSERT_EQUAL_MEMORY("\xf0\x28\x8c\x28", buf, len); - tc_string_free(s); + tc_string_free(&s); + TEST_ASSERT_NULL(s.ptr); } // borrowed strings echo back their content static void test_string_borrowed_strings_echo(void) { - TCString *s = tc_string_borrow("abcdef"); - TEST_ASSERT_NOT_NULL(s); + TCString s = tc_string_borrow("abcdef"); + TEST_ASSERT_NOT_NULL(s.ptr); - TEST_ASSERT_EQUAL_STRING("abcdef", tc_string_content(s)); + TEST_ASSERT_EQUAL_STRING("abcdef", tc_string_content(&s)); size_t len; - const char *buf = tc_string_content_with_len(s, &len); + const char *buf = tc_string_content_with_len(&s, &len); TEST_ASSERT_NOT_NULL(buf); TEST_ASSERT_EQUAL(6, len); TEST_ASSERT_EQUAL_MEMORY("abcdef", buf, len); - tc_string_free(s); + tc_string_free(&s); + TEST_ASSERT_NULL(s.ptr); } // cloned strings echo back their content static void test_string_cloned_strings_echo(void) { char *orig = strdup("abcdef"); - TCString *s = tc_string_clone(orig); - TEST_ASSERT_NOT_NULL(s); + TCString s = tc_string_clone(orig); + TEST_ASSERT_NOT_NULL(s.ptr); free(orig); - TEST_ASSERT_EQUAL_STRING("abcdef", tc_string_content(s)); + TEST_ASSERT_EQUAL_STRING("abcdef", tc_string_content(&s)); size_t len; - const char *buf = tc_string_content_with_len(s, &len); + const char *buf = tc_string_content_with_len(&s, &len); TEST_ASSERT_NOT_NULL(buf); TEST_ASSERT_EQUAL(6, len); TEST_ASSERT_EQUAL_MEMORY("abcdef", buf, len); - tc_string_free(s); + tc_string_free(&s); + TEST_ASSERT_NULL(s.ptr); } // tc_clone_with_len can have NULs, and tc_string_content returns NULL for // strings containing embedded NULs static void test_string_content_null_for_embedded_nuls(void) { - TCString *s = tc_string_clone_with_len("ab\0de", 5); - TEST_ASSERT_NOT_NULL(s); + TCString s = tc_string_clone_with_len("ab\0de", 5); + TEST_ASSERT_NOT_NULL(s.ptr); - TEST_ASSERT_NULL(tc_string_content(s)); + TEST_ASSERT_NULL(tc_string_content(&s)); size_t len; - const char *buf = tc_string_content_with_len(s, &len); + const char *buf = tc_string_content_with_len(&s, &len); TEST_ASSERT_NOT_NULL(buf); TEST_ASSERT_EQUAL(5, len); TEST_ASSERT_EQUAL_MEMORY("ab\0de", buf, len); - tc_string_free(s); + tc_string_free(&s); + TEST_ASSERT_NULL(s.ptr); } // tc_string_clone_with_len will accept invalid utf-8, but then tc_string_content // returns NULL. static void test_string_clone_with_len_invalid_utf8(void) { - TCString *s = tc_string_clone_with_len("\xf0\x28\x8c\x28", 4); - TEST_ASSERT_NOT_NULL(s); + TCString s = tc_string_clone_with_len("\xf0\x28\x8c\x28", 4); + TEST_ASSERT_NOT_NULL(s.ptr); - TEST_ASSERT_NULL(tc_string_content(s)); + TEST_ASSERT_NULL(tc_string_content(&s)); size_t len; - const char *buf = tc_string_content_with_len(s, &len); + const char *buf = tc_string_content_with_len(&s, &len); TEST_ASSERT_NOT_NULL(buf); TEST_ASSERT_EQUAL(4, len); TEST_ASSERT_EQUAL_MEMORY("\xf0\x28\x8c\x28", buf, len); - tc_string_free(s); + tc_string_free(&s); + TEST_ASSERT_NULL(s.ptr); } int string_tests(void) { diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index b2412f3c3..f3ff29ae0 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -6,7 +6,7 @@ // creating a task succeeds and the resulting task looks good static void test_task_creation(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCTask *task = tc_replica_new_task( rep, @@ -16,10 +16,10 @@ static void test_task_creation(void) { TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task)); - TCString *desc = tc_task_get_description(task); - TEST_ASSERT_NOT_NULL(desc); - TEST_ASSERT_EQUAL_STRING("my task", tc_string_content(desc)); - tc_string_free(desc); + TCString desc = tc_task_get_description(task); + TEST_ASSERT_NOT_NULL(desc.ptr); + TEST_ASSERT_EQUAL_STRING("my task", tc_string_content(&desc)); + tc_string_free(&desc); tc_task_free(task); @@ -29,7 +29,7 @@ static void test_task_creation(void) { // freeing a mutable task works, marking it immutable static void test_task_free_mutable_task(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCTask *task = tc_replica_new_task( rep, @@ -57,7 +57,7 @@ static void test_task_free_mutable_task(void) { // updating status on a task works static void test_task_get_set_status(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCTask *task = tc_replica_new_task( rep, @@ -81,7 +81,7 @@ static void test_task_get_set_status(void) { // updating description on a task works static void test_task_get_set_description(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCTask *task = tc_replica_new_task( rep, @@ -89,22 +89,22 @@ static void test_task_get_set_description(void) { tc_string_borrow("my task")); TEST_ASSERT_NOT_NULL(task); - TCString *desc; + TCString desc; tc_task_to_mut(task, rep); TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_description(task, tc_string_borrow("updated"))); - TEST_ASSERT_TRUE(desc = tc_task_get_description(task)); - TEST_ASSERT_NOT_NULL(desc); - TEST_ASSERT_EQUAL_STRING("updated", tc_string_content(desc)); - tc_string_free(desc); + desc = tc_task_get_description(task); + TEST_ASSERT_NOT_NULL(desc.ptr); + TEST_ASSERT_EQUAL_STRING("updated", tc_string_content(&desc)); + tc_string_free(&desc); tc_task_to_immut(task); desc = tc_task_get_description(task); - TEST_ASSERT_NOT_NULL(desc); - TEST_ASSERT_EQUAL_STRING("updated", tc_string_content(desc)); - tc_string_free(desc); + TEST_ASSERT_NOT_NULL(desc.ptr); + TEST_ASSERT_EQUAL_STRING("updated", tc_string_content(&desc)); + tc_string_free(&desc); tc_task_free(task); @@ -114,7 +114,7 @@ static void test_task_get_set_description(void) { // updating entry on a task works static void test_task_get_set_entry(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCTask *task = tc_replica_new_task( rep, @@ -141,7 +141,7 @@ static void test_task_get_set_entry(void) { // updating wait on a task works static void test_task_get_set_wait_and_is_waiting(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCTask *task = tc_replica_new_task( rep, @@ -175,7 +175,7 @@ static void test_task_get_set_wait_and_is_waiting(void) { // updating modified on a task works static void test_task_get_set_modified(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCTask *task = tc_replica_new_task( rep, @@ -202,7 +202,7 @@ static void test_task_get_set_modified(void) { // starting and stopping a task works, as seen by tc_task_is_active static void test_task_start_stop_is_active(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCTask *task = tc_replica_new_task( rep, @@ -227,7 +227,7 @@ static void test_task_start_stop_is_active(void) { // tc_task_done and delete work and set the status static void test_task_done_and_delete(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCTask *task = tc_replica_new_task( rep, @@ -250,7 +250,7 @@ static void test_task_done_and_delete(void) { // adding and removing tags to a task works, and invalid tags are rejected static void test_task_add_remove_has_tag(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCTask *task = tc_replica_new_task( rep, @@ -263,33 +263,33 @@ static void test_task_add_remove_has_tag(void) { TEST_ASSERT_FALSE(tc_task_has_tag(task, tc_string_borrow("next"))); TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_tag(task, tc_string_borrow("next"))); - TEST_ASSERT_NULL(tc_task_error(task)); + TEST_ASSERT_NULL(tc_task_error(task).ptr); TEST_ASSERT_TRUE(tc_task_has_tag(task, tc_string_borrow("next"))); // invalid - synthetic tag TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_task_add_tag(task, tc_string_borrow("PENDING"))); - TCString *err = tc_task_error(task); - TEST_ASSERT_NOT_NULL(err); - tc_string_free(err); + TCString err = tc_task_error(task); + TEST_ASSERT_NOT_NULL(err.ptr); + tc_string_free(&err); // invald - not a valid tag string TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_task_add_tag(task, tc_string_borrow("my tag"))); err = tc_task_error(task); - TEST_ASSERT_NOT_NULL(err); - tc_string_free(err); + TEST_ASSERT_NOT_NULL(err.ptr); + tc_string_free(&err); // invald - not utf-8 TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_task_add_tag(task, tc_string_borrow("\xf0\x28\x8c\x28"))); err = tc_task_error(task); - TEST_ASSERT_NOT_NULL(err); - tc_string_free(err); + TEST_ASSERT_NOT_NULL(err.ptr); + tc_string_free(&err); TEST_ASSERT_TRUE(tc_task_has_tag(task, tc_string_borrow("next"))); // remove the tag TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_remove_tag(task, tc_string_borrow("next"))); - TEST_ASSERT_NULL(tc_task_error(task)); + TEST_ASSERT_NULL(tc_task_error(task).ptr); TEST_ASSERT_FALSE(tc_task_has_tag(task, tc_string_borrow("next"))); @@ -300,7 +300,7 @@ static void test_task_add_remove_has_tag(void) { // get_tags returns the list of tags static void test_task_get_tags(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCTask *task = tc_replica_new_task( rep, @@ -316,10 +316,10 @@ static void test_task_get_tags(void) { int found_pending = false, found_next = false; for (size_t i = 0; i < tags.len; i++) { - if (strcmp("PENDING", tc_string_content(tags.items[i])) == 0) { + if (strcmp("PENDING", tc_string_content(&tags.items[i])) == 0) { found_pending = true; } - if (strcmp("next", tc_string_content(tags.items[i])) == 0) { + if (strcmp("next", tc_string_content(&tags.items[i])) == 0) { found_next = true; } } @@ -336,7 +336,7 @@ static void test_task_get_tags(void) { // annotation manipulation (add, remove, list, free) static void test_task_annotations(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCTask *task = tc_replica_new_task( rep, @@ -356,22 +356,22 @@ static void test_task_annotations(void) { ann.entry = 1644623411; ann.description = tc_string_borrow("ann1"); TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_annotation(task, &ann)); - TEST_ASSERT_NULL(ann.description); + TEST_ASSERT_NULL(ann.description.ptr); ann.entry = 1644623422; ann.description = tc_string_borrow("ann2"); TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_annotation(task, &ann)); - TEST_ASSERT_NULL(ann.description); + TEST_ASSERT_NULL(ann.description.ptr); anns = tc_task_get_annotations(task); int found1 = false, found2 = false; for (size_t i = 0; i < anns.len; i++) { - if (0 == strcmp("ann1", tc_string_content(anns.items[i].description))) { + if (0 == strcmp("ann1", tc_string_content(&anns.items[i].description))) { TEST_ASSERT_EQUAL(anns.items[i].entry, 1644623411); found1 = true; } - if (0 == strcmp("ann2", tc_string_content(anns.items[i].description))) { + if (0 == strcmp("ann2", tc_string_content(&anns.items[i].description))) { TEST_ASSERT_EQUAL(anns.items[i].entry, 1644623422); found2 = true; } @@ -389,7 +389,7 @@ static void test_task_annotations(void) { // UDA manipulation (add, remove, list, free) static void test_task_udas(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCTask *task = tc_replica_new_task( rep, @@ -399,11 +399,11 @@ static void test_task_udas(void) { tc_task_to_mut(task, rep); - TCString *value; + TCString value; TCUdaList udas; - TEST_ASSERT_NULL(tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1"))); - TEST_ASSERT_NULL(tc_task_get_legacy_uda(task, tc_string_borrow("leg1"))); + TEST_ASSERT_NULL(tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1")).ptr); + TEST_ASSERT_NULL(tc_task_get_legacy_uda(task, tc_string_borrow("leg1")).ptr); udas = tc_task_get_udas(task); TEST_ASSERT_NOT_NULL(udas.items); @@ -422,25 +422,25 @@ static void test_task_udas(void) { tc_string_borrow("vvv"))); value = tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1")); - TEST_ASSERT_NOT_NULL(value); - TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(value)); - tc_string_free(value); - TEST_ASSERT_NULL(tc_task_get_legacy_uda(task, tc_string_borrow("leg1"))); + TEST_ASSERT_NOT_NULL(value.ptr); + TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(&value)); + tc_string_free(&value); + TEST_ASSERT_NULL(tc_task_get_legacy_uda(task, tc_string_borrow("leg1")).ptr); udas = tc_task_get_udas(task); TEST_ASSERT_NOT_NULL(udas.items); TEST_ASSERT_EQUAL(1, udas.len); - TEST_ASSERT_EQUAL_STRING("ns", tc_string_content(udas.items[0].ns)); - TEST_ASSERT_EQUAL_STRING("u1", tc_string_content(udas.items[0].key)); - TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(udas.items[0].value)); + TEST_ASSERT_EQUAL_STRING("ns", tc_string_content(&udas.items[0].ns)); + TEST_ASSERT_EQUAL_STRING("u1", tc_string_content(&udas.items[0].key)); + TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(&udas.items[0].value)); tc_uda_list_free(&udas); udas = tc_task_get_legacy_udas(task); TEST_ASSERT_NOT_NULL(udas.items); TEST_ASSERT_EQUAL(1, udas.len); - TEST_ASSERT_NULL(udas.items[0].ns); - TEST_ASSERT_EQUAL_STRING("ns.u1", tc_string_content(udas.items[0].key)); - TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(udas.items[0].value)); + TEST_ASSERT_NULL(udas.items[0].ns.ptr); + TEST_ASSERT_EQUAL_STRING("ns.u1", tc_string_content(&udas.items[0].key)); + TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(&udas.items[0].value)); tc_uda_list_free(&udas); TEST_ASSERT_EQUAL(TC_RESULT_OK, @@ -449,14 +449,14 @@ static void test_task_udas(void) { tc_string_borrow("legv"))); value = tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1")); - TEST_ASSERT_NOT_NULL(value); - TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(value)); - tc_string_free(value); + TEST_ASSERT_NOT_NULL(value.ptr); + TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(&value)); + tc_string_free(&value); value = tc_task_get_legacy_uda(task, tc_string_borrow("leg1")); - TEST_ASSERT_NOT_NULL(value); - TEST_ASSERT_EQUAL_STRING("legv", tc_string_content(value)); - tc_string_free(value); + TEST_ASSERT_NOT_NULL(value.ptr); + TEST_ASSERT_EQUAL_STRING("legv", tc_string_content(&value)); + tc_string_free(&value); udas = tc_task_get_udas(task); TEST_ASSERT_NOT_NULL(udas.items); @@ -473,7 +473,7 @@ static void test_task_udas(void) { tc_string_borrow("ns"), tc_string_borrow("u1"))); - TEST_ASSERT_NULL(tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1"))); + TEST_ASSERT_NULL(tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1")).ptr); TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_remove_uda(task, @@ -483,24 +483,24 @@ static void test_task_udas(void) { udas = tc_task_get_udas(task); TEST_ASSERT_NOT_NULL(udas.items); TEST_ASSERT_EQUAL(1, udas.len); - TEST_ASSERT_EQUAL_STRING("", tc_string_content(udas.items[0].ns)); - TEST_ASSERT_EQUAL_STRING("leg1", tc_string_content(udas.items[0].key)); - TEST_ASSERT_EQUAL_STRING("legv", tc_string_content(udas.items[0].value)); + TEST_ASSERT_EQUAL_STRING("", tc_string_content(&udas.items[0].ns)); + TEST_ASSERT_EQUAL_STRING("leg1", tc_string_content(&udas.items[0].key)); + TEST_ASSERT_EQUAL_STRING("legv", tc_string_content(&udas.items[0].value)); tc_uda_list_free(&udas); udas = tc_task_get_legacy_udas(task); TEST_ASSERT_NOT_NULL(udas.items); TEST_ASSERT_EQUAL(1, udas.len); - TEST_ASSERT_NULL(udas.items[0].ns); - TEST_ASSERT_EQUAL_STRING("leg1", tc_string_content(udas.items[0].key)); - TEST_ASSERT_EQUAL_STRING("legv", tc_string_content(udas.items[0].value)); + TEST_ASSERT_NULL(udas.items[0].ns.ptr); + TEST_ASSERT_EQUAL_STRING("leg1", tc_string_content(&udas.items[0].key)); + TEST_ASSERT_EQUAL_STRING("legv", tc_string_content(&udas.items[0].value)); tc_uda_list_free(&udas); TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_remove_legacy_uda(task, tc_string_borrow("leg1"))); - TEST_ASSERT_NULL(tc_task_get_legacy_uda(task, tc_string_borrow("leg1"))); + TEST_ASSERT_NULL(tc_task_get_legacy_uda(task, tc_string_borrow("leg1")).ptr); TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_remove_legacy_uda(task, @@ -523,8 +523,8 @@ static void test_task_udas(void) { static void tckvlist_assert_key(TCKVList *list, char *key, char *value) { TEST_ASSERT_NOT_NULL(list); for (size_t i = 0; i < list->len; i++) { - if (0 == strcmp(tc_string_content(list->items[i].key), key)) { - TEST_ASSERT_EQUAL_STRING(value, tc_string_content(list->items[i].value)); + if (0 == strcmp(tc_string_content(&list->items[i].key), key)) { + TEST_ASSERT_EQUAL_STRING(value, tc_string_content(&list->items[i].value)); return; } } @@ -534,7 +534,7 @@ static void tckvlist_assert_key(TCKVList *list, char *key, char *value) { // get_tags returns the list of tags static void test_task_taskmap(void) { TCReplica *rep = tc_replica_new_in_memory(); - TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_NULL(tc_replica_error(rep).ptr); TCTask *task = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("my task")); TEST_ASSERT_NOT_NULL(task); diff --git a/integration-tests/src/bindings_tests/uuid.c b/integration-tests/src/bindings_tests/uuid.c index 0c9b72be1..83b02482d 100644 --- a/integration-tests/src/bindings_tests/uuid.c +++ b/integration-tests/src/bindings_tests/uuid.c @@ -22,11 +22,11 @@ static void test_uuid_to_buf(void) { // converting UUIDs to a buf works static void test_uuid_to_str(void) { TCUuid u = tc_uuid_nil(); - TCString *s = tc_uuid_to_str(u); + TCString s = tc_uuid_to_str(u); TEST_ASSERT_EQUAL_STRING( "00000000-0000-0000-0000-000000000000", - tc_string_content(s)); - tc_string_free(s); + tc_string_content(&s)); + tc_string_free(&s); } // converting valid UUIDs from string works diff --git a/lib/src/annotation.rs b/lib/src/annotation.rs index 7142eff62..774f94478 100644 --- a/lib/src/annotation.rs +++ b/lib/src/annotation.rs @@ -22,24 +22,24 @@ pub struct TCAnnotation { /// Time the annotation was made. Must be nonzero. pub entry: libc::time_t, /// Content of the annotation. Must not be NULL. - pub description: *mut TCString<'static>, + pub description: TCString, } impl PassByValue for TCAnnotation { // NOTE: we cannot use `RustType = Annotation` here because conversion of the - // TCString to a String can fail. - type RustType = (DateTime, TCString<'static>); + // Rust to a String can fail. + type RustType = (DateTime, RustString<'static>); - unsafe fn from_ctype(self) -> Self::RustType { + unsafe fn from_ctype(mut self) -> Self::RustType { // SAFETY: // - any time_t value is valid // - time_t is copy, so ownership is not important let entry = unsafe { libc::time_t::val_from_arg(self.entry) }.unwrap(); // SAFETY: - // - self.description is not NULL (field docstring) - // - self.description came from return_ptr in as_ctype + // - self.description is valid (came from return_val in as_ctype) // - self is owned, so we can take ownership of this TCString - let description = unsafe { TCString::take_from_ptr_arg(self.description) }; + let description = + unsafe { TCString::take_val_from_arg(&mut self.description, TCString::default()) }; (entry, description) } @@ -48,7 +48,7 @@ impl PassByValue for TCAnnotation { entry: libc::time_t::as_ctype(Some(entry)), // SAFETY: // - ownership of the TCString tied to ownership of Self - description: unsafe { description.return_ptr() }, + description: unsafe { TCString::return_val(description) }, } } } @@ -57,7 +57,7 @@ impl Default for TCAnnotation { fn default() -> Self { TCAnnotation { entry: 0 as libc::time_t, - description: std::ptr::null_mut(), + description: TCString::default(), } } } @@ -125,7 +125,7 @@ mod test { #[test] fn empty_list_has_non_null_pointer() { - let tcanns = TCAnnotationList::return_val(Vec::new()); + let tcanns = unsafe { TCAnnotationList::return_val(Vec::new()) }; assert!(!tcanns.items.is_null()); assert_eq!(tcanns.len, 0); assert_eq!(tcanns._capacity, 0); @@ -133,7 +133,7 @@ mod test { #[test] fn free_sets_null_pointer() { - let mut tcanns = TCAnnotationList::return_val(Vec::new()); + let mut tcanns = unsafe { TCAnnotationList::return_val(Vec::new()) }; // SAFETY: testing expected behavior unsafe { tc_annotation_list_free(&mut tcanns) }; assert!(tcanns.items.is_null()); diff --git a/lib/src/kv.rs b/lib/src/kv.rs index 1a6f61c00..716883c75 100644 --- a/lib/src/kv.rs +++ b/lib/src/kv.rs @@ -7,21 +7,21 @@ use crate::types::*; /// will be freed when it is freed with tc_kv_list_free. #[repr(C)] pub struct TCKV { - pub key: *mut TCString<'static>, - pub value: *mut TCString<'static>, + pub key: TCString, + pub value: TCString, } impl PassByValue for TCKV { - type RustType = (TCString<'static>, TCString<'static>); + type RustType = (RustString<'static>, RustString<'static>); unsafe fn from_ctype(self) -> Self::RustType { // SAFETY: // - self.key is not NULL (field docstring) // - self.key came from return_ptr in as_ctype // - self is owned, so we can take ownership of this TCString - let key = unsafe { TCString::take_from_ptr_arg(self.key) }; + let key = unsafe { TCString::val_from_arg(self.key) }; // SAFETY: (same) - let value = unsafe { TCString::take_from_ptr_arg(self.value) }; + let value = unsafe { TCString::val_from_arg(self.value) }; (key, value) } @@ -29,10 +29,10 @@ impl PassByValue for TCKV { TCKV { // SAFETY: // - ownership of the TCString tied to ownership of Self - key: unsafe { key.return_ptr() }, + key: unsafe { TCString::return_val(key) }, // SAFETY: // - ownership of the TCString tied to ownership of Self - value: unsafe { value.return_ptr() }, + value: unsafe { TCString::return_val(value) }, } } } @@ -88,7 +88,7 @@ mod test { #[test] fn empty_list_has_non_null_pointer() { - let tckvs = TCKVList::return_val(Vec::new()); + let tckvs = unsafe { TCKVList::return_val(Vec::new()) }; assert!(!tckvs.items.is_null()); assert_eq!(tckvs.len, 0); assert_eq!(tckvs._capacity, 0); @@ -96,7 +96,7 @@ mod test { #[test] fn free_sets_null_pointer() { - let mut tckvs = TCKVList::return_val(Vec::new()); + let mut tckvs = unsafe { TCKVList::return_val(Vec::new()) }; // SAFETY: testing expected behavior unsafe { tc_kv_list_free(&mut tckvs) }; assert!(tckvs.items.is_null()); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 614d57f7b..946342fb3 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -29,7 +29,7 @@ pub(crate) mod types { pub(crate) use crate::result::TCResult; pub(crate) use crate::server::TCServer; pub(crate) use crate::status::TCStatus; - pub(crate) use crate::string::{TCString, TCStringList}; + pub(crate) use crate::string::{RustString, TCString, TCStringList}; pub(crate) use crate::task::{TCTask, TCTaskList}; pub(crate) use crate::uda::{TCUda, TCUdaList, Uda}; pub(crate) use crate::uuid::{TCUuid, TCUuidList}; diff --git a/lib/src/replica.rs b/lib/src/replica.rs index a35260b51..a1d488ede 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -1,6 +1,6 @@ use crate::traits::*; use crate::types::*; -use crate::util::err_to_tcstring; +use crate::util::err_to_ruststring; use std::ptr::NonNull; use taskchampion::{Replica, StorageConfig}; @@ -34,7 +34,7 @@ pub struct TCReplica { mut_borrowed: bool, /// The error from the most recent operation, if any - error: Option>, + error: Option>, } impl PassByPointer for TCReplica {} @@ -88,7 +88,7 @@ where match f(&mut rep.inner) { Ok(v) => v, Err(e) => { - rep.error = Some(err_to_tcstring(e)); + rep.error = Some(err_to_ruststring(e)); err_value } } @@ -111,14 +111,20 @@ pub unsafe extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica { /// must free this string. #[no_mangle] pub unsafe extern "C" fn tc_replica_new_on_disk( - path: *mut TCString, - error_out: *mut *mut TCString, + path: TCString, + error_out: *mut TCString, ) -> *mut TCReplica { + if !error_out.is_null() { + // SAFETY: + // - error_out is not NULL (just checked) + // - properly aligned and valid (promised by caller) + unsafe { *error_out = TCString::default() }; + } + // SAFETY: - // - path is not NULL (promised by caller) - // - path is return from a tc_string_.. so is valid + // - path is valid (promised by caller) // - caller will not use path after this call (convention) - let path = unsafe { TCString::take_from_ptr_arg(path) }; + let path = unsafe { TCString::val_from_arg(path) }; let storage_res = StorageConfig::OnDisk { taskdb_dir: path.to_path_buf(), } @@ -130,11 +136,9 @@ pub unsafe extern "C" fn tc_replica_new_on_disk( if !error_out.is_null() { unsafe { // SAFETY: - // - return_ptr: caller promises to free this string - // - *error_out: - // - error_out is not NULL (checked) - // - error_out points to a valid place for a pointer (caller promises) - *error_out = err_to_tcstring(e).return_ptr(); + // - error_out is not NULL (just checked) + // - properly aligned and valid (promised by caller) + TCString::val_to_arg_out(err_to_ruststring(e), error_out); } } return std::ptr::null_mut(); @@ -169,7 +173,9 @@ pub unsafe extern "C" fn tc_replica_all_tasks(rep: *mut TCReplica) -> TCTaskList .expect("TCTask::return_ptr returned NULL") }) .collect(); - Ok(TCTaskList::return_val(tasks)) + // SAFETY: + // - value is not allocated and need not be freed + Ok(unsafe { TCTaskList::return_val(tasks) }) }, TCTaskList::null_value(), ) @@ -178,6 +184,8 @@ pub unsafe extern "C" fn tc_replica_all_tasks(rep: *mut TCReplica) -> TCTaskList /// Get a list of all uuids for tasks in the replica. /// /// Returns a TCUuidList with a NULL items field on error. +/// +/// The caller must free the UUID list with `tc_uuid_list_free`. #[no_mangle] pub unsafe extern "C" fn tc_replica_all_task_uuids(rep: *mut TCReplica) -> TCUuidList { wrap( @@ -186,9 +194,13 @@ pub unsafe extern "C" fn tc_replica_all_task_uuids(rep: *mut TCReplica) -> TCUui let uuids: Vec<_> = rep .all_task_uuids()? .drain(..) - .map(TCUuid::return_val) + // SAFETY: + // - value is not allocated and need not be freed + .map(|uuid| unsafe { TCUuid::return_val(uuid) }) .collect(); - Ok(TCUuidList::return_val(uuids)) + // SAFETY: + // - value will be freed (promised by caller) + Ok(unsafe { TCUuidList::return_val(uuids) }) }, TCUuidList::null_value(), ) @@ -244,13 +256,12 @@ pub unsafe extern "C" fn tc_replica_get_task(rep: *mut TCReplica, tcuuid: TCUuid pub unsafe extern "C" fn tc_replica_new_task( rep: *mut TCReplica, status: TCStatus, - description: *mut TCString, + description: TCString, ) -> *mut TCTask { // SAFETY: - // - description is not NULL (promised by caller) - // - description is return from a tc_string_.. so is valid + // - description is valid (promised by caller) // - caller will not use description after this call (convention) - let description = unsafe { TCString::take_from_ptr_arg(description) }; + let mut description = unsafe { TCString::val_from_arg(description) }; wrap( rep, |rep| { @@ -369,23 +380,23 @@ pub unsafe extern "C" fn tc_replica_rebuild_working_set( ) } -/// Get the latest error for a replica, or NULL if the last operation succeeded. Subsequent calls -/// to this function will return NULL. The rep pointer must not be NULL. The caller must free the -/// returned string. +/// Get the latest error for a replica, or a string with NULL ptr if no error exists. Subsequent +/// calls to this function will return NULL. The rep pointer must not be NULL. The caller must +/// free the returned string. #[no_mangle] -pub unsafe extern "C" fn tc_replica_error(rep: *mut TCReplica) -> *mut TCString<'static> { +pub unsafe extern "C" fn tc_replica_error(rep: *mut TCReplica) -> TCString { // SAFETY: // - rep is not NULL (promised by caller) // - *rep is a valid TCReplica (promised by caller) // - rep is valid for the duration of this function // - rep is not modified by anything else (not threadsafe) let rep: &mut TCReplica = unsafe { TCReplica::from_ptr_arg_ref_mut(rep) }; - if let Some(tcstring) = rep.error.take() { + if let Some(rstring) = rep.error.take() { // SAFETY: // - caller promises to free this string - unsafe { tcstring.return_ptr() } + unsafe { TCString::return_val(rstring) } } else { - std::ptr::null_mut() + TCString::default() } } diff --git a/lib/src/server.rs b/lib/src/server.rs index 74feff34f..711fcacf5 100644 --- a/lib/src/server.rs +++ b/lib/src/server.rs @@ -1,6 +1,6 @@ use crate::traits::*; use crate::types::*; -use crate::util::err_to_tcstring; +use crate::util::err_to_ruststring; use taskchampion::{Server, ServerConfig}; /// TCServer represents an interface to a sync server. Aside from new and free, a server @@ -26,20 +26,26 @@ impl AsMut> for TCServer { } /// Utility function to allow using `?` notation to return an error value. -fn wrap(f: F, error_out: *mut *mut TCString, err_value: T) -> T +fn wrap(f: F, error_out: *mut TCString, err_value: T) -> T where F: FnOnce() -> anyhow::Result, { + if !error_out.is_null() { + // SAFETY: + // - error_out is not NULL (just checked) + // - properly aligned and valid (promised by caller) + unsafe { *error_out = TCString::default() }; + } + match f() { Ok(v) => v, Err(e) => { if !error_out.is_null() { // SAFETY: - // - error_out is not NULL (checked) - // - ..and points to a valid pointer (promised by caller) - // - caller will free this string (promised by caller) + // - error_out is not NULL (just checked) + // - properly aligned and valid (promised by caller) unsafe { - *error_out = err_to_tcstring(e).return_ptr(); + TCString::val_to_arg_out(err_to_ruststring(e), error_out); } } err_value @@ -56,16 +62,15 @@ where /// The server must be freed after it is used - tc_replica_sync does not automatically free it. #[no_mangle] pub unsafe extern "C" fn tc_server_new_local( - server_dir: *mut TCString, - error_out: *mut *mut TCString, + server_dir: TCString, + error_out: *mut TCString, ) -> *mut TCServer { wrap( || { // SAFETY: - // - server_dir is not NULL (promised by caller) - // - server_dir is return from a tc_string_.. so is valid + // - server_dir is valid (promised by caller) // - caller will not use server_dir after this call (convention) - let server_dir = unsafe { TCString::take_from_ptr_arg(server_dir) }; + let server_dir = unsafe { TCString::val_from_arg(server_dir) }; let server_config = ServerConfig::Local { server_dir: server_dir.to_path_buf(), }; @@ -87,30 +92,26 @@ pub unsafe extern "C" fn tc_server_new_local( /// The server must be freed after it is used - tc_replica_sync does not automatically free it. #[no_mangle] pub unsafe extern "C" fn tc_server_new_remote( - origin: *mut TCString, + origin: TCString, client_key: TCUuid, - encryption_secret: *mut TCString, - error_out: *mut *mut TCString, + encryption_secret: TCString, + error_out: *mut TCString, ) -> *mut TCServer { wrap( || { - debug_assert!(!origin.is_null()); - debug_assert!(!encryption_secret.is_null()); // SAFETY: - // - origin is not NULL // - origin is valid (promised by caller) // - origin ownership is transferred to this function - let origin = unsafe { TCString::take_from_ptr_arg(origin) }.into_string()?; + let origin = unsafe { TCString::val_from_arg(origin) }.into_string()?; // SAFETY: // - client_key is a valid Uuid (any 8-byte sequence counts) let client_key = unsafe { TCUuid::val_from_arg(client_key) }; // SAFETY: - // - encryption_secret is not NULL // - encryption_secret is valid (promised by caller) // - encryption_secret ownership is transferred to this function - let encryption_secret = unsafe { TCString::take_from_ptr_arg(encryption_secret) } + let encryption_secret = unsafe { TCString::val_from_arg(encryption_secret) } .as_bytes() .to_vec(); diff --git a/lib/src/string.rs b/lib/src/string.rs index 70e4795ee..3767f7bb2 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -1,9 +1,9 @@ use crate::traits::*; +use crate::util::{string_into_raw_parts, vec_into_raw_parts}; use std::ffi::{CStr, CString, OsStr}; +use std::os::raw::c_char; use std::os::unix::ffi::OsStrExt; use std::path::PathBuf; -use std::ptr::NonNull; -use std::str::Utf8Error; /// TCString supports passing strings into and out of the TaskChampion API. /// @@ -22,98 +22,273 @@ use std::str::Utf8Error; /// /// # Safety /// -/// When a `*TCString` appears as a return value or output argument, ownership is passed to the +/// The `ptr` field may be checked for NULL, where documentation indicates this is possible. All +/// other fields in a TCString are private and must not be used from C. They exist in the struct +/// to ensure proper allocation and alignment. +/// +/// When a `TCString` appears as a return value or output argument, ownership is passed to the /// caller. The caller must pass that ownership back to another function or free the string. /// -/// Any function taking a `*TCString` requires: +/// Any function taking a `TCString` requires: /// - the pointer must not be NUL; /// - the pointer must be one previously returned from a tc_… function; and /// - the memory referenced by the pointer must never be modified by C code. /// -/// Unless specified otherwise, TaskChampion functions take ownership of a `*TCString` when it is -/// given as a function argument, and the pointer is invalid when the function returns. Callers -/// must not use or free TCStrings after passing them to such API functions. +/// Unless specified otherwise, TaskChampion functions take ownership of a `TCString` when it is +/// given as a function argument, and the caller must not use or free TCStrings after passing them +/// to such API functions. +/// +/// A TCString with a NULL `ptr` field need not be freed, although tc_free_string will not fail +/// for such a value. /// /// TCString is not threadsafe. +/// cbindgen:field-names=[ptr, _u1, _u2, _u3] +#[repr(C)] +pub struct TCString { + // defined based on the type + ptr: *mut libc::c_void, + len: usize, + cap: usize, + + // type of TCString this represents + ty: u8, +} + +// TODO: figure out how to ignore this but still use it in TCString +/// A discriminator for TCString +#[repr(u8)] +enum TCStringType { + /// Null. Nothing is contained in this string. + /// + /// * `ptr` is NULL. + /// * `len` and `cap` are zero. + Null = 0, + + /// A CString. + /// + /// * `ptr` is the result of CString::into_raw, containing a terminating NUL. It may not be + /// valid UTF-8. + /// * `len` and `cap` are zero. + CString, + + /// A CStr, referencing memory borrowed from C + /// + /// * `ptr` points to the string, containing a terminating NUL. It may not be valid UTF-8. + /// * `len` and `cap` are zero. + CStr, + + /// A String. + /// + /// * `ptr`, `len`, and `cap` are as would be returned from String::into_raw_parts. + String, + + /// A byte sequence. + /// + /// * `ptr`, `len`, and `cap` are as would be returned from Vec::into_raw_parts. + Bytes, +} + +impl Default for TCString { + fn default() -> Self { + TCString { + ptr: std::ptr::null_mut(), + len: 0, + cap: 0, + ty: TCStringType::Null as u8, + } + } +} + +impl TCString { + pub(crate) fn is_null(&self) -> bool { + self.ptr.is_null() + } +} + #[derive(PartialEq, Debug)] -pub enum TCString<'a> { +pub enum RustString<'a> { + Null, CString(CString), CStr(&'a CStr), String(String), - - /// This variant denotes an input string that was not valid UTF-8. This allows reporting this - /// error when the string is read, with the constructor remaining infallible. - InvalidUtf8(Utf8Error, Vec), - - /// None is the default value for TCString, but this variant is never seen by C code or by Rust - /// code outside of this module. - None, + Bytes(Vec), } -impl<'a> Default for TCString<'a> { +impl<'a> Default for RustString<'a> { fn default() -> Self { - TCString::None + RustString::Null } } -impl<'a> PassByPointer for TCString<'a> {} +impl PassByValue for TCString { + type RustType = RustString<'static>; -impl<'a> TCString<'a> { - /// Get a regular Rust &str for this value. - pub(crate) fn as_str(&self) -> Result<&str, std::str::Utf8Error> { - match self { - TCString::CString(cstring) => cstring.as_c_str().to_str(), - TCString::CStr(cstr) => cstr.to_str(), - TCString::String(string) => Ok(string.as_ref()), - TCString::InvalidUtf8(e, _) => Err(*e), - TCString::None => unreachable!(), + unsafe fn from_ctype(self) -> Self::RustType { + match self.ty { + ty if ty == TCStringType::CString as u8 => { + // SAFETY: + // - ptr was derived from CString::into_raw + // - data was not modified since that time (caller promises) + RustString::CString(unsafe { CString::from_raw(self.ptr as *mut c_char) }) + } + ty if ty == TCStringType::CStr as u8 => { + // SAFETY: + // - ptr was created by CStr::as_ptr + // - data was not modified since that time (caller promises) + RustString::CStr(unsafe { CStr::from_ptr(self.ptr as *mut c_char) }) + } + ty if ty == TCStringType::String as u8 => { + // SAFETY: + // - ptr was created by string_into_raw_parts + // - data was not modified since that time (caller promises) + RustString::String(unsafe { + String::from_raw_parts(self.ptr as *mut u8, self.len, self.cap) + }) + } + ty if ty == TCStringType::Bytes as u8 => { + // SAFETY: + // - ptr was created by vec_into_raw_parts + // - data was not modified since that time (caller promises) + RustString::Bytes(unsafe { + Vec::from_raw_parts(self.ptr as *mut u8, self.len, self.cap) + }) + } + _ => RustString::Null, } } - /// Consume this TCString and return an equivalent String, or an error if not - /// valid UTF-8. In the error condition, the original data is lost. - pub(crate) fn into_string(self) -> Result { + fn as_ctype(arg: Self::RustType) -> Self { + match arg { + RustString::Null => Self { + ty: TCStringType::Null as u8, + ..Default::default() + }, + RustString::CString(cstring) => Self { + ty: TCStringType::CString as u8, + ptr: cstring.into_raw() as *mut libc::c_void, + ..Default::default() + }, + RustString::CStr(cstr) => Self { + ty: TCStringType::CStr as u8, + ptr: cstr.as_ptr() as *mut libc::c_void, + ..Default::default() + }, + RustString::String(string) => { + let (ptr, len, cap) = string_into_raw_parts(string); + Self { + ty: TCStringType::String as u8, + ptr: ptr as *mut libc::c_void, + len, + cap, + } + } + RustString::Bytes(bytes) => { + let (ptr, len, cap) = vec_into_raw_parts(bytes); + Self { + ty: TCStringType::Bytes as u8, + ptr: ptr as *mut libc::c_void, + len, + cap, + } + } + } + } +} + +impl<'a> RustString<'a> { + /// Get a regular Rust &str for this value. + pub(crate) fn as_str(&mut self) -> Result<&str, std::str::Utf8Error> { match self { - TCString::CString(cstring) => cstring.into_string().map_err(|e| e.utf8_error()), - TCString::CStr(cstr) => cstr.to_str().map(|s| s.to_string()), - TCString::String(string) => Ok(string), - TCString::InvalidUtf8(e, _) => Err(e), - TCString::None => unreachable!(), + RustString::CString(cstring) => cstring.as_c_str().to_str(), + RustString::CStr(cstr) => cstr.to_str(), + RustString::String(ref string) => Ok(string.as_ref()), + RustString::Bytes(_) => { + self.bytes_to_string()?; + self.as_str() // now the String variant, so won't recurse + } + RustString::Null => unreachable!(), + } + } + + /// Consume this RustString and return an equivalent String, or an error if not + /// valid UTF-8. In the error condition, the original data is lost. + pub(crate) fn into_string(mut self) -> Result { + match self { + RustString::CString(cstring) => cstring.into_string().map_err(|e| e.utf8_error()), + RustString::CStr(cstr) => cstr.to_str().map(|s| s.to_string()), + RustString::String(string) => Ok(string), + RustString::Bytes(_) => { + self.bytes_to_string()?; + self.into_string() // now the String variant, so won't recurse + } + RustString::Null => unreachable!(), } } pub(crate) fn as_bytes(&self) -> &[u8] { match self { - TCString::CString(cstring) => cstring.as_bytes(), - TCString::CStr(cstr) => cstr.to_bytes(), - TCString::String(string) => string.as_bytes(), - TCString::InvalidUtf8(_, data) => data.as_ref(), - TCString::None => unreachable!(), + RustString::CString(cstring) => cstring.as_bytes(), + RustString::CStr(cstr) => cstr.to_bytes(), + RustString::String(string) => string.as_bytes(), + RustString::Bytes(bytes) => bytes.as_ref(), + RustString::Null => unreachable!(), } } - /// Convert the TCString, in place, into one of the C variants. If this is not + /// Convert the RustString, in place, from the Bytes to String variant. On successful return, + /// the RustString has variant RustString::String. + fn bytes_to_string(&mut self) -> Result<(), std::str::Utf8Error> { + let mut owned = RustString::Null; + // temporarily swap a Null value into self; we'll swap that back + // shortly. + std::mem::swap(self, &mut owned); + match owned { + RustString::Bytes(bytes) => match String::from_utf8(bytes) { + Ok(string) => { + *self = RustString::String(string); + Ok(()) + } + Err(e) => { + let (e, bytes) = (e.utf8_error(), e.into_bytes()); + // put self back as we found it + *self = RustString::Bytes(bytes); + Err(e) + } + }, + _ => { + // not bytes, so just swap back + std::mem::swap(self, &mut owned); + Ok(()) + } + } + } + + /// Convert the RustString, in place, into one of the C variants. If this is not /// possible, such as if the string contains an embedded NUL, then the string /// remains unchanged. - fn to_c_string_mut(&mut self) { - if matches!(self, TCString::String(_)) { - // we must take ownership of the String in order to try converting it, - // leaving the underlying TCString as its default (None) - if let TCString::String(string) = std::mem::take(self) { + fn string_to_cstring(&mut self) { + let mut owned = RustString::Null; + // temporarily swap a Null value into self; we'll swap that back shortly + std::mem::swap(self, &mut owned); + match owned { + RustString::String(string) => { match CString::new(string) { - Ok(cstring) => *self = TCString::CString(cstring), + Ok(cstring) => { + *self = RustString::CString(cstring); + } Err(nul_err) => { // recover the underlying String from the NulError and restore - // the TCString + // the RustString let original_bytes = nul_err.into_vec(); // SAFETY: original_bytes came from a String moments ago, so still valid utf8 let string = unsafe { String::from_utf8_unchecked(original_bytes) }; - *self = TCString::String(string); + *self = RustString::String(string); } } - } else { - // the `matches!` above verified self was a TCString::String - unreachable!() + } + _ => { + // not a CString, so just swap back + std::mem::swap(self, &mut owned); } } } @@ -125,18 +300,55 @@ impl<'a> TCString<'a> { } } -impl<'a> From for TCString<'a> { - fn from(string: String) -> TCString<'a> { - TCString::String(string) +impl<'a> From for RustString<'a> { + fn from(string: String) -> RustString<'a> { + RustString::String(string) } } -impl<'a> From<&str> for TCString<'static> { - fn from(string: &str) -> TCString<'static> { - TCString::String(string.to_string()) +impl<'a> From<&str> for RustString<'static> { + fn from(string: &str) -> RustString<'static> { + RustString::String(string.to_string()) } } +/// Utility function to borrow a TCString from a pointer arg, modify it, +/// and restore it. +/// +/// This implements a kind of "interior mutability", relying on the +/// single-threaded use of all TC* types. +/// +/// # SAFETY +/// +/// - tcstring must not be NULL +/// - *tcstring must be a valid TCString +/// - *tcstring must not be accessed by anything else, despite the *const +unsafe fn wrap(tcstring: *const TCString, f: F) -> T +where + F: FnOnce(&mut RustString) -> T, +{ + debug_assert!(!tcstring.is_null()); + + // SAFETY: + // - we have exclusive to *tcstring (promised by caller) + let tcstring = tcstring as *mut TCString; + + // SAFETY: + // - tcstring is not NULL + // - *tcstring is a valid string (promised by caller) + let mut rstring = unsafe { TCString::take_val_from_arg(tcstring, TCString::default()) }; + + let rv = f(&mut rstring); + + // update the caller's TCString with the updated RustString + // SAFETY: + // - tcstring is not NULL (we just took from it) + // - tcstring points to valid memory (we just took from it) + unsafe { TCString::val_to_arg_out(rstring, tcstring) }; + + rv +} + /// TCStringList represents a list of strings. /// /// The content of this struct must be treated as read-only. @@ -151,11 +363,11 @@ pub struct TCStringList { /// TCStringList representing each string. these remain owned by the TCStringList instance and will /// be freed by tc_string_list_free. This pointer is never NULL for a valid TCStringList, and the /// *TCStringList at indexes 0..len-1 are not NULL. - items: *const NonNull>, + items: *const TCString, } impl CList for TCStringList { - type Element = NonNull>; + type Element = TCString; unsafe fn from_raw_parts(items: *const Self::Element, len: usize, cap: usize) -> Self { TCStringList { @@ -185,7 +397,7 @@ impl CList for TCStringList { /// free(url); // string is no longer referenced and can be freed /// ``` #[no_mangle] -pub unsafe extern "C" fn tc_string_borrow(cstr: *const libc::c_char) -> *mut TCString<'static> { +pub unsafe extern "C" fn tc_string_borrow(cstr: *const libc::c_char) -> TCString { debug_assert!(!cstr.is_null()); // SAFETY: // - cstr is not NULL (promised by caller, verified by assertion) @@ -195,13 +407,13 @@ pub unsafe extern "C" fn tc_string_borrow(cstr: *const libc::c_char) -> *mut TCS let cstr: &CStr = unsafe { CStr::from_ptr(cstr) }; // SAFETY: // - caller promises to free this string - unsafe { TCString::CStr(cstr).return_ptr() } + unsafe { TCString::return_val(RustString::CStr(cstr)) } } /// Create a new TCString by cloning the content of the given C string. The resulting TCString /// is independent of the given string, which can be freed or overwritten immediately. #[no_mangle] -pub unsafe extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> *mut TCString<'static> { +pub unsafe extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> TCString { debug_assert!(!cstr.is_null()); // SAFETY: // - cstr is not NULL (promised by caller, verified by assertion) @@ -209,21 +421,24 @@ pub unsafe extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> *mut TCSt // - cstr contains a valid NUL terminator (promised by caller) // - cstr's content will not change before it is destroyed (by C convention) let cstr: &CStr = unsafe { CStr::from_ptr(cstr) }; + let cstring: CString = cstr.into(); // SAFETY: // - caller promises to free this string - unsafe { TCString::CString(cstr.into()).return_ptr() } + unsafe { TCString::return_val(RustString::CString(cstring)) } } /// Create a new TCString containing the given string with the given length. This allows creation /// of strings containing embedded NUL characters. As with `tc_string_clone`, the resulting /// TCString is independent of the passed buffer, which may be reused or freed immediately. /// +/// The length should _not_ include any trailing NUL. +/// /// The given length must be less than half the maximum value of usize. #[no_mangle] pub unsafe extern "C" fn tc_string_clone_with_len( buf: *const libc::c_char, len: usize, -) -> *mut TCString<'static> { +) -> TCString { debug_assert!(!buf.is_null()); debug_assert!(len < isize::MAX as usize); // SAFETY: @@ -237,47 +452,45 @@ pub unsafe extern "C" fn tc_string_clone_with_len( // allocate and copy into Rust-controlled memory let vec = slice.to_vec(); - // try converting to a string, which is the only variant that can contain embedded NULs. If - // the bytes are not valid utf-8, store that information for reporting later. - let tcstring = match String::from_utf8(vec) { - Ok(string) => TCString::String(string), - Err(e) => { - let (e, vec) = (e.utf8_error(), e.into_bytes()); - TCString::InvalidUtf8(e, vec) - } - }; - // SAFETY: // - caller promises to free this string - unsafe { tcstring.return_ptr() } + unsafe { TCString::return_val(RustString::Bytes(vec)) } } -/// Get the content of the string as a regular C string. The given string must not be NULL. The +/// Get the content of the string as a regular C string. The given string must be valid. The /// returned value is NULL if the string contains NUL bytes or (in some cases) invalid UTF-8. The /// returned C string is valid until the TCString is freed or passed to another TC API function. /// /// In general, prefer [`tc_string_content_with_len`] except when it's certain that the string is /// valid and NUL-free. /// +/// This function takes the TCString by pointer because it may be modified in-place to add a NUL +/// terminator. The pointer must not be NULL. +/// /// This function does _not_ take ownership of the TCString. #[no_mangle] -pub unsafe extern "C" fn tc_string_content(tcstring: *mut TCString) -> *const libc::c_char { - // SAFETY: +pub unsafe extern "C" fn tc_string_content(tcstring: *const TCString) -> *const libc::c_char { + // SAFETY; // - tcstring is not NULL (promised by caller) - // - lifetime of tcstring outlives the lifetime of this function - // - lifetime of tcstring outlives the lifetime of the returned pointer (promised by caller) - let tcstring = unsafe { TCString::from_ptr_arg_ref_mut(tcstring) }; + // - *tcstring is valid (promised by caller) + // - *tcstring is not accessed concurrently (single-threaded) + unsafe { + wrap(tcstring, |rstring| { + // try to eliminate the Bytes variant. If this fails, we'll return NULL + // below, so the error is ignorable. + let _ = rstring.bytes_to_string(); - // if we have a String, we need to consume it and turn it into - // a CString. - tcstring.to_c_string_mut(); + // and eliminate the String variant + rstring.string_to_cstring(); - match tcstring { - TCString::CString(cstring) => cstring.as_ptr(), - TCString::String(_) => std::ptr::null(), // to_c_string_mut failed - TCString::CStr(cstr) => cstr.as_ptr(), - TCString::InvalidUtf8(_, _) => std::ptr::null(), - TCString::None => unreachable!(), + match &rstring { + RustString::CString(cstring) => cstring.as_ptr(), + RustString::String(_) => std::ptr::null(), // string_to_cstring failed + RustString::CStr(cstr) => cstr.as_ptr(), + RustString::Bytes(_) => std::ptr::null(), // already returned above + RustString::Null => unreachable!(), + } + }) } } @@ -286,26 +499,31 @@ pub unsafe extern "C" fn tc_string_content(tcstring: *mut TCString) -> *const li /// returned buffer is valid until the TCString is freed or passed to another TaskChampio /// function. /// +/// This function takes the TCString by pointer because it may be modified in-place to add a NUL +/// terminator. The pointer must not be NULL. +/// /// This function does _not_ take ownership of the TCString. #[no_mangle] pub unsafe extern "C" fn tc_string_content_with_len( - tcstring: *mut TCString, + tcstring: *const TCString, len_out: *mut usize, ) -> *const libc::c_char { - // SAFETY: + // SAFETY; // - tcstring is not NULL (promised by caller) - // - lifetime of tcstring outlives the lifetime of this function - // - lifetime of tcstring outlives the lifetime of the returned pointer (promised by caller) - let tcstring = unsafe { TCString::from_ptr_arg_ref(tcstring) }; + // - *tcstring is valid (promised by caller) + // - *tcstring is not accessed concurrently (single-threaded) + unsafe { + wrap(tcstring, |rstring| { + let bytes = rstring.as_bytes(); - let bytes = tcstring.as_bytes(); - - // SAFETY: - // - len_out is not NULL (promised by caller) - // - len_out points to valid memory (promised by caller) - // - len_out is properly aligned (C convention) - unsafe { usize::val_to_arg_out(bytes.len(), len_out) }; - bytes.as_ptr() as *const libc::c_char + // SAFETY: + // - len_out is not NULL (promised by caller) + // - len_out points to valid memory (promised by caller) + // - len_out is properly aligned (C convention) + usize::val_to_arg_out(bytes.len(), len_out); + bytes.as_ptr() as *const libc::c_char + }) + } } /// Free a TCString. The given string must not be NULL. The string must not be used @@ -315,7 +533,7 @@ pub unsafe extern "C" fn tc_string_free(tcstring: *mut TCString) { // SAFETY: // - tcstring is not NULL (promised by caller) // - caller is exclusive owner of tcstring (promised by caller) - drop(unsafe { TCString::take_from_ptr_arg(tcstring) }); + drop(unsafe { TCString::take_val_from_arg(tcstring, TCString::default()) }); } /// Free a TCStringList instance. The instance, and all TCStringList it contains, must not be used after @@ -328,7 +546,7 @@ pub unsafe extern "C" fn tc_string_list_free(tcstrings: *mut TCStringList) { // - tcstrings is not NULL and points to a valid TCStringList (caller is not allowed to // modify the list) // - caller promises not to use the value after return - unsafe { drop_pointer_list(tcstrings) }; + unsafe { drop_value_list(tcstrings) }; } #[cfg(test)] @@ -338,7 +556,7 @@ mod test { #[test] fn empty_list_has_non_null_pointer() { - let tcstrings = TCStringList::return_val(Vec::new()); + let tcstrings = unsafe { TCStringList::return_val(Vec::new()) }; assert!(!tcstrings.items.is_null()); assert_eq!(tcstrings.len, 0); assert_eq!(tcstrings._capacity, 0); @@ -346,7 +564,7 @@ mod test { #[test] fn free_sets_null_pointer() { - let mut tcstrings = TCStringList::return_val(Vec::new()); + let mut tcstrings = unsafe { TCStringList::return_val(Vec::new()) }; // SAFETY: testing expected behavior unsafe { tc_string_list_free(&mut tcstrings) }; assert!(tcstrings.items.is_null()); @@ -356,26 +574,29 @@ mod test { const INVALID_UTF8: &[u8] = b"abc\xf0\x28\x8c\x28"; - fn make_cstring() -> TCString<'static> { - TCString::CString(CString::new("a string").unwrap()) + fn make_cstring() -> RustString<'static> { + RustString::CString(CString::new("a string").unwrap()) } - fn make_cstr() -> TCString<'static> { + fn make_cstr() -> RustString<'static> { let cstr = CStr::from_bytes_with_nul(b"a string\0").unwrap(); - TCString::CStr(&cstr) + RustString::CStr(&cstr) } - fn make_string() -> TCString<'static> { - TCString::String("a string".into()) + fn make_string() -> RustString<'static> { + RustString::String("a string".into()) } - fn make_string_with_nul() -> TCString<'static> { - TCString::String("a \0 nul!".into()) + fn make_string_with_nul() -> RustString<'static> { + RustString::String("a \0 nul!".into()) } - fn make_invalid() -> TCString<'static> { - let e = String::from_utf8(INVALID_UTF8.to_vec()).unwrap_err(); - TCString::InvalidUtf8(e.utf8_error(), e.into_bytes()) + fn make_invalid_bytes() -> RustString<'static> { + RustString::Bytes(INVALID_UTF8.to_vec()) + } + + fn make_bytes() -> RustString<'static> { + RustString::Bytes(b"bytes".to_vec()) } #[test] @@ -399,11 +620,16 @@ mod test { } #[test] - fn invalid_as_str() { - let as_str_err = make_invalid().as_str().unwrap_err(); + fn invalid_bytes_as_str() { + let as_str_err = make_invalid_bytes().as_str().unwrap_err(); assert_eq!(as_str_err.valid_up_to(), 3); // "abc" is valid } + #[test] + fn valid_bytes_as_str() { + assert_eq!(make_bytes().as_str().unwrap(), "bytes"); + } + #[test] fn cstring_as_bytes() { assert_eq!(make_cstring().as_bytes(), b"a string"); @@ -425,42 +651,42 @@ mod test { } #[test] - fn invalid_as_bytes() { - assert_eq!(make_invalid().as_bytes(), INVALID_UTF8); + fn invalid_bytes_as_bytes() { + assert_eq!(make_invalid_bytes().as_bytes(), INVALID_UTF8); } #[test] - fn cstring_to_c_string_mut() { + fn cstring_string_to_cstring() { let mut tcstring = make_cstring(); - tcstring.to_c_string_mut(); + tcstring.string_to_cstring(); assert_eq!(tcstring, make_cstring()); // unchanged } #[test] - fn cstr_to_c_string_mut() { + fn cstr_string_to_cstring() { let mut tcstring = make_cstr(); - tcstring.to_c_string_mut(); + tcstring.string_to_cstring(); assert_eq!(tcstring, make_cstr()); // unchanged } #[test] - fn string_to_c_string_mut() { + fn string_string_to_cstring() { let mut tcstring = make_string(); - tcstring.to_c_string_mut(); + tcstring.string_to_cstring(); assert_eq!(tcstring, make_cstring()); // converted to CString, same content } #[test] - fn string_with_nul_to_c_string_mut() { + fn string_with_nul_string_to_cstring() { let mut tcstring = make_string_with_nul(); - tcstring.to_c_string_mut(); + tcstring.string_to_cstring(); assert_eq!(tcstring, make_string_with_nul()); // unchanged } #[test] - fn invalid_to_c_string_mut() { - let mut tcstring = make_invalid(); - tcstring.to_c_string_mut(); - assert_eq!(tcstring, make_invalid()); // unchanged + fn bytes_string_to_cstring() { + let mut tcstring = make_bytes(); + tcstring.string_to_cstring(); + assert_eq!(tcstring, make_bytes()); // unchanged } } diff --git a/lib/src/task.rs b/lib/src/task.rs index ccea9fa20..c3c0b896b 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -1,6 +1,6 @@ use crate::traits::*; use crate::types::*; -use crate::util::err_to_tcstring; +use crate::util::err_to_ruststring; use chrono::{TimeZone, Utc}; use std::convert::TryFrom; use std::ops::Deref; @@ -42,7 +42,7 @@ pub struct TCTask { inner: Inner, /// The error from the most recent operation, if any - error: Option>, + error: Option>, } enum Inner { @@ -154,17 +154,17 @@ where match f(task) { Ok(rv) => rv, Err(e) => { - tctask.error = Some(err_to_tcstring(e)); + tctask.error = Some(err_to_ruststring(e)); err_value } } } -impl TryFrom> for Tag { +impl TryFrom> for Tag { type Error = anyhow::Error; - fn try_from(tcstring: TCString) -> Result { - let tagstr = tcstring.as_str()?; + fn try_from(mut rstring: RustString) -> Result { + let tagstr = rstring.as_str()?; Tag::from_str(tagstr) } } @@ -248,7 +248,11 @@ pub unsafe extern "C" fn tc_task_to_immut(task: *mut TCTask) { /// Get a task's UUID. #[no_mangle] pub unsafe extern "C" fn tc_task_get_uuid(task: *mut TCTask) -> TCUuid { - wrap(task, |task| TCUuid::return_val(task.get_uuid())) + wrap(task, |task| { + // SAFETY: + // - value is not allocated and need not be freed + unsafe { TCUuid::return_val(task.get_uuid()) } + }) } /// Get a task's status. @@ -259,7 +263,7 @@ pub unsafe extern "C" fn tc_task_get_status(task: *mut TCTask) -> TCStatus { /// Get the underlying key/value pairs for this task. The returned TCKVList is /// a "snapshot" of the task and will not be updated if the task is subsequently -/// modified. +/// modified. It is the caller's responsibility to free the TCKVList. #[no_mangle] pub unsafe extern "C" fn tc_task_get_taskmap(task: *mut TCTask) -> TCKVList { wrap(task, |task| { @@ -267,24 +271,26 @@ pub unsafe extern "C" fn tc_task_get_taskmap(task: *mut TCTask) -> TCKVList { .get_taskmap() .iter() .map(|(k, v)| { - let key = TCString::from(k.as_ref()); - let value = TCString::from(v.as_ref()); + let key = RustString::from(k.as_ref()); + let value = RustString::from(v.as_ref()); TCKV::as_ctype((key, value)) }) .collect(); - TCKVList::return_val(vec) + // SAFETY: + // - caller will free this list + unsafe { TCKVList::return_val(vec) } }) } /// Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it /// contains embedded NUL characters). #[no_mangle] -pub unsafe extern "C" fn tc_task_get_description(task: *mut TCTask) -> *mut TCString<'static> { +pub unsafe extern "C" fn tc_task_get_description(task: *mut TCTask) -> TCString { wrap(task, |task| { - let descr: TCString = task.get_description().into(); + let descr = task.get_description(); // SAFETY: // - caller promises to free this string - unsafe { descr.return_ptr() } + unsafe { TCString::return_val(descr.into()) } }) } @@ -321,12 +327,11 @@ pub unsafe extern "C" fn tc_task_is_active(task: *mut TCTask) -> bool { /// Check if a task has the given tag. If the tag is invalid, this function will return false, as /// that (invalid) tag is not present. No error will be reported via `tc_task_error`. #[no_mangle] -pub unsafe extern "C" fn tc_task_has_tag(task: *mut TCTask, tag: *mut TCString) -> bool { +pub unsafe extern "C" fn tc_task_has_tag(task: *mut TCTask, tag: TCString) -> bool { // SAFETY: - // - tag is not NULL (promised by caller) - // - tag is return from a tc_string_.. so is valid + // - tag is valid (promised by caller) // - caller will not use tag after this call (convention) - let tcstring = unsafe { TCString::take_from_ptr_arg(tag) }; + let tcstring = unsafe { TCString::val_from_arg(tag) }; wrap(task, |task| { if let Ok(tag) = Tag::try_from(tcstring) { task.has_tag(&tag) @@ -343,18 +348,17 @@ pub unsafe extern "C" fn tc_task_has_tag(task: *mut TCTask, tag: *mut TCString) #[no_mangle] pub unsafe extern "C" fn tc_task_get_tags(task: *mut TCTask) -> TCStringList { wrap(task, |task| { - let vec: Vec>> = task + let vec: Vec = task .get_tags() .map(|t| { - NonNull::new( - // SAFETY: - // - this TCString will be freed via tc_string_list_free. - unsafe { TCString::from(t.as_ref()).return_ptr() }, - ) - .expect("TCString::return_ptr() returned NULL") + // SAFETY: + // - this TCString will be freed via tc_string_list_free. + unsafe { TCString::return_val(t.as_ref().into()) } }) .collect(); - TCStringList::return_val(vec) + // SAFETY: + // - caller will free the list + unsafe { TCStringList::return_val(vec) } }) } @@ -368,39 +372,40 @@ pub unsafe extern "C" fn tc_task_get_annotations(task: *mut TCTask) -> TCAnnotat let vec: Vec = task .get_annotations() .map(|a| { - let description = TCString::from(a.description); + let description = RustString::from(a.description); TCAnnotation::as_ctype((a.entry, description)) }) .collect(); - TCAnnotationList::return_val(vec) + // SAFETY: + // - caller will free the list + unsafe { TCAnnotationList::return_val(vec) } }) } /// Get the named UDA from the task. /// -/// Returns NULL if the UDA does not exist. +/// Returns a TCString with NULL ptr field if the UDA does not exist. #[no_mangle] pub unsafe extern "C" fn tc_task_get_uda<'a>( task: *mut TCTask, - ns: *mut TCString<'a>, - key: *mut TCString<'a>, -) -> *mut TCString<'static> { + ns: TCString, + key: TCString, +) -> TCString { wrap(task, |task| { // SAFETY: - // - ns is not NULL (promised by caller) - // - ns is return from a tc_string_.. so is valid + // - ns is valid (promised by caller) // - caller will not use ns after this call (convention) - if let Ok(ns) = unsafe { TCString::take_from_ptr_arg(ns) }.as_str() { + if let Ok(ns) = unsafe { TCString::val_from_arg(ns) }.as_str() { // SAFETY: same - if let Ok(key) = unsafe { TCString::take_from_ptr_arg(key) }.as_str() { + if let Ok(key) = unsafe { TCString::val_from_arg(key) }.as_str() { if let Some(value) = task.get_uda(ns, key) { // SAFETY: // - caller will free this string (caller promises) - return unsafe { TCString::return_ptr(value.into()) }; + return unsafe { TCString::return_val(value.into()) }; } } } - std::ptr::null_mut() + TCString::default() }) } @@ -408,23 +413,19 @@ pub unsafe extern "C" fn tc_task_get_uda<'a>( /// /// Returns NULL if the UDA does not exist. #[no_mangle] -pub unsafe extern "C" fn tc_task_get_legacy_uda<'a>( - task: *mut TCTask, - key: *mut TCString<'a>, -) -> *mut TCString<'static> { +pub unsafe extern "C" fn tc_task_get_legacy_uda<'a>(task: *mut TCTask, key: TCString) -> TCString { wrap(task, |task| { // SAFETY: - // - key is not NULL (promised by caller) - // - key is return from a tc_string_.. so is valid + // - key is valid (promised by caller) // - caller will not use key after this call (convention) - if let Ok(key) = unsafe { TCString::take_from_ptr_arg(key) }.as_str() { + if let Ok(key) = unsafe { TCString::val_from_arg(key) }.as_str() { if let Some(value) = task.get_legacy_uda(key) { // SAFETY: // - caller will free this string (caller promises) - return unsafe { TCString::return_ptr(value.into()) }; + return unsafe { TCString::return_val(value.into()) }; } } - std::ptr::null_mut() + TCString::default() }) } @@ -437,35 +438,47 @@ pub unsafe extern "C" fn tc_task_get_udas(task: *mut TCTask) -> TCUdaList { let vec: Vec = task .get_udas() .map(|((ns, key), value)| { - TCUda::return_val(Uda { - ns: Some(ns.into()), - key: key.into(), - value: value.into(), - }) + // SAFETY: + // - will be freed by tc_uda_list_free + unsafe { + TCUda::return_val(Uda { + ns: Some(ns.into()), + key: key.into(), + value: value.into(), + }) + } }) .collect(); - TCUdaList::return_val(vec) + // SAFETY: + // - caller will free this list + unsafe { TCUdaList::return_val(vec) } }) } /// Get all UDAs for this task. /// /// All TCUdas in this list have a NULL ns field. The entire UDA key is -/// included in the key field. +/// included in the key field. The caller must free the returned list. #[no_mangle] pub unsafe extern "C" fn tc_task_get_legacy_udas(task: *mut TCTask) -> TCUdaList { wrap(task, |task| { let vec: Vec = task .get_legacy_udas() .map(|(key, value)| { - TCUda::return_val(Uda { - ns: None, - key: key.into(), - value: value.into(), - }) + // SAFETY: + // - will be freed by tc_uda_list_free + unsafe { + TCUda::return_val(Uda { + ns: None, + key: key.into(), + value: value.into(), + }) + } }) .collect(); - TCUdaList::return_val(vec) + // SAFETY: + // - caller will free this list + unsafe { TCUdaList::return_val(vec) } }) } @@ -486,13 +499,12 @@ pub unsafe extern "C" fn tc_task_set_status(task: *mut TCTask, status: TCStatus) #[no_mangle] pub unsafe extern "C" fn tc_task_set_description( task: *mut TCTask, - description: *mut TCString, + description: TCString, ) -> TCResult { // SAFETY: - // - description is not NULL (promised by caller) - // - description is return from a tc_string_.. so is valid + // - description is valid (promised by caller) // - caller will not use description after this call (convention) - let description = unsafe { TCString::take_from_ptr_arg(description) }; + let mut description = unsafe { TCString::val_from_arg(description) }; wrap_mut( task, |task| { @@ -606,12 +618,11 @@ pub unsafe extern "C" fn tc_task_delete(task: *mut TCTask) -> TCResult { /// Add a tag to a mutable task. #[no_mangle] -pub unsafe extern "C" fn tc_task_add_tag(task: *mut TCTask, tag: *mut TCString) -> TCResult { +pub unsafe extern "C" fn tc_task_add_tag(task: *mut TCTask, tag: TCString) -> TCResult { // SAFETY: - // - tag is not NULL (promised by caller) - // - tag is return from a tc_string_.. so is valid + // - tag is valid (promised by caller) // - caller will not use tag after this call (convention) - let tcstring = unsafe { TCString::take_from_ptr_arg(tag) }; + let tcstring = unsafe { TCString::val_from_arg(tag) }; wrap_mut( task, |task| { @@ -625,12 +636,11 @@ pub unsafe extern "C" fn tc_task_add_tag(task: *mut TCTask, tag: *mut TCString) /// Remove a tag from a mutable task. #[no_mangle] -pub unsafe extern "C" fn tc_task_remove_tag(task: *mut TCTask, tag: *mut TCString) -> TCResult { +pub unsafe extern "C" fn tc_task_remove_tag(task: *mut TCTask, tag: TCString) -> TCResult { // SAFETY: - // - tag is not NULL (promised by caller) - // - tag is return from a tc_string_.. so is valid + // - tag is valid (promised by caller) // - caller will not use tag after this call (convention) - let tcstring = unsafe { TCString::take_from_ptr_arg(tag) }; + let tcstring = unsafe { TCString::val_from_arg(tag) }; wrap_mut( task, |task| { @@ -683,19 +693,18 @@ pub unsafe extern "C" fn tc_task_remove_annotation(task: *mut TCTask, entry: i64 #[no_mangle] pub unsafe extern "C" fn tc_task_set_uda( task: *mut TCTask, - ns: *mut TCString, - key: *mut TCString, - value: *mut TCString, + ns: TCString, + key: TCString, + value: TCString, ) -> TCResult { // safety: - // - ns is not null (promised by caller) - // - ns is return from a tc_string_.. so is valid + // - ns is valid (promised by caller) // - caller will not use ns after this call (convention) - let ns = unsafe { TCString::take_from_ptr_arg(ns) }; + let mut ns = unsafe { TCString::val_from_arg(ns) }; // SAFETY: same - let key = unsafe { TCString::take_from_ptr_arg(key) }; + let mut key = unsafe { TCString::val_from_arg(key) }; // SAFETY: same - let value = unsafe { TCString::take_from_ptr_arg(value) }; + let mut value = unsafe { TCString::val_from_arg(value) }; wrap_mut( task, |task| { @@ -714,16 +723,15 @@ pub unsafe extern "C" fn tc_task_set_uda( #[no_mangle] pub unsafe extern "C" fn tc_task_remove_uda( task: *mut TCTask, - ns: *mut TCString, - key: *mut TCString, + ns: TCString, + key: TCString, ) -> TCResult { // safety: - // - ns is not null (promised by caller) - // - ns is return from a tc_string_.. so is valid + // - ns is valid (promised by caller) // - caller will not use ns after this call (convention) - let ns = unsafe { TCString::take_from_ptr_arg(ns) }; + let mut ns = unsafe { TCString::val_from_arg(ns) }; // SAFETY: same - let key = unsafe { TCString::take_from_ptr_arg(key) }; + let mut key = unsafe { TCString::val_from_arg(key) }; wrap_mut( task, |task| { @@ -738,16 +746,15 @@ pub unsafe extern "C" fn tc_task_remove_uda( #[no_mangle] pub unsafe extern "C" fn tc_task_set_legacy_uda( task: *mut TCTask, - key: *mut TCString, - value: *mut TCString, + key: TCString, + value: TCString, ) -> TCResult { // safety: - // - key is not null (promised by caller) - // - key is return from a tc_string_.. so is valid + // - key is valid (promised by caller) // - caller will not use key after this call (convention) - let key = unsafe { TCString::take_from_ptr_arg(key) }; + let mut key = unsafe { TCString::val_from_arg(key) }; // SAFETY: same - let value = unsafe { TCString::take_from_ptr_arg(value) }; + let mut value = unsafe { TCString::val_from_arg(value) }; wrap_mut( task, |task| { @@ -760,15 +767,11 @@ pub unsafe extern "C" fn tc_task_set_legacy_uda( /// Remove a UDA fraom a mutable task. #[no_mangle] -pub unsafe extern "C" fn tc_task_remove_legacy_uda( - task: *mut TCTask, - key: *mut TCString, -) -> TCResult { +pub unsafe extern "C" fn tc_task_remove_legacy_uda(task: *mut TCTask, key: TCString) -> TCResult { // safety: - // - key is not null (promised by caller) - // - key is return from a tc_string_.. so is valid + // - key is valid (promised by caller) // - caller will not use key after this call (convention) - let key = unsafe { TCString::take_from_ptr_arg(key) }; + let mut key = unsafe { TCString::val_from_arg(key) }; wrap_mut( task, |task| { @@ -779,21 +782,21 @@ pub unsafe extern "C" fn tc_task_remove_legacy_uda( ) } -/// Get the latest error for a task, or NULL if the last operation succeeded. Subsequent calls -/// to this function will return NULL. The task pointer must not be NULL. The caller must free the -/// returned string. +/// Get the latest error for a task, or a string NULL ptr field if the last operation succeeded. +/// Subsequent calls to this function will return NULL. The task pointer must not be NULL. The +/// caller must free the returned string. #[no_mangle] -pub unsafe extern "C" fn tc_task_error(task: *mut TCTask) -> *mut TCString<'static> { +pub unsafe extern "C" fn tc_task_error(task: *mut TCTask) -> TCString { // SAFETY: // - task is not null (promised by caller) // - task outlives 'a (promised by caller) let task: &mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; - if let Some(tcstring) = task.error.take() { + if let Some(rstring) = task.error.take() { // SAFETY: // - caller promises to free this value - unsafe { tcstring.return_ptr() } + unsafe { TCString::return_val(rstring) } } else { - std::ptr::null_mut() + TCString::default() } } @@ -833,7 +836,7 @@ mod test { #[test] fn empty_list_has_non_null_pointer() { - let tctasks = TCTaskList::return_val(Vec::new()); + let tctasks = unsafe { TCTaskList::return_val(Vec::new()) }; assert!(!tctasks.items.is_null()); assert_eq!(tctasks.len, 0); assert_eq!(tctasks._capacity, 0); @@ -841,7 +844,7 @@ mod test { #[test] fn free_sets_null_pointer() { - let mut tctasks = TCTaskList::return_val(Vec::new()); + let mut tctasks = unsafe { TCTaskList::return_val(Vec::new()) }; // SAFETY: testing expected behavior unsafe { tc_task_list_free(&mut tctasks) }; assert!(tctasks.items.is_null()); diff --git a/lib/src/traits.rs b/lib/src/traits.rs index e71ab0b9f..ffbdf9a79 100644 --- a/lib/src/traits.rs +++ b/lib/src/traits.rs @@ -28,8 +28,6 @@ pub(crate) trait PassByValue: Sized { /// /// - `self` must be a valid instance of the C type. This is typically ensured either by /// requiring that C code not modify it, or by defining the valid values in C comments. - /// - if RustType is not Copy, then arg must not be used by the caller after calling this - /// function unsafe fn val_from_arg(arg: Self) -> Self::RustType { // SAFETY: // - arg is a valid CType (promised by caller) @@ -54,7 +52,11 @@ pub(crate) trait PassByValue: Sized { } /// Return a value to C - fn return_val(arg: Self::RustType) -> Self { + /// + /// # Safety + /// + /// - if the value is allocated, the caller must ensure that the value is eventually freed + unsafe fn return_val(arg: Self::RustType) -> Self { Self::as_ctype(arg) } diff --git a/lib/src/uda.rs b/lib/src/uda.rs index 3712932ad..607d77e27 100644 --- a/lib/src/uda.rs +++ b/lib/src/uda.rs @@ -4,18 +4,18 @@ use crate::types::*; /// TCUda contains the details of a UDA. #[repr(C)] pub struct TCUda { - /// Namespace of the UDA. For legacy UDAs, this is NULL. - pub ns: *mut TCString<'static>, + /// Namespace of the UDA. For legacy UDAs, this may have a NULL ptr field. + pub ns: TCString, /// UDA key. Must not be NULL. - pub key: *mut TCString<'static>, + pub key: TCString, /// Content of the UDA. Must not be NULL. - pub value: *mut TCString<'static>, + pub value: TCString, } pub(crate) struct Uda { - pub ns: Option>, - pub key: TCString<'static>, - pub value: TCString<'static>, + pub ns: Option>, + pub key: RustString<'static>, + pub value: RustString<'static>, } impl PassByValue for TCUda { @@ -29,16 +29,16 @@ impl PassByValue for TCUda { // SAFETY: // - self is owned, so we can take ownership of this TCString // - self.ns is a valid, non-null TCString (NULL just checked) - Some(unsafe { TCString::take_from_ptr_arg(self.ns) }) + Some(unsafe { TCString::val_from_arg(self.ns) }) }, // SAFETY: // - self is owned, so we can take ownership of this TCString // - self.key is a valid, non-null TCString (see type docstring) - key: unsafe { TCString::take_from_ptr_arg(self.key) }, + key: unsafe { TCString::val_from_arg(self.key) }, // SAFETY: // - self is owned, so we can take ownership of this TCString // - self.value is a valid, non-null TCString (see type docstring) - value: unsafe { TCString::take_from_ptr_arg(self.value) }, + value: unsafe { TCString::val_from_arg(self.value) }, } } @@ -46,14 +46,14 @@ impl PassByValue for TCUda { TCUda { // SAFETY: caller assumes ownership of this value ns: if let Some(ns) = uda.ns { - unsafe { ns.return_ptr() } + unsafe { TCString::return_val(ns) } } else { - std::ptr::null_mut() + TCString::default() }, // SAFETY: caller assumes ownership of this value - key: unsafe { uda.key.return_ptr() }, + key: unsafe { TCString::return_val(uda.key) }, // SAFETY: caller assumes ownership of this value - value: unsafe { uda.value.return_ptr() }, + value: unsafe { TCString::return_val(uda.value) }, } } } @@ -61,9 +61,9 @@ impl PassByValue for TCUda { impl Default for TCUda { fn default() -> Self { TCUda { - ns: std::ptr::null_mut(), - key: std::ptr::null_mut(), - value: std::ptr::null_mut(), + ns: TCString::default(), + key: TCString::default(), + value: TCString::default(), } } } @@ -130,7 +130,7 @@ mod test { #[test] fn empty_list_has_non_null_pointer() { - let tcudas = TCUdaList::return_val(Vec::new()); + let tcudas = unsafe { TCUdaList::return_val(Vec::new()) }; assert!(!tcudas.items.is_null()); assert_eq!(tcudas.len, 0); assert_eq!(tcudas._capacity, 0); @@ -138,7 +138,7 @@ mod test { #[test] fn free_sets_null_pointer() { - let mut tcudas = TCUdaList::return_val(Vec::new()); + let mut tcudas = unsafe { TCUdaList::return_val(Vec::new()) }; // SAFETY: testing expected behavior unsafe { tc_uda_list_free(&mut tcudas) }; assert!(tcudas.items.is_null()); diff --git a/lib/src/util.rs b/lib/src/util.rs index 405a8b292..bfd739282 100644 --- a/lib/src/util.rs +++ b/lib/src/util.rs @@ -1,7 +1,7 @@ -use crate::string::TCString; +use crate::string::RustString; -pub(crate) fn err_to_tcstring(e: impl std::string::ToString) -> TCString<'static> { - TCString::from(e.to_string()) +pub(crate) fn err_to_ruststring(e: impl std::string::ToString) -> RustString<'static> { + RustString::from(e.to_string()) } /// An implementation of Vec::into_raw_parts, which is still unstable. Returns ptr, len, cap. @@ -12,3 +12,12 @@ pub(crate) fn vec_into_raw_parts(vec: Vec) -> (*mut T, usize, usize) { let mut vec = std::mem::ManuallyDrop::new(vec); (vec.as_mut_ptr(), vec.len(), vec.capacity()) } + +/// An implementation of String::into_raw_parts, which is still unstable. Returns ptr, len, cap. +pub(crate) fn string_into_raw_parts(string: String) -> (*mut u8, usize, usize) { + // emulate String::into_raw_parts(): + // - disable dropping the String with ManuallyDrop + // - extract ptr, len, and capacity using those methods + let mut string = std::mem::ManuallyDrop::new(string); + (string.as_mut_ptr(), string.len(), string.capacity()) +} diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs index 1bc300cf1..c4ec8f474 100644 --- a/lib/src/uuid.rs +++ b/lib/src/uuid.rs @@ -31,13 +31,17 @@ impl PassByValue for TCUuid { /// Create a new, randomly-generated UUID. #[no_mangle] pub unsafe extern "C" fn tc_uuid_new_v4() -> TCUuid { - TCUuid::return_val(Uuid::new_v4()) + // SAFETY: + // - value is not allocated + unsafe { TCUuid::return_val(Uuid::new_v4()) } } /// Create a new UUID with the nil value. #[no_mangle] pub unsafe extern "C" fn tc_uuid_nil() -> TCUuid { - TCUuid::return_val(Uuid::nil()) + // SAFETY: + // - value is not allocated + unsafe { TCUuid::return_val(Uuid::nil()) } } /// TCUuidList represents a list of uuids. @@ -95,27 +99,26 @@ pub unsafe extern "C" fn tc_uuid_to_buf(tcuuid: TCUuid, buf: *mut libc::c_char) /// Return the hyphenated string representation of a TCUuid. The returned string /// must be freed with tc_string_free. #[no_mangle] -pub unsafe extern "C" fn tc_uuid_to_str(tcuuid: TCUuid) -> *mut TCString<'static> { +pub unsafe extern "C" fn tc_uuid_to_str(tcuuid: TCUuid) -> TCString { // SAFETY: // - tcuuid is a valid TCUuid (all byte patterns are valid) let uuid: Uuid = unsafe { TCUuid::val_from_arg(tcuuid) }; let s = uuid.to_string(); // SAFETY: // - caller promises to free this value. - unsafe { TCString::from(s).return_ptr() } + unsafe { TCString::return_val(s.into()) } } /// Parse the given string as a UUID. Returns TC_RESULT_ERROR on parse failure or if the given /// string is not valid. #[no_mangle] -pub unsafe extern "C" fn tc_uuid_from_str(s: *mut TCString, uuid_out: *mut TCUuid) -> TCResult { +pub unsafe extern "C" fn tc_uuid_from_str(s: TCString, uuid_out: *mut TCUuid) -> TCResult { debug_assert!(!s.is_null()); debug_assert!(!uuid_out.is_null()); // SAFETY: - // - s is not NULL (promised by caller) - // - s is return from a tc_string_.. so is valid + // - s is valid (promised by caller) // - caller will not use s after this call (convention) - let s = unsafe { TCString::take_from_ptr_arg(s) }; + let mut s = unsafe { TCString::val_from_arg(s) }; if let Ok(s) = s.as_str() { if let Ok(u) = Uuid::parse_str(s) { // SAFETY: @@ -147,7 +150,7 @@ mod test { #[test] fn empty_list_has_non_null_pointer() { - let tcuuids = TCUuidList::return_val(Vec::new()); + let tcuuids = unsafe { TCUuidList::return_val(Vec::new()) }; assert!(!tcuuids.items.is_null()); assert_eq!(tcuuids.len, 0); assert_eq!(tcuuids._capacity, 0); @@ -155,7 +158,7 @@ mod test { #[test] fn free_sets_null_pointer() { - let mut tcuuids = TCUuidList::return_val(Vec::new()); + let mut tcuuids = unsafe { TCUuidList::return_val(Vec::new()) }; // SAFETY: testing expected behavior unsafe { tc_uuid_list_free(&mut tcuuids) }; assert!(tcuuids.items.is_null()); diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 702e24446..f09bddfe1 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -151,40 +151,6 @@ typedef struct TCReplica TCReplica; */ typedef struct TCServer TCServer; -/** - * TCString supports passing strings into and out of the TaskChampion API. - * - * # Rust Strings and C Strings - * - * A Rust string can contain embedded NUL characters, while C considers such a character to mark - * the end of a string. Strings containing embedded NULs cannot be represented as a "C string" - * and must be accessed using `tc_string_content_and_len` and `tc_string_clone_with_len`. In - * general, these two functions should be used for handling arbitrary data, while more convenient - * forms may be used where embedded NUL characters are impossible, such as in static strings. - * - * # UTF-8 - * - * TaskChampion expects all strings to be valid UTF-8. `tc_string_…` functions will fail if given - * a `*TCString` containing invalid UTF-8. - * - * # Safety - * - * When a `*TCString` appears as a return value or output argument, ownership is passed to the - * caller. The caller must pass that ownership back to another function or free the string. - * - * Any function taking a `*TCString` requires: - * - the pointer must not be NUL; - * - the pointer must be one previously returned from a tc_… function; and - * - the memory referenced by the pointer must never be modified by C code. - * - * Unless specified otherwise, TaskChampion functions take ownership of a `*TCString` when it is - * given as a function argument, and the pointer is invalid when the function returns. Callers - * must not use or free TCStrings after passing them to such API functions. - * - * TCString is not threadsafe. - */ -typedef struct TCString TCString; - /** * A task, as publicly exposed by this library. * @@ -243,6 +209,52 @@ typedef struct TCTask TCTask; */ typedef struct TCWorkingSet TCWorkingSet; +/** + * TCString supports passing strings into and out of the TaskChampion API. + * + * # Rust Strings and C Strings + * + * A Rust string can contain embedded NUL characters, while C considers such a character to mark + * the end of a string. Strings containing embedded NULs cannot be represented as a "C string" + * and must be accessed using `tc_string_content_and_len` and `tc_string_clone_with_len`. In + * general, these two functions should be used for handling arbitrary data, while more convenient + * forms may be used where embedded NUL characters are impossible, such as in static strings. + * + * # UTF-8 + * + * TaskChampion expects all strings to be valid UTF-8. `tc_string_…` functions will fail if given + * a `*TCString` containing invalid UTF-8. + * + * # Safety + * + * The `ptr` field may be checked for NULL, where documentation indicates this is possible. All + * other fields in a TCString are private and must not be used from C. They exist in the struct + * to ensure proper allocation and alignment. + * + * When a `TCString` appears as a return value or output argument, ownership is passed to the + * caller. The caller must pass that ownership back to another function or free the string. + * + * Any function taking a `TCString` requires: + * - the pointer must not be NUL; + * - the pointer must be one previously returned from a tc_… function; and + * - the memory referenced by the pointer must never be modified by C code. + * + * Unless specified otherwise, TaskChampion functions take ownership of a `TCString` when it is + * given as a function argument, and the caller must not use or free TCStrings after passing them + * to such API functions. + * + * A TCString with a NULL `ptr` field need not be freed, although tc_free_string will not fail + * for such a value. + * + * TCString is not threadsafe. + */ +typedef struct TCString { + void *ptr; + size_t _u1; + size_t _u2; + uint8_t _u3; +} TCString; + /** * TCAnnotation contains the details of an annotation. * @@ -268,7 +280,7 @@ typedef struct TCAnnotation { /** * Content of the annotation. Must not be NULL. */ - struct TCString *description; + struct TCString description; } TCAnnotation; /** @@ -299,8 +311,8 @@ typedef struct TCAnnotationList { * will be freed when it is freed with tc_kv_list_free. */ typedef struct TCKV { - struct TCString *key; - struct TCString *value; + struct TCString key; + struct TCString value; } TCKV; /** @@ -395,7 +407,7 @@ typedef struct TCStringList { * be freed by tc_string_list_free. This pointer is never NULL for a valid TCStringList, and the * *TCStringList at indexes 0..len-1 are not NULL. */ - struct TCString *const *items; + const struct TCString *items; } TCStringList; /** @@ -403,17 +415,17 @@ typedef struct TCStringList { */ typedef struct TCUda { /** - * Namespace of the UDA. For legacy UDAs, this is NULL. + * Namespace of the UDA. For legacy UDAs, this may have a NULL ptr field. */ - struct TCString *ns; + struct TCString ns; /** * UDA key. Must not be NULL. */ - struct TCString *key; + struct TCString key; /** * Content of the UDA. Must not be NULL. */ - struct TCString *value; + struct TCString value; } TCUda; /** @@ -474,7 +486,7 @@ struct TCReplica *tc_replica_new_in_memory(void); * is written to the error_out parameter (if it is not NULL) and NULL is returned. The caller * must free this string. */ -struct TCReplica *tc_replica_new_on_disk(struct TCString *path, struct TCString **error_out); +struct TCReplica *tc_replica_new_on_disk(struct TCString path, struct TCString *error_out); /** * Get a list of all tasks in the replica. @@ -487,6 +499,8 @@ struct TCTaskList tc_replica_all_tasks(struct TCReplica *rep); * Get a list of all uuids for tasks in the replica. * * Returns a TCUuidList with a NULL items field on error. + * + * The caller must free the UUID list with `tc_uuid_list_free`. */ struct TCUuidList tc_replica_all_task_uuids(struct TCReplica *rep); @@ -513,7 +527,7 @@ struct TCTask *tc_replica_get_task(struct TCReplica *rep, struct TCUuid tcuuid); */ struct TCTask *tc_replica_new_task(struct TCReplica *rep, enum TCStatus status, - struct TCString *description); + struct TCString description); /** * Create a new task. The task must not already exist. @@ -553,11 +567,11 @@ TCResult tc_replica_add_undo_point(struct TCReplica *rep, bool force); TCResult tc_replica_rebuild_working_set(struct TCReplica *rep, bool renumber); /** - * Get the latest error for a replica, or NULL if the last operation succeeded. Subsequent calls - * to this function will return NULL. The rep pointer must not be NULL. The caller must free the - * returned string. + * Get the latest error for a replica, or a string with NULL ptr if no error exists. Subsequent + * calls to this function will return NULL. The rep pointer must not be NULL. The caller must + * free the returned string. */ -struct TCString *tc_replica_error(struct TCReplica *rep); +struct TCString tc_replica_error(struct TCReplica *rep); /** * Free a replica. The replica may not be used after this function returns and must not be freed @@ -574,7 +588,7 @@ void tc_replica_free(struct TCReplica *rep); * * The server must be freed after it is used - tc_replica_sync does not automatically free it. */ -struct TCServer *tc_server_new_local(struct TCString *server_dir, struct TCString **error_out); +struct TCServer *tc_server_new_local(struct TCString server_dir, struct TCString *error_out); /** * Create a new TCServer that connects to a remote server. See the TaskChampion docs for the @@ -585,10 +599,10 @@ struct TCServer *tc_server_new_local(struct TCString *server_dir, struct TCStrin * * The server must be freed after it is used - tc_replica_sync does not automatically free it. */ -struct TCServer *tc_server_new_remote(struct TCString *origin, +struct TCServer *tc_server_new_remote(struct TCString origin, struct TCUuid client_key, - struct TCString *encryption_secret, - struct TCString **error_out); + struct TCString encryption_secret, + struct TCString *error_out); /** * Free a server. The server may not be used after this function returns and must not be freed @@ -612,34 +626,39 @@ void tc_server_free(struct TCServer *server); * free(url); // string is no longer referenced and can be freed * ``` */ -struct TCString *tc_string_borrow(const char *cstr); +struct TCString tc_string_borrow(const char *cstr); /** * Create a new TCString by cloning the content of the given C string. The resulting TCString * is independent of the given string, which can be freed or overwritten immediately. */ -struct TCString *tc_string_clone(const char *cstr); +struct TCString tc_string_clone(const char *cstr); /** * Create a new TCString containing the given string with the given length. This allows creation * of strings containing embedded NUL characters. As with `tc_string_clone`, the resulting * TCString is independent of the passed buffer, which may be reused or freed immediately. * + * The length should _not_ include any trailing NUL. + * * The given length must be less than half the maximum value of usize. */ -struct TCString *tc_string_clone_with_len(const char *buf, size_t len); +struct TCString tc_string_clone_with_len(const char *buf, size_t len); /** - * Get the content of the string as a regular C string. The given string must not be NULL. The + * Get the content of the string as a regular C string. The given string must be valid. The * returned value is NULL if the string contains NUL bytes or (in some cases) invalid UTF-8. The * returned C string is valid until the TCString is freed or passed to another TC API function. * * In general, prefer [`tc_string_content_with_len`] except when it's certain that the string is * valid and NUL-free. * + * This function takes the TCString by pointer because it may be modified in-place to add a NUL + * terminator. The pointer must not be NULL. + * * This function does _not_ take ownership of the TCString. */ -const char *tc_string_content(struct TCString *tcstring); +const char *tc_string_content(const struct TCString *tcstring); /** * Get the content of the string as a pointer and length. The given string must not be NULL. @@ -647,9 +666,12 @@ const char *tc_string_content(struct TCString *tcstring); * returned buffer is valid until the TCString is freed or passed to another TaskChampio * function. * + * This function takes the TCString by pointer because it may be modified in-place to add a NUL + * terminator. The pointer must not be NULL. + * * This function does _not_ take ownership of the TCString. */ -const char *tc_string_content_with_len(struct TCString *tcstring, size_t *len_out); +const char *tc_string_content_with_len(const struct TCString *tcstring, size_t *len_out); /** * Free a TCString. The given string must not be NULL. The string must not be used @@ -707,7 +729,7 @@ enum TCStatus tc_task_get_status(struct TCTask *task); /** * Get the underlying key/value pairs for this task. The returned TCKVList is * a "snapshot" of the task and will not be updated if the task is subsequently - * modified. + * modified. It is the caller's responsibility to free the TCKVList. */ struct TCKVList tc_task_get_taskmap(struct TCTask *task); @@ -715,7 +737,7 @@ struct TCKVList tc_task_get_taskmap(struct TCTask *task); * Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it * contains embedded NUL characters). */ -struct TCString *tc_task_get_description(struct TCTask *task); +struct TCString tc_task_get_description(struct TCTask *task); /** * Get the entry timestamp for a task (when it was created), or 0 if not set. @@ -746,7 +768,7 @@ bool tc_task_is_active(struct TCTask *task); * Check if a task has the given tag. If the tag is invalid, this function will return false, as * that (invalid) tag is not present. No error will be reported via `tc_task_error`. */ -bool tc_task_has_tag(struct TCTask *task, struct TCString *tag); +bool tc_task_has_tag(struct TCTask *task, struct TCString tag); /** * Get the tags for the task. @@ -767,16 +789,16 @@ struct TCAnnotationList tc_task_get_annotations(struct TCTask *task); /** * Get the named UDA from the task. * - * Returns NULL if the UDA does not exist. + * Returns a TCString with NULL ptr field if the UDA does not exist. */ -struct TCString *tc_task_get_uda(struct TCTask *task, struct TCString *ns, struct TCString *key); +struct TCString tc_task_get_uda(struct TCTask *task, struct TCString ns, struct TCString key); /** * Get the named legacy UDA from the task. * * Returns NULL if the UDA does not exist. */ -struct TCString *tc_task_get_legacy_uda(struct TCTask *task, struct TCString *key); +struct TCString tc_task_get_legacy_uda(struct TCTask *task, struct TCString key); /** * Get all UDAs for this task. @@ -789,7 +811,7 @@ struct TCUdaList tc_task_get_udas(struct TCTask *task); * Get all UDAs for this task. * * All TCUdas in this list have a NULL ns field. The entire UDA key is - * included in the key field. + * included in the key field. The caller must free the returned list. */ struct TCUdaList tc_task_get_legacy_udas(struct TCTask *task); @@ -801,7 +823,7 @@ TCResult tc_task_set_status(struct TCTask *task, enum TCStatus status); /** * Set a mutable task's description. */ -TCResult tc_task_set_description(struct TCTask *task, struct TCString *description); +TCResult tc_task_set_description(struct TCTask *task, struct TCString description); /** * Set a mutable task's entry (creation time). Pass entry=0 to unset @@ -842,12 +864,12 @@ TCResult tc_task_delete(struct TCTask *task); /** * Add a tag to a mutable task. */ -TCResult tc_task_add_tag(struct TCTask *task, struct TCString *tag); +TCResult tc_task_add_tag(struct TCTask *task, struct TCString tag); /** * Remove a tag from a mutable task. */ -TCResult tc_task_remove_tag(struct TCTask *task, struct TCString *tag); +TCResult tc_task_remove_tag(struct TCTask *task, struct TCString tag); /** * Add an annotation to a mutable task. This call takes ownership of the @@ -864,31 +886,31 @@ TCResult tc_task_remove_annotation(struct TCTask *task, int64_t entry); * Set a UDA on a mutable task. */ TCResult tc_task_set_uda(struct TCTask *task, - struct TCString *ns, - struct TCString *key, - struct TCString *value); + struct TCString ns, + struct TCString key, + struct TCString value); /** * Remove a UDA fraom a mutable task. */ -TCResult tc_task_remove_uda(struct TCTask *task, struct TCString *ns, struct TCString *key); +TCResult tc_task_remove_uda(struct TCTask *task, struct TCString ns, struct TCString key); /** * Set a legacy UDA on a mutable task. */ -TCResult tc_task_set_legacy_uda(struct TCTask *task, struct TCString *key, struct TCString *value); +TCResult tc_task_set_legacy_uda(struct TCTask *task, struct TCString key, struct TCString value); /** * Remove a UDA fraom a mutable task. */ -TCResult tc_task_remove_legacy_uda(struct TCTask *task, struct TCString *key); +TCResult tc_task_remove_legacy_uda(struct TCTask *task, struct TCString key); /** - * Get the latest error for a task, or NULL if the last operation succeeded. Subsequent calls - * to this function will return NULL. The task pointer must not be NULL. The caller must free the - * returned string. + * Get the latest error for a task, or a string NULL ptr field if the last operation succeeded. + * Subsequent calls to this function will return NULL. The task pointer must not be NULL. The + * caller must free the returned string. */ -struct TCString *tc_task_error(struct TCTask *task); +struct TCString tc_task_error(struct TCTask *task); /** * Free a task. The given task must not be NULL. The task must not be used after this function @@ -940,13 +962,13 @@ void tc_uuid_to_buf(struct TCUuid tcuuid, char *buf); * Return the hyphenated string representation of a TCUuid. The returned string * must be freed with tc_string_free. */ -struct TCString *tc_uuid_to_str(struct TCUuid tcuuid); +struct TCString tc_uuid_to_str(struct TCUuid tcuuid); /** * Parse the given string as a UUID. Returns TC_RESULT_ERROR on parse failure or if the given * string is not valid. */ -TCResult tc_uuid_from_str(struct TCString *s, struct TCUuid *uuid_out); +TCResult tc_uuid_from_str(struct TCString s, struct TCUuid *uuid_out); /** * Free a TCUuidList instance. The instance, and all TCUuids it contains, must not be used after From f0178d4fabdc6c4bfd5dd4ec809a41e9edf8daf2 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 24 Feb 2022 03:33:08 +0000 Subject: [PATCH 86/95] don't use unsafe_op_in_unsafe_fn, as it's not in MSRV --- lib/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 946342fb3..014a70c5a 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,4 +1,7 @@ -#![warn(unsafe_op_in_unsafe_fn)] +// Not compatible with the MSRV +// #![warn(unsafe_op_in_unsafe_fn)] +#![allow(unused_unsafe)] + // Not working yet in stable - https://github.com/rust-lang/rust-clippy/issues/8020 // #![warn(clippy::undocumented_unsafe_blocks)] From 17ccaea0966363270f5e145cec61ffa25fc5b42c Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sat, 26 Feb 2022 22:45:41 +0000 Subject: [PATCH 87/95] try the latest 'cc' crate in hopes it fixes things --- Cargo.lock | 4 ++-- integration-tests/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fed2a869a..0f5d620c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -612,9 +612,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.68" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" dependencies = [ "jobserver", ] diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 2b4a3bd8a..1d57b1a7d 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -21,5 +21,5 @@ env_logger = "^0.8.3" lazy_static = "1" [build-dependencies] -cc = "1.0" +cc = "1.0.73" taskchampion-lib = { path = "../lib" } From 2c9d74515ebf19ef833a2f08d775be8e0e896376 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 27 Feb 2022 17:03:04 +0000 Subject: [PATCH 88/95] link to libtaskchampion separately from the unity tests --- integration-tests/build.rs | 43 ++++++++++++++++++++++---------------- lib/Cargo.toml | 2 +- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/integration-tests/build.rs b/integration-tests/build.rs index b5217f5a0..0537507db 100644 --- a/integration-tests/build.rs +++ b/integration-tests/build.rs @@ -2,25 +2,29 @@ use std::env; use std::fs; use std::path::Path; -fn build_libtaskchampion(suites: &[&'static str]) { - // This crate has taskchampion-lib in its build-dependencies, so - // libtaskchampion.so should be built already. Hopefully it's in target/$PROFILE, and hopefully - // it's named libtaskchampion.so and not something else - let mut libtaskchampion = env::current_dir().unwrap(); - libtaskchampion.pop(); - libtaskchampion.push("target"); - libtaskchampion.push(env::var("PROFILE").unwrap()); - libtaskchampion.push("deps"); - libtaskchampion.push(if cfg!(target_os = "macos") { - "libtaskchampion.dylib" - } else { - "libtaskchampion.so" - }); +/// Link to the libtaskchampion library produced by the `taskchampion-lib` crate. This is done as +/// a build dependency, rather than a cargo dependency, for unclear reasons. TODO +fn link_libtaskchampion() { + // This crate has taskchampion-lib in its build-dependencies, so libtaskchampion.so should be + // built already. + // + // Shared libraries (crate-type=cdylib) appear to be placed in target/$PROFILE/deps. + let mut libtc_dir = env::current_dir().unwrap(); + libtc_dir.pop(); + libtc_dir.push("target"); + libtc_dir.push(env::var("PROFILE").unwrap()); + libtc_dir.push("deps"); + let libtc_dir = libtc_dir.to_str().expect("path is valid utf-8"); + println!("cargo:rustc-link-search={}", libtc_dir); + println!("cargo:rustc-link-lib=dylib=taskchampion"); +} + +/// Build the Unity-based C test suite in `src/bindings_tests`, linking the result with this +/// package's library crate. +fn build_bindings_tests(suites: &[&'static str]) { let mut build = cc::Build::new(); - build.shared_flag(true); - build.object(libtaskchampion); - build.include("../lib"); + build.include("../lib"); // include path for taskchampion.h build.include("src/bindings_tests/unity"); build.define("UNITY_OUTPUT_CHAR", "test_output"); build.define( @@ -41,6 +45,8 @@ fn build_libtaskchampion(suites: &[&'static str]) { build.compile("bindings-tests"); } +/// Make `bindings_test_suites.rs` listing all of the test suites, for use in building the +/// bindings-test binary. fn make_suite_file(suites: &[&'static str]) { let out_dir = env::var_os("OUT_DIR").unwrap(); let dest_path = Path::new(&out_dir).join("bindings_test_suites.rs"); @@ -55,6 +61,7 @@ fn main() { println!("cargo:rerun-if-changed=build.rs"); let suites = &["uuid", "string", "task", "replica"]; - build_libtaskchampion(suites); + link_libtaskchampion(); + build_bindings_tests(suites); make_suite_file(suites); } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 564f28590..6f85b62b7 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -5,7 +5,7 @@ edition = "2018" [lib] name = "taskchampion" -crate-type = ["cdylib"] +crate-type = ["staticlib", "cdylib"] [dependencies] libc = "0.2.113" From 8a96ca72736de8ea87b354565638355c6c623e67 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 27 Feb 2022 17:07:09 +0000 Subject: [PATCH 89/95] fix formatting --- integration-tests/build.rs | 3 ++- lib/src/lib.rs | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/build.rs b/integration-tests/build.rs index 0537507db..8369b57a7 100644 --- a/integration-tests/build.rs +++ b/integration-tests/build.rs @@ -3,7 +3,8 @@ use std::fs; use std::path::Path; /// Link to the libtaskchampion library produced by the `taskchampion-lib` crate. This is done as -/// a build dependency, rather than a cargo dependency, for unclear reasons. TODO +/// a build dependency, rather than a cargo dependency, so that the symbols are available to +/// bindings-tests. fn link_libtaskchampion() { // This crate has taskchampion-lib in its build-dependencies, so libtaskchampion.so should be // built already. diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 014a70c5a..4f83aaf0d 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,7 +1,6 @@ // Not compatible with the MSRV // #![warn(unsafe_op_in_unsafe_fn)] #![allow(unused_unsafe)] - // Not working yet in stable - https://github.com/rust-lang/rust-clippy/issues/8020 // #![warn(clippy::undocumented_unsafe_blocks)] From 5072ed74582fa25c63c1cf4b1c77a939a77fe8da Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 27 Feb 2022 17:13:55 +0000 Subject: [PATCH 90/95] a bit of docs --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 6bf44be76..f1c66f275 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,18 @@ There are five crates here: The _taskchampion_lib_ crate uses a bit of code generation to create the `lib/taskchampion.h` header file. To regenerate this file, run `cargo xtask codegen`. +## C libraries + +The `taskchampion-lib` crate generates libraries suitable for use from C (or any C-compatible language). + +The necessary bits are: + +* a shared object in `target/$PROFILE/deps` (e.g., `target/debug/deps/libtaskchampion.so`) +* a static library in `target/$PROFILE` (e.g., `target/debug/libtaskchampion.a`) +* a header file, `lib/taskchampion.h`. + +Downstream consumers may use either the static or dynamic library, as they prefer. + ## Documentation Generation The `mdbook` configuration contains a "preprocessor" implemented in the `taskchampion-cli` crate in order to reflect CLI usage information into the generated book. From aadd4a762febb69625eb504dc1c8499dd1339f1e Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 27 Feb 2022 17:28:36 +0000 Subject: [PATCH 91/95] support generation of paths from TCString on Windows --- lib/src/replica.rs | 76 +++++++++++++++++++++++++++------------------- lib/src/server.rs | 4 +-- lib/src/string.rs | 22 ++++++++++---- 3 files changed, 62 insertions(+), 40 deletions(-) diff --git a/lib/src/replica.rs b/lib/src/replica.rs index a1d488ede..11fd26186 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -94,6 +94,34 @@ where } } +/// Utility function to allow using `?` notation to return an error value in the constructor. +fn wrap_constructor(f: F, error_out: *mut TCString, err_value: T) -> T +where + F: FnOnce() -> anyhow::Result, +{ + if !error_out.is_null() { + // SAFETY: + // - error_out is not NULL (just checked) + // - properly aligned and valid (promised by caller) + unsafe { *error_out = TCString::default() }; + } + + match f() { + Ok(v) => v, + Err(e) => { + if !error_out.is_null() { + // SAFETY: + // - error_out is not NULL (just checked) + // - properly aligned and valid (promised by caller) + unsafe { + TCString::val_to_arg_out(err_to_ruststring(e), error_out); + } + } + err_value + } + } +} + /// Create a new TCReplica with an in-memory database. The contents of the database will be /// lost when it is freed with tc_replica_free. #[no_mangle] @@ -114,40 +142,24 @@ pub unsafe extern "C" fn tc_replica_new_on_disk( path: TCString, error_out: *mut TCString, ) -> *mut TCReplica { - if !error_out.is_null() { - // SAFETY: - // - error_out is not NULL (just checked) - // - properly aligned and valid (promised by caller) - unsafe { *error_out = TCString::default() }; - } - - // SAFETY: - // - path is valid (promised by caller) - // - caller will not use path after this call (convention) - let path = unsafe { TCString::val_from_arg(path) }; - let storage_res = StorageConfig::OnDisk { - taskdb_dir: path.to_path_buf(), - } - .into_storage(); - - let storage = match storage_res { - Ok(storage) => storage, - Err(e) => { - if !error_out.is_null() { - unsafe { - // SAFETY: - // - error_out is not NULL (just checked) - // - properly aligned and valid (promised by caller) - TCString::val_to_arg_out(err_to_ruststring(e), error_out); - } + wrap_constructor( + || { + // SAFETY: + // - path is valid (promised by caller) + // - caller will not use path after this call (convention) + let mut path = unsafe { TCString::val_from_arg(path) }; + let storage = StorageConfig::OnDisk { + taskdb_dir: path.to_path_buf()?, } - return std::ptr::null_mut(); - } - }; + .into_storage()?; - // SAFETY: - // - caller promises to free this value - unsafe { TCReplica::from(Replica::new(storage)).return_ptr() } + // SAFETY: + // - caller promises to free this value + Ok(unsafe { TCReplica::from(Replica::new(storage)).return_ptr() }) + }, + error_out, + std::ptr::null_mut(), + ) } /// Get a list of all tasks in the replica. diff --git a/lib/src/server.rs b/lib/src/server.rs index 711fcacf5..b7247bdb6 100644 --- a/lib/src/server.rs +++ b/lib/src/server.rs @@ -70,9 +70,9 @@ pub unsafe extern "C" fn tc_server_new_local( // SAFETY: // - server_dir is valid (promised by caller) // - caller will not use server_dir after this call (convention) - let server_dir = unsafe { TCString::val_from_arg(server_dir) }; + let mut server_dir = unsafe { TCString::val_from_arg(server_dir) }; let server_config = ServerConfig::Local { - server_dir: server_dir.to_path_buf(), + server_dir: server_dir.to_path_buf()?, }; let server = server_config.into_server()?; // SAFETY: caller promises to free this server. diff --git a/lib/src/string.rs b/lib/src/string.rs index 3767f7bb2..2791aadcc 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -1,8 +1,7 @@ use crate::traits::*; use crate::util::{string_into_raw_parts, vec_into_raw_parts}; -use std::ffi::{CStr, CString, OsStr}; +use std::ffi::{CStr, CString, OsStr, OsString}; use std::os::raw::c_char; -use std::os::unix::ffi::OsStrExt; use std::path::PathBuf; /// TCString supports passing strings into and out of the TaskChampion API. @@ -293,10 +292,21 @@ impl<'a> RustString<'a> { } } - pub(crate) fn to_path_buf(&self) -> PathBuf { - // TODO: this is UNIX-specific. - let path: &OsStr = OsStr::from_bytes(self.as_bytes()); - path.to_os_string().into() + pub(crate) fn to_path_buf(&mut self) -> Result { + #[cfg(unix)] + let path: OsString = { + // on UNIX, we can use the bytes directly, without requiring that they + // be valid UTF-8. + use std::os::unix::ffi::OsStrExt; + OsStr::from_bytes(self.as_bytes()).to_os_string() + }; + #[cfg(windows)] + let path: OsString = { + // on Windows, we assume the filename is valid Unicode, so it can be + // represented as UTF-8. + OsString::from(self.as_str()?.to_string()) + }; + Ok(path.into()) } } From 1c5b01975c55da2df58136bee2c3abfde0085127 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 27 Feb 2022 17:57:34 +0000 Subject: [PATCH 92/95] fix unused symbol on windows --- lib/src/string.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/string.rs b/lib/src/string.rs index 2791aadcc..c6f1c3ce4 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -1,6 +1,6 @@ use crate::traits::*; use crate::util::{string_into_raw_parts, vec_into_raw_parts}; -use std::ffi::{CStr, CString, OsStr, OsString}; +use std::ffi::{CStr, CString, OsString}; use std::os::raw::c_char; use std::path::PathBuf; @@ -298,6 +298,7 @@ impl<'a> RustString<'a> { // on UNIX, we can use the bytes directly, without requiring that they // be valid UTF-8. use std::os::unix::ffi::OsStrExt; + use std::ffi::OsStr; OsStr::from_bytes(self.as_bytes()).to_os_string() }; #[cfg(windows)] From 85153423beb1523717751c9301ee88e7a40b1ff7 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 27 Feb 2022 17:59:58 +0000 Subject: [PATCH 93/95] include BCrypt on Windows --- README.md | 2 ++ integration-tests/build.rs | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/README.md b/README.md index f1c66f275..3622cc50e 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ The necessary bits are: Downstream consumers may use either the static or dynamic library, as they prefer. +NOTE: on Windows, the "BCrypt" library must be included when linking to taskchampion. + ## Documentation Generation The `mdbook` configuration contains a "preprocessor" implemented in the `taskchampion-cli` crate in order to reflect CLI usage information into the generated book. diff --git a/integration-tests/build.rs b/integration-tests/build.rs index 8369b57a7..90165f6d0 100644 --- a/integration-tests/build.rs +++ b/integration-tests/build.rs @@ -19,6 +19,11 @@ fn link_libtaskchampion() { let libtc_dir = libtc_dir.to_str().expect("path is valid utf-8"); println!("cargo:rustc-link-search={}", libtc_dir); println!("cargo:rustc-link-lib=dylib=taskchampion"); + + // on windows, it appears that rust std requires BCrypt + if cfg!(target_os = "windows") { + println!("cargo:rustc-link-lib=dylib=bcrypt"); + } } /// Build the Unity-based C test suite in `src/bindings_tests`, linking the result with this From a5259350087e0997894451f868ad3fe29f44e21d Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 27 Feb 2022 18:14:21 +0000 Subject: [PATCH 94/95] import order fix --- lib/src/string.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/string.rs b/lib/src/string.rs index c6f1c3ce4..86b1ac3bd 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -297,8 +297,8 @@ impl<'a> RustString<'a> { let path: OsString = { // on UNIX, we can use the bytes directly, without requiring that they // be valid UTF-8. - use std::os::unix::ffi::OsStrExt; use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; OsStr::from_bytes(self.as_bytes()).to_os_string() }; #[cfg(windows)] From 3a4c417ceea83d3026967b77dab042694cefc05e Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 2 Mar 2022 20:58:36 -0500 Subject: [PATCH 95/95] free replica in test --- integration-tests/src/bindings_tests/replica.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration-tests/src/bindings_tests/replica.c b/integration-tests/src/bindings_tests/replica.c index cd02d3a30..88651fc45 100644 --- a/integration-tests/src/bindings_tests/replica.c +++ b/integration-tests/src/bindings_tests/replica.c @@ -305,6 +305,8 @@ static void test_replica_get_task_not_found(void) { TCTask *task = tc_replica_get_task(rep, uuid); TEST_ASSERT_NULL(task); TEST_ASSERT_NULL(tc_replica_error(rep).ptr); + + tc_replica_free(rep); } int replica_tests(void) {