diff --git a/.githooks/pre-commit b/.githooks/pre-commit index e7e7dc29..1d273a52 100644 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -6,7 +6,9 @@ cd $SCRIPTPATH/../ set -e CMD_TO_RUN="make && make test && make doc && cp build/linux/zeroerr.hpp ./zeroerr.hpp" -if [ -f "/etc/wsl.conf" ]; then +if [ "$(uname -o)" == "GNU/Linux" ]; then + bash -c "$CMD_TO_RUN" +elif [ -f "/etc/wsl.conf" ]; then bash -c "$CMD_TO_RUN" else wsl -- bash -c "$CMD_TO_RUN" diff --git a/.vscode/launch.json b/.vscode/launch.json index cd9c9554..bb42506e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,6 +12,14 @@ "args": [], "cwd": "${workspaceFolder}" }, + { + "type": "lldb", + "request": "launch", + "name": "Windows 2_log", + "program": "${workspaceFolder}/build/windows/examples/Debug/2_log.exe", + "args": ["--testcase=parsing test"], + "cwd": "${workspaceFolder}" + }, { "type": "lldb", "request": "launch", diff --git a/CMakeLists.txt b/CMakeLists.txt index 65b64646..7c63ab9d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,11 +64,6 @@ option(BUILD_EXAMPLES "Build examples(ON, OFF)" OFF) option(BUILD_DOC "Build documentation" OFF) option(BUILD_TEST "Build unittest" OFF) -add_compile_options("$<$:/utf-8>") -add_compile_options("$<$:/utf-8>") -add_compile_options("$<$:-fstandalone-debug>") -# add_compile_options("$<$:-ftime-report>") - set(header_dirs ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/extension diff --git a/Makefile b/Makefile index 6e2bbbe1..9cb793b0 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ all: linux windows build/linux/Makefile: Makefile mkdir -p build/linux cmake -B build/linux -S . -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_STANDARD=11 \ - -DBUILD_EXAMPLES=ON -DBUILD_TEST=ON -DUSE_MOLD=ON -DENABLE_FUZZING=ON \ + -DBUILD_EXAMPLES=ON -DBUILD_TEST=ON -DUSE_MOLD=ON -DDISABLE_CUDA_BUILD=OFF -DENABLE_FUZZING=ON \ -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang linux: build/linux/Makefile @@ -14,18 +14,21 @@ linux: build/linux/Makefile build/windows/ZeroErr.sln: mkdir -p build/windows cmake.exe -B build/windows -S . \ - -DBUILD_EXAMPLES=ON -DBUILD_TEST=ON -T host=x64 -A x64 + -DBUILD_EXAMPLES=ON -DBUILD_TEST=ON -DDISABLE_CUDA_BUILD=OFF -T host=x64 -A x64 windows: build/windows/ZeroErr.sln cmake.exe --build build/windows --config Debug -j `nproc` test: linux-test windows-test fuzz-test +cuda-test: windows + cd build/windows/test && ./Debug/cudatest.exe + fuzz-test: linux cd build/linux/test && ./unittest --testcase=fuzz_serialize.* fuzz: linux - cd build/linux/test && ./unittest -f --testcase=fuzz_test.* + cd build/linux/test && ./unittest -f --testcase=presentation linux-test: linux cd build/linux/test && ./unittest @@ -34,7 +37,7 @@ reporter: linux cd build/linux/test && ./unittest --no-color --log-to-report --reporters=xml windows-test: windows - cd build/windows/test && ./Debug/unittest.exe + cd build/windows/test && ../examples/Debug/2_log.exe --testcase="parsing test" build/linux-release/Makefile: Makefile diff --git a/Readme.md b/Readme.md index e1118b4a..e42767bf 100644 --- a/Readme.md +++ b/Readme.md @@ -192,13 +192,26 @@ clang++ -std=c++11 -fsanitize=fuzzer-no-link -L=`clang++ -print-runtime-dir` -lc ``` -## Other Good Features +## CppCon 24 Talk + +If you are interested in the design and internal techiques used in `zeroerr`, please checkout [the talk in CppCon 24](https://youtu.be/otSPZyXqY_M?si=LLSHh8gkfCiQk983). + +[![](docs/fig/video-screenshot.png)](https://youtu.be/otSPZyXqY_M?si=LLSHh8gkfCiQk983) + +## Header-only libraries + +* dbg +* print (without use extern functions) +* assert +* color (if always enabled) + +## Other Good Features Here are a list of features we provided: 1. Partially include -You can only include what you need. If you need only assertion but no unit testing, no problem. +You can only include what you need. If you need only assertion but no unit testing, you can only include the header `zeroerr/assert.h`. 2. Optional thread safety You can choose to build with/without thread safety. @@ -235,12 +248,6 @@ We can support output structured information directly into plain text or lisp fo 13. Automatic Tracing with logging While logging at the end, we can record the time consuming for this function. -## Header-only libraries - -* dbg -* print (without use extern functions) -* assert -* color (if always enabled) ## The logo generation diff --git a/docs/fig/video-screenshot.png b/docs/fig/video-screenshot.png new file mode 100644 index 00000000..7e433b51 Binary files /dev/null and b/docs/fig/video-screenshot.png differ diff --git a/examples/1_basic.cpp b/examples/1_basic.cpp index c519ca1b..5f8c5506 100644 --- a/examples/1_basic.cpp +++ b/examples/1_basic.cpp @@ -47,5 +47,5 @@ TEST_CASE("fib function test") { CHECK(fib(3) == 2); CHECK(fib(4) == 3); CHECK(fib(5) == 5); - CHECK(fib(20) == 6765); + CHECK_THROWS(fib(20)); } diff --git a/examples/2_log.cpp b/examples/2_log.cpp new file mode 100644 index 00000000..cfe40708 --- /dev/null +++ b/examples/2_log.cpp @@ -0,0 +1,79 @@ +#define ZEROERR_IMPLEMENTATION +#include "zeroerr.hpp" + +using namespace zeroerr; + +TEST_CASE("1. basic log test") { + LOG("Basic log"); + WARN("Warning log"); + ERR("Error log"); + FATAL("Fatal log"); + + LOG("log with basic thype {} {} {} {}", 1, true, 1.0, "string"); + + std::vector> data = { + {1, 1.0, "string"}, {2, 2.0, "string"} + }; + LOG("log with complex type: {data}", data); + + LOG_IF(1==1, "log if condition is true"); + LOG_FIRST(1==1, "log only at the first time condition is true"); + WARN_EVERY_(2, "log every 2 times"); + WARN_IF_EVERY_(2, 1==1, "log if condition is true every 2 times"); + + DLOG(WARN_IF, 1==1, "debug log for WARN_IF"); +} + + +struct Expr { + virtual Expr* Clone() { return new Expr(); }; + virtual ~Expr() {} +}; + +Expr* parse_the_input(std::string input) { + return new Expr(); +} + + +Expr* parseExpr(std::string input) +{ + static std::map cache; + if (cache.count(input) == 0) { + Expr* expr = parse_the_input(input); + cache[input] = expr; + return expr; + } else { + LOG("CacheHit: input = {input}", input); + return cache[input]->Clone(); + } +} + +TEST_CASE("parsing test") { + zeroerr::suspendLog(); + std::string log; + Expr* e1 = parseExpr("1 + 2"); + log = LOG_GET(parseExpr, "CacheHit", input, std::string); + CHECK(log == std::string{}); + Expr* e2 = parseExpr("1 + 2"); + log = zeroerr::LogStream::getDefault() + .getLog("parseExpr", "CacheHit", "input"); + CHECK(log == "1 + 2"); + zeroerr::resumeLog(); +} + + +TEST_CASE("iterate log stream") { + zeroerr::suspendLog(); + + auto& stream = zeroerr::LogStream::getDefault(); + + for (auto p = stream.begin("function log {sum}, {i}"); p != stream.end(); ++p) { + std::cerr << "p.get(\"sum\") = " << p.get("sum") << std::endl; + std::cerr << "p.get(\"i\") = " << p.get("i") << std::endl; + CHECK(p.get("sum") == 10); + CHECK(p.get("i") == 1); + } + + zeroerr::resumeLog(); +} + diff --git a/examples/3_fuzzing.cpp b/examples/3_fuzzing.cpp new file mode 100644 index 00000000..450e63c3 --- /dev/null +++ b/examples/3_fuzzing.cpp @@ -0,0 +1,41 @@ +#define ZEROERR_IMPLEMENTATION +#define ZEROERR_FUZZING +#include "zeroerr.hpp" + +using namespace zeroerr; + + + +int find_the_biggest(std::vector vec) { + if (vec.empty()) { + WARN("Empty vector, vec.size() = {size}", vec.size()); + return 0; + } + int max = 0; + for (int i = 0; i < vec.size(); ++i) { + if (vec[i] > max) {max = vec[i];} + } + return max; +} + +FUZZ_TEST_CASE("presentation") { + LOG("Run fuzz_test"); + FUZZ_FUNC([=](std::vector vec) { + int ans = find_the_biggest(vec); + // verify the result + for (int i = 0; i < vec.size(); ++i) { + CHECK(ans >= vec[i]); + } + if (vec.size() == 0) { + CHECK(ans == 0); + // verify WARN message to make sure the path is correct + CHECK(LOG_GET(find_the_biggest, + "Empty vector, vec.size() = {size}", size, size_t) == 0); + } + }) + .WithDomains(ContainerOf>( + InRange(0, 100))) + .WithSeeds({{{0, 1, 2, 3, 4, 5}}, {{1, 8, 4, 2, 3}}}) + .Run(100); + +} \ No newline at end of file diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 820d1c46..09cf3e5b 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,2 +1,8 @@ -add_executable(1_basic ${CMAKE_CURRENT_SOURCE_DIR}/1_basic.cpp) -add_dependencies(1_basic assemble_single_header) \ No newline at end of file +macro(define_example name) + add_executable(${name} ${CMAKE_CURRENT_SOURCE_DIR}/${name}.cpp) + add_dependencies(${name} assemble_single_header) +endmacro(define_example) + +define_example(1_basic) +define_example(2_log) +define_example(3_fuzzing) diff --git a/include/zeroerr/assert.h b/include/zeroerr/assert.h index 8f296d91..c87be5c8 100644 --- a/include/zeroerr/assert.h +++ b/include/zeroerr/assert.h @@ -21,6 +21,16 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH #define ZEROERR_G_CONTEXT_SCOPE(x) #endif + +/** + * @brief Default printer for assertion messages + * + * This macro defines the default printer for assertion messages. + * It prints the assertion message in different colors based on the assertion level. + * + * The macro can be overridden by defining ZEROERR_PRINT_ASSERT_DEFAULT_PRINTER before + * including this header. (Or undefine it and implement your own printer.) + */ #ifndef ZEROERR_PRINT_ASSERT_DEFAULT_PRINTER #define ZEROERR_PRINT_ASSERT_DEFAULT_PRINTER(cond, level, ...) \ do { \ diff --git a/include/zeroerr/benchmark.h b/include/zeroerr/benchmark.h index 2d71070e..ed3dad9f 100644 --- a/include/zeroerr/benchmark.h +++ b/include/zeroerr/benchmark.h @@ -14,13 +14,14 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH -#define ZEROERR_CREATE_BENCHMARK_FUNC(function, name) \ - static void function(zeroerr::TestContext*); \ - static zeroerr::detail::regTest ZEROERR_NAMEGEN(_zeroerr_reg)( \ - {name, __FILE__, __LINE__, function}, zeroerr::TestType::bench); \ +#define ZEROERR_CREATE_BENCHMARK_FUNC(function, name, ...) \ + static void function(zeroerr::TestContext*); \ + static zeroerr::detail::regTest ZEROERR_NAMEGEN(_zeroerr_reg)( \ + {name, __FILE__, __LINE__, function, {__VA_ARGS__}}, zeroerr::TestType::bench); \ static void function(ZEROERR_UNUSED(zeroerr::TestContext* _ZEROERR_TEST_CONTEXT)) -#define BENCHMARK(name) ZEROERR_CREATE_BENCHMARK_FUNC(ZEROERR_NAMEGEN(_zeroerr_benchmark), name) +#define BENCHMARK(name, ...) \ + ZEROERR_CREATE_BENCHMARK_FUNC(ZEROERR_NAMEGEN(_zeroerr_benchmark), name, __VA_ARGS__) namespace zeroerr { @@ -61,7 +62,7 @@ struct PerformanceCounter { void endMeasure(); void updateResults(uint64_t numIters); - PerfCountSet const& val() const noexcept { return _val; } + const PerfCountSet& val() const noexcept { return _val; } PerfCountSet has() const noexcept { return _has; } static PerformanceCounter& inst(); @@ -164,10 +165,10 @@ struct Benchmark { namespace detail { #if defined(_MSC_VER) -void doNotOptimizeAwaySink(void const*); +void doNotOptimizeAwaySink(const void*); template -void doNotOptimizeAway(T const& val) { +void doNotOptimizeAway(const T& val) { doNotOptimizeAwaySink(&val); } @@ -178,7 +179,7 @@ void doNotOptimizeAway(T const& val) { // Google Benchmark seemed to be the most well tested anyways. see // https://github.com/google/benchmark/blob/master/include/benchmark/benchmark.h#L307 template -void doNotOptimizeAway(T const& val) { +void doNotOptimizeAway(const T& val) { // NOLINTNEXTLINE(hicpp-no-assembler) asm volatile("" : : "r,m"(val) : "memory"); } diff --git a/include/zeroerr/domains/aggregate_of.h b/include/zeroerr/domains/aggregate_of.h index 6588e46d..34f2eff6 100644 --- a/include/zeroerr/domains/aggregate_of.h +++ b/include/zeroerr/domains/aggregate_of.h @@ -13,6 +13,30 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH namespace zeroerr { + +/** + * @brief AggregateOf is a domain that combines multiple inner domains into a tuple or aggregate type + * + * @tparam T The aggregate type to generate (e.g. struct or tuple) + * @tparam Inner The inner domain types that will generate each field + * + * This domain allows generating structured data by composing multiple inner domains. + * Each inner domain generates one field of the aggregate type. + * + * Example: + * ```cpp + * struct Point { + * int x; + * int y; + * }; + * + * auto domain = AggregateOf( + * InRange(0, 100), // Domain for x + * InRange(0, 100) // Domain for y + * ); + * ``` + */ + template class AggregateOf : public Domain> { public: diff --git a/include/zeroerr/domains/arbitrary.h b/include/zeroerr/domains/arbitrary.h index b53ee5c0..c5bdf5d7 100644 --- a/include/zeroerr/domains/arbitrary.h +++ b/include/zeroerr/domains/arbitrary.h @@ -13,8 +13,36 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH namespace zeroerr { -template -class Arbitrary { + +/** + * @brief Arbitrary is a domain that generates random values of a given type + * + * @tparam T The type to generate values for + * @tparam N Template parameter for SFINAE-based specialization selection + * + * This domain provides default random value generation for common types. + * It uses template specialization to handle different types appropriately. + * + * The base template is empty and specializations are provided for: + * - bool + * - unsigned integers + * - signed integers + * - floating point numbers + * - strings + * - containers + * + * Example: + * ```cpp + * auto domain = Arbitrary(); // Generates random integers + * auto domain = Arbitrary(); // Generates random strings + * ``` + */ + +template +class Arbitrary : public Arbitrary {}; + +template +struct Arbitrary { static_assert(detail::always_false::value, "No Arbitrary specialization for this type"); }; @@ -35,7 +63,7 @@ using is_unsigned_int = typename std::enable_if::value && !std::numeric_limits::is_signed, void>::type; template -class Arbitrary> : public DomainConvertable { +class Arbitrary> : public DomainConvertable { public: using ValueType = T; using CorpusType = T; @@ -53,7 +81,7 @@ using is_signed_int = void>::type; template -class Arbitrary> : public DomainConvertable { +class Arbitrary> : public DomainConvertable { public: using ValueType = T; using CorpusType = T; @@ -69,7 +97,7 @@ class Arbitrary> : public DomainConvertable { template using is_float_point = typename std::enable_if::value, void>::type; template -class Arbitrary> : public DomainConvertable { +class Arbitrary> : public DomainConvertable { public: using ValueType = T; using CorpusType = T; @@ -89,7 +117,7 @@ using is_string = typename std::enable_if::value>::type; template -class Arbitrary> : public Domain> { +class Arbitrary> : public Domain> { Arbitrary> impl; public: @@ -108,23 +136,11 @@ class Arbitrary> : public Domain -using is_modifable = - typename std::enable_if::value, - decltype( - // Iterable - T().begin(), T().end(), T().size(), - // Values are mutable - // This rejects associative containers, for example - // *T().begin() = std::declval>(), - // Can insert and erase elements - T().insert(T().end(), std::declval()), - T().erase(T().begin()), - // - (void)0)>::type; +using is_modifiable = typename std::enable_if::value>::type; + template -class Arbitrary> +class Arbitrary> : public SequenceContainerOf> { public: Arbitrary() @@ -133,18 +149,17 @@ class Arbitrary> }; template -class Arbitrary> +class Arbitrary, 1> : public AggregateOf< std::pair::type, typename std::remove_const::type>> {}; template -class Arbitrary> +class Arbitrary, 1> : public AggregateOf::type...>> {}; template -class Arbitrary : public Arbitrary {}; - +class Arbitrary : public Arbitrary {}; } // namespace zeroerr diff --git a/include/zeroerr/domains/container_of.h b/include/zeroerr/domains/container_of.h index 2d1b3ec1..0108a7f7 100644 --- a/include/zeroerr/domains/container_of.h +++ b/include/zeroerr/domains/container_of.h @@ -8,6 +8,29 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH namespace zeroerr { +/** + * @brief ContainerOf is a domain that generates random containers filled with elements from an inner domain + * + * @tparam T The container type to generate (e.g. vector, list, set) + * @tparam InnerDomain The domain type used to generate the container elements + * + * This domain allows generating containers where each element is generated by an inner domain. + * It supports configuring the size constraints of the generated containers. + * + * Example: + * ```cpp + * // Generate vectors of ints between 0-100 + * auto domain = ContainerOf>(InRange(0, 100)); + * + * // Generate sets of strings + * auto domain = ContainerOf>(Arbitrary()); + * + * // Configure size constraints + * domain.WithMinSize(5); // At least 5 elements + * domain.WithMaxSize(10); // At most 10 elements + * domain.WithSize(7); // Exactly 7 elements + * ``` + */ struct ContainerOfBase { int min_size = 0, max_size = 100, size = -1; diff --git a/include/zeroerr/domains/domain.h b/include/zeroerr/domains/domain.h index aad8b9dd..775cbe01 100644 --- a/include/zeroerr/domains/domain.h +++ b/include/zeroerr/domains/domain.h @@ -17,7 +17,6 @@ namespace zeroerr { * but will store the list in a vector, then ValueType will be * std::list and CorpusType will be std::vector. */ - template class Domain { public: @@ -38,6 +37,13 @@ class Domain { // virtual unsigned CountNumberOfFields(CorpusType v) const { return 0; } }; + +/** + * @brief DomainConvertable is a base class for domains that can be converted to and from a ValueType + * + * This class provides default implementations for the GetValue and FromValue methods. + * It is used to convert between the corpus types and the value types. + */ template class DomainConvertable : public Domain { public: diff --git a/include/zeroerr/domains/element_of.h b/include/zeroerr/domains/element_of.h index 444f6d41..fd42c6ff 100644 --- a/include/zeroerr/domains/element_of.h +++ b/include/zeroerr/domains/element_of.h @@ -7,6 +7,23 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH namespace zeroerr { +/** + * @brief ElementOf is a domain that generates random values from a fixed set of elements + * + * @tparam T The type of elements to generate + * + * This domain allows generating random values by selecting from a predefined set of elements. + * The elements are provided as a vector during construction. + * + * Example: + * ```cpp + * // Generate random values from a set of strings + * auto domain = ElementOf({"red", "green", "blue"}); + * + * // Generate random values from a set of integers + * auto domain = ElementOf({1, 2, 3, 4, 5}); + * ``` + */ template class ElementOf : public Domain { public: diff --git a/include/zeroerr/domains/in_range.h b/include/zeroerr/domains/in_range.h index 9ac9551b..2b9cf772 100644 --- a/include/zeroerr/domains/in_range.h +++ b/include/zeroerr/domains/in_range.h @@ -7,6 +7,23 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH namespace zeroerr { +/** + * @brief InRange is a domain that generates random values within a specified range + * + * @tparam T The numeric type to generate values for (e.g. int, float) + * + * This domain generates random values between a minimum and maximum value (inclusive). + * It supports any numeric type that can be used with arithmetic operations. + * + * Example: + * ```cpp + * // Generate integers between 1 and 100 + * auto domain = InRange(1, 100); + * + * // Generate floating point numbers between 0.0 and 1.0 + * auto domain = InRange(0.0, 1.0); + * ``` + */ template class InRange : public DomainConvertable { public: diff --git a/include/zeroerr/fuzztest.h b/include/zeroerr/fuzztest.h index 6ca0cd82..44101b4d 100644 --- a/include/zeroerr/fuzztest.h +++ b/include/zeroerr/fuzztest.h @@ -20,13 +20,14 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH -#define ZEROERR_CREATE_FUZZ_TEST_FUNC(function, name) \ - static void function(zeroerr::TestContext*); \ - static zeroerr::detail::regTest ZEROERR_NAMEGEN(_zeroerr_reg)( \ - {name, __FILE__, __LINE__, function}, zeroerr::TestType::fuzz_test); \ +#define ZEROERR_CREATE_FUZZ_TEST_FUNC(function, name, ...) \ + static void function(zeroerr::TestContext*); \ + static zeroerr::detail::regTest ZEROERR_NAMEGEN(_zeroerr_reg)( \ + {name, __FILE__, __LINE__, function, {__VA_ARGS__}}, zeroerr::TestType::fuzz_test); \ static void function(ZEROERR_UNUSED(zeroerr::TestContext* _ZEROERR_TEST_CONTEXT)) -#define FUZZ_TEST_CASE(name) ZEROERR_CREATE_FUZZ_TEST_FUNC(ZEROERR_NAMEGEN(_zeroerr_testcase), name) +#define FUZZ_TEST_CASE(name, ...) \ + ZEROERR_CREATE_FUZZ_TEST_FUNC(ZEROERR_NAMEGEN(_zeroerr_testcase), name, __VA_ARGS__) #define FUZZ_FUNC(func) zeroerr::FuzzFunction(func, _ZEROERR_TEST_CONTEXT) diff --git a/include/zeroerr/internal/config.h b/include/zeroerr/internal/config.h index d3af5faa..2af28226 100644 --- a/include/zeroerr/internal/config.h +++ b/include/zeroerr/internal/config.h @@ -317,6 +317,23 @@ ZEROERR_CLANG_SUPPRESS_WARNING_POP ZEROERR_GCC_SUPPRESS_WARNING_POP \ ZEROERR_MSVC_SUPPRESS_WARNING_POP +/** + * Macro to suppress unused variable/parameter warnings + * + * This macro can be used to mark variables or parameters as intentionally unused + * while maintaining cross-compiler compatibility. It handles different compiler-specific + * attributes and warning suppressions: + * + * - For Clang/GCC: Uses __attribute__((unused)) + * - For LCLINT: Uses @unused@ comment annotation + * - For MSVC: Suppresses warning C4100 (unreferenced formal parameter) + * - For other compilers: No special handling + * + * Usage example: + * void foo(ZEROERR_UNUSED(int x)) { + * // x is marked as intentionally unused + * } + */ #if ZEROERR_CLANG || ZEROERR_GCC #define ZEROERR_UNUSED(x) x __attribute__((unused)) #elif defined(__LCLINT__) diff --git a/include/zeroerr/internal/serialization.h b/include/zeroerr/internal/serialization.h index cc9840d6..4b1d67f9 100644 --- a/include/zeroerr/internal/serialization.h +++ b/include/zeroerr/internal/serialization.h @@ -13,6 +13,32 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH namespace zeroerr { +/** + * @brief Interface for serializable objects + * + * IRObject (Intermediate Representation Object) provides a low-level interface for serializing + * data types into a common format. It uses a union to store different data types and provides + * type-safe access through templated getter methods. + * + * The object can store: + * - Integers (int64_t) + * - Floating point numbers (double) + * - Strings (char* for long strings, char[8] for short strings) + * - Nested objects (IRObject*) + * + * Memory management: + * - The object takes ownership of allocated strings and nested objects + * - Copy/move operations perform deep copies/moves + * - The destructor frees any owned memory + * + * Usage example: + * @code + * IRObject obj; + * obj.type = IRObject::Int; + * obj.i = 42; + * int value = obj.GetScalar(); // value = 42 + * @endcode + */ struct IRObject { IRObject() { std::memset(this, 0, sizeof(IRObject)); } diff --git a/include/zeroerr/internal/threadsafe.h b/include/zeroerr/internal/threadsafe.h index fb49a271..9189dd31 100644 --- a/include/zeroerr/internal/threadsafe.h +++ b/include/zeroerr/internal/threadsafe.h @@ -1,7 +1,13 @@ #pragma once #include "zeroerr/internal/config.h" -// Thread safety support +/** + * @brief Thread safety support + * This header provides thread-safe support for zeroerr. + * + * It defines macros for mutexes, locks, and atomic operations. + * The macros are conditionally defined based on the ZEROERR_NO_THREAD_SAFE flag. + */ #ifdef ZEROERR_NO_THREAD_SAFE #define ZEROERR_MUTEX(x) diff --git a/include/zeroerr/internal/typetraits.h b/include/zeroerr/internal/typetraits.h index c4746715..c4de6e0d 100644 --- a/include/zeroerr/internal/typetraits.h +++ b/include/zeroerr/internal/typetraits.h @@ -29,6 +29,25 @@ class weak_ptr; namespace zeroerr { +/** + * @brief rank is a helper class for Printer to define the priority of overloaded functions. + * @tparam N the priority of the rule. 0 is the lowest priority. The maximum priority is max_rank. + * + * You can define a rule by adding it as a function parameter with rank where N is the priority. + * For example: + * template + * void Foo(T v, rank<0>); // lowest priority + * void Foo(int v, rank<1>); // higher priority + * + * Even though in the first rule, type T can be an int, the second function will still be called due + * to the priority. + */ +template +struct rank : rank {}; +template <> +struct rank<0> {}; + + namespace detail { // C++11 void_t @@ -129,6 +148,22 @@ template struct is_array()[0])>> : std::true_type {}; +template +struct is_modifiable : std::false_type {}; + +template +struct is_modifiable>(), + // Can insert and erase elements + T().insert(T().end(), std::declval()), + T().erase(T().begin()), (void)0)>> : std::true_type {}; + + + // Check if a type has the element type as std::pair template using has_pair_type = diff --git a/include/zeroerr/print.h b/include/zeroerr/print.h index cb4a8e18..8e84c05e 100644 --- a/include/zeroerr/print.h +++ b/include/zeroerr/print.h @@ -27,23 +27,6 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH namespace zeroerr { -/** - * @brief rank is a helper class for Printer to define the priority of overloaded functions. - * @tparam N the priority of the rule. 0 is the lowest priority. The maximum priority is max_rank. - * - * You can define a rule by adding it as a function parameter with rank where N is the priority. - * For example: - * template - * void Foo(T v, rank<0>); // lowest priority - * void Foo(int v, rank<1>); // higher priority - * - * Even though in the first rule, type T can be an int, the second function will still be called due - * to the priority. - */ -template -struct rank : rank {}; -template <> -struct rank<0> {}; constexpr unsigned max_rank = 5; @@ -163,7 +146,7 @@ struct Printer { (void)_; } - ZEROERR_ENABLE_IF(ZEROERR_IS_CLASS&& ZEROERR_IS_POD) + ZEROERR_ENABLE_IF(ZEROERR_IS_CLASS && ZEROERR_IS_POD) print(const T& value, unsigned level, const char* lb, rank<1>) { os << tab(level) << "{"; print_struct(value, level, isCompact ? " " : line_break, @@ -177,7 +160,7 @@ struct Printer { os << tab(level) << (value ? "true" : "false") << lb; } - ZEROERR_ENABLE_IF(ZEROERR_IS_CLASS&& ZEROERR_IS_STREAMABLE) + ZEROERR_ENABLE_IF(ZEROERR_IS_CLASS && ZEROERR_IS_STREAMABLE) print(T value, unsigned level, const char* lb, rank<2>) { os << tab(level) << value << lb; } @@ -192,7 +175,7 @@ struct Printer { os << tab(level) << "}" << lb; } - ZEROERR_ENABLE_IF(ZEROERR_IS_CONTAINER&& ZEROERR_IS_ARRAY) + ZEROERR_ENABLE_IF(ZEROERR_IS_CONTAINER && ZEROERR_IS_ARRAY) print(const T& value, unsigned level, const char* lb, rank<3>) { os << tab(level) << "["; bool last = false; @@ -212,7 +195,7 @@ struct Printer { os << tab(level) << "<" << type(value) << " at " << value.get() << ">" << lb; } - ZEROERR_ENABLE_IF(ZEROERR_IS_CONTAINER&& ZEROERR_IS_MAP) + ZEROERR_ENABLE_IF(ZEROERR_IS_CONTAINER && ZEROERR_IS_MAP) print(const T& value, unsigned level, const char* lb, rank<4>) { os << tab(level) << "{" << (isCompact ? "" : line_break); bool last = false; diff --git a/include/zeroerr/unittest.h b/include/zeroerr/unittest.h index 5c71aa81..5931eb79 100644 --- a/include/zeroerr/unittest.h +++ b/include/zeroerr/unittest.h @@ -2,25 +2,27 @@ #include "zeroerr/internal/config.h" +#include #include #include #include ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH -#define ZEROERR_CREATE_TEST_FUNC(function, name) \ +#define ZEROERR_CREATE_TEST_FUNC(function, name, ...) \ static void function(zeroerr::TestContext*); \ static zeroerr::detail::regTest ZEROERR_NAMEGEN(_zeroerr_reg)( \ - {name, __FILE__, __LINE__, function}); \ + {name, __FILE__, __LINE__, function, {__VA_ARGS__}}); \ static void function(ZEROERR_UNUSED(zeroerr::TestContext* _ZEROERR_TEST_CONTEXT)) -#define TEST_CASE(name) ZEROERR_CREATE_TEST_FUNC(ZEROERR_NAMEGEN(_zeroerr_testcase), name) +#define TEST_CASE(name, ...) \ + ZEROERR_CREATE_TEST_FUNC(ZEROERR_NAMEGEN(_zeroerr_testcase), name, __VA_ARGS__) -#define SUB_CASE(name) \ - zeroerr::SubCase(name, __FILE__, __LINE__, _ZEROERR_TEST_CONTEXT) \ +#define SUB_CASE(name, ...) \ + zeroerr::SubCase(name, __FILE__, __LINE__, _ZEROERR_TEST_CONTEXT, {__VA_ARGS__}) \ << [=](ZEROERR_UNUSED(zeroerr::TestContext * _ZEROERR_TEST_CONTEXT)) mutable -#define ZEROERR_CREATE_TEST_CLASS(fixture, classname, funcname, name) \ +#define ZEROERR_CREATE_TEST_CLASS(fixture, classname, funcname, name, ...) \ class classname : public fixture { \ public: \ void funcname(zeroerr::TestContext*); \ @@ -30,12 +32,12 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH instance.funcname(_ZEROERR_TEST_CONTEXT); \ } \ static zeroerr::detail::regTest ZEROERR_NAMEGEN(_zeroerr_reg)( \ - {name, __FILE__, __LINE__, ZEROERR_CAT(call_, funcname)}); \ + {name, __FILE__, __LINE__, ZEROERR_CAT(call_, funcname), {__VA_ARGS__}}); \ inline void classname::funcname(ZEROERR_UNUSED(zeroerr::TestContext* _ZEROERR_TEST_CONTEXT)) -#define TEST_CASE_FIXTURE(fixture, name) \ +#define TEST_CASE_FIXTURE(fixture, name, ...) \ ZEROERR_CREATE_TEST_CLASS(fixture, ZEROERR_NAMEGEN(_zeroerr_class), \ - ZEROERR_NAMEGEN(_zeroerr_test_method), name) + ZEROERR_NAMEGEN(_zeroerr_test_method), name, __VA_ARGS__) #define ZEROERR_HAVE_SAME_OUTPUT _ZEROERR_TEST_CONTEXT->save_output(); @@ -51,6 +53,7 @@ namespace zeroerr { class IReporter; struct TestCase; +class Decorator; /** * @brief TestContext is a class that holds the test results and reporter context. @@ -66,8 +69,16 @@ struct TestCase; */ class TestContext { public: - unsigned passed = 0, warning = 0, failed = 0, skipped = 0; - unsigned passed_as = 0, warning_as = 0, failed_as = 0, skipped_as = 0; + unsigned passed = 0; + unsigned warning = 0; + unsigned failed = 0; + unsigned skipped = 0; + unsigned passed_as = 0; + unsigned warning_as = 0; + unsigned failed_as = 0; + unsigned skipped_as = 0; + + std::chrono::duration duration = std::chrono::duration::zero(); IReporter& reporter; @@ -160,7 +171,7 @@ struct TestCase { unsigned line; std::function func; std::vector subcases; - + std::vector decorators; /** * @brief Compare the test cases. * @param rhs The test case that will be compared. @@ -174,8 +185,8 @@ struct TestCase { * @param file The file that the test case is defined. * @param line The line that the test case is defined. */ - TestCase(std::string name, std::string file, unsigned line) - : name(name), file(file), line(line) {} + TestCase(std::string name, std::string file, unsigned line, std::vector decorators) + : name(name), file(file), line(line), decorators(decorators) {} /** * @brief Construct a new Test Case object @@ -183,10 +194,11 @@ struct TestCase { * @param file The file that the test case is defined. * @param line The line that the test case is defined. * @param func The function that will be run to test the test case. + * @param decorators The decorators that will be used to decorate the test case. */ TestCase(std::string name, std::string file, unsigned line, - std::function func) - : name(name), file(file), line(line), func(func) {} + std::function func, std::vector decorators) + : name(name), file(file), line(line), func(func), decorators(decorators) {} }; @@ -194,7 +206,8 @@ struct TestCase { * @brief SubCase is a class that holds the subcase information. */ struct SubCase : TestCase { - SubCase(std::string name, std::string file, unsigned line, TestContext* context); + SubCase(std::string name, std::string file, unsigned line, TestContext* context, + std::vector decorators); ~SubCase() = default; TestContext* context; void operator<<(std::function op); @@ -239,7 +252,7 @@ class IReporter { * @brief Create the reporter object with the given name. * @param name The name of the reporter. Available reporters are: console, xml. * @param ut The unit test object that will be used to configure the test. - */ + */ static IReporter* create(const std::string& name, UnitTest& ut); IReporter(UnitTest& ut) : ut(ut) {} @@ -284,7 +297,7 @@ struct regReporter { * }); * test(a, b); * ``` - * + * * This will test the targetFunc with all the combinations of a and b, e.g. (1,4), (1,5), (1,6), * (2,4), (2,5) ... etc. */ @@ -332,6 +345,27 @@ class TestArgs { int index = 0; }; + +class Decorator { +public: + // Called when the test registered, return true can block the test registering + virtual bool onStartup(const TestCase&) { return false; } + + // Called when the test executing, return true can block the test execution + virtual bool onExecution(const TestCase&) { return false; } + + // Called on each assertion, return true can skip the assertion + virtual bool onAssertion() { return false; } + + // Called when the test finished, return true means the test containing errors + virtual bool onFinish(const TestCase&, const TestContext&) { return false; } +}; + +Decorator* skip(bool isSkip = true); +Decorator* timeout(float timeout = 0.1f); // in seconds +Decorator* may_fail(bool isMayFail = true); +Decorator* should_fail(bool isShouldFail = true); + } // namespace zeroerr ZEROERR_SUPPRESS_COMMON_WARNINGS_POP \ No newline at end of file diff --git a/scripts/gen-single-file.cmake b/scripts/gen-single-file.cmake index f500cb5a..d04a0e0d 100644 --- a/scripts/gen-single-file.cmake +++ b/scripts/gen-single-file.cmake @@ -44,6 +44,7 @@ loadfile(${my_src_folder}/log.cpp log_cpp) loadfile(${my_src_folder}/table.cpp table_cpp) loadfile(${my_src_folder}/unittest.cpp unittest_cpp) loadfile(${my_src_folder}/fuzztest.cpp fuzztest_cpp) +loadfile(${my_src_folder}/serialization.cpp serialization_cpp) file(WRITE zeroerr.hpp "// ======================================================================\n") @@ -53,5 +54,5 @@ file(APPEND zeroerr.hpp "${config}\n${color}\n${console}\n${debugbreak}\n${threa file(APPEND zeroerr.hpp "${domain}\n${in_range}\n${element_of}\n${container_of}\n${aggregate_of}\n${arbitrary}\n") file(APPEND zeroerr.hpp "${benchmark}\n${assert}\n${dbg}\n${format}\n${log}\n${table}\n${profiler}\n${unittest}\n${fuzztest}\n") file(APPEND zeroerr.hpp "#ifdef ZEROERR_IMPLEMENTATION\n") -file(APPEND zeroerr.hpp "${rng_cpp}\n${color_cpp}\n${print_cpp}\n${console_cpp}\n${log_cpp}\n${table_cpp}\n${unittest_cpp}\n${fuzztest_cpp}\n${benchmark_cpp}\n") +file(APPEND zeroerr.hpp "${rng_cpp}\n${color_cpp}\n${print_cpp}\n${console_cpp}\n${log_cpp}\n${table_cpp}\n${unittest_cpp}\n${fuzztest_cpp}\n${serialization_cpp}\n${benchmark_cpp}\n") file(APPEND zeroerr.hpp "#endif // ZEROERR_IMPLEMENTATION\n") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 22fefc2c..14a85d53 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -17,7 +17,9 @@ set(source_files ${source_files} PARENT_SCOPE) add_library(zeroerr STATIC ${source_files}) target_compile_options(zeroerr PRIVATE - $<$:/W4> + $<$:/W4 /utf-8> + $<$:/utf-8> + $<$:-fstandalone-debug> $<$>:-Wall -Wextra -Wpedantic> ) diff --git a/src/fuzztest.cpp b/src/fuzztest.cpp index fe6328e9..c95c3f95 100644 --- a/src/fuzztest.cpp +++ b/src/fuzztest.cpp @@ -58,6 +58,13 @@ void RunFuzzTest(IFuzzTest& fuzz_test, int seed, int runs, int max_len, int time }); current_fuzz_test = nullptr; +#else + (void) fuzz_test; + (void) seed; + (void) runs; + (void) max_len; + (void) timeout; + (void) len_control; #endif } diff --git a/src/log.cpp b/src/log.cpp index 82a5bb24..5c93b46d 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -39,7 +39,7 @@ LogInfo::LogInfo(const char* filename, const char* function, const char* message const char* q = p + 1; while (*q && *q != '}') q++; if (*q == '}') { - std::string N(p + 1, (size_t)(q-p-1)); + std::string N(p + 1, (size_t)(q - p - 1)); names[N] = static_cast(names.size()); p = q; } @@ -162,10 +162,15 @@ void* LogStream::getRawLog(std::string func, unsigned line, std::string name) { return nullptr; } +static bool startWith(const std::string& str, const std::string& prefix) { + return str.rfind(prefix, 0) == 0; +} + void* LogStream::getRawLog(std::string func, std::string msg, std::string name) { for (DataBlock* p = first; p; p = p->next) for (auto q = p->begin(); q < p->end(); q = moveBytes(q, q->info->size)) - if (msg == q->info->message && func == q->info->function) return q->getRawLog(name); + if (startWith(q->info->message, msg) && func == q->info->function) + return q->getRawLog(name); return nullptr; } @@ -207,7 +212,7 @@ LogIterator& LogIterator::operator++() { } bool LogIterator::check_filter() { - if (!message_filter.empty() && q->info->message != message_filter) return false; + if (!message_filter.empty() && startWith(q->info->message, message_filter)) return false; if (!function_name_filter.empty() && q->info->function != function_name_filter) return false; if (line_filter != -1 && static_cast(q->info->line) != line_filter) return false; return true; diff --git a/src/unittest.cpp b/src/unittest.cpp index 34dd9eb7..ef013e32 100644 --- a/src/unittest.cpp +++ b/src/unittest.cpp @@ -75,8 +75,9 @@ static inline std::string getFileName(std::string file) { return fileName; } -SubCase::SubCase(std::string name, std::string file, unsigned line, TestContext* context) - : TestCase(name, file, line), context(context) {} +SubCase::SubCase(std::string name, std::string file, unsigned line, TestContext* context, + std::vector decorators) + : TestCase(name, file, line, decorators), context(context) {} void SubCase::operator<<(std::function op) { func = op; @@ -235,6 +236,20 @@ bool UnitTest::run_filter(const TestCase& tc) { return true; } +static bool runOnExecution(const TestCase& tc) { + for (auto& decorator : tc.decorators) { + if (decorator->onExecution(tc)) return true; + } + return false; +} + +static bool runOnFinish(const TestCase& tc, const TestContext& ctx) { + for (auto& decorator : tc.decorators) { + if (decorator->onFinish(tc, ctx)) return true; + } + return false; +} + int UnitTest::run() { IReporter* reporter = IReporter::create(reporter_name, *this); if (!reporter) reporter = IReporter::create("console", *this); @@ -250,11 +265,16 @@ int UnitTest::run() { for (auto& tc : test_cases) { if (!run_filter(tc)) continue; + if (runOnExecution(tc)) { + sum.skipped += 1; + continue; + } reporter->testCaseStart(tc, new_buf); if (!list_test_cases) { std::streambuf* orig_buf = std::cerr.rdbuf(); std::cerr.rdbuf(&new_buf); std::cerr << std::endl; + auto start = std::chrono::high_resolution_clock::now(); try { tc.func(&context); // run the test case } catch (const AssertionData&) { @@ -265,9 +285,17 @@ int UnitTest::run() { context.failed_as = 1; } } + auto end = std::chrono::high_resolution_clock::now(); + context.duration = end - start; std::cerr.rdbuf(orig_buf); } int type = sum.add(context); + if (runOnFinish(tc, context)) { + if (type != 2) { + sum.failed += 1; + type = 2; + } + } reporter->testCaseEnd(tc, new_buf, context, type); context.reset(); new_buf.str(""); @@ -305,7 +333,12 @@ static std::set getRegisteredTests(unsigned type) { return result; } -regTest::regTest(const TestCase& tc, TestType type) { getTestSet(type).insert(tc); } +regTest::regTest(const TestCase& tc, TestType type) { + for (auto& decorator : tc.decorators) { + if (decorator->onStartup(tc)) return; + } + getTestSet(type).insert(tc); +} static std::set& getRegisteredReporters() { static std::set data; @@ -591,8 +624,8 @@ XmlWriter::ScopedElement::~ScopedElement() { if (m_writer) m_writer->endElement(); } -XmlWriter::ScopedElement& XmlWriter::ScopedElement::writeText(const std::string& text, - bool indent, bool new_line) { +XmlWriter::ScopedElement& XmlWriter::ScopedElement::writeText(const std::string& text, bool indent, + bool new_line) { m_writer->writeText(text, indent, new_line); return *this; } @@ -626,8 +659,10 @@ XmlWriter& XmlWriter::endElement() { m_os << "/>"; m_tagIsOpen = false; } else { - if (m_needsIndent) m_os << m_indent; - else m_needsIndent = true; + if (m_needsIndent) + m_os << m_indent; + else + m_needsIndent = true; m_os << ""; } m_os << std::endl; @@ -659,7 +694,7 @@ XmlWriter& XmlWriter::writeText(const std::string& text, bool indent, bool new_l if (tagWasOpen && indent) m_os << m_indent; m_os << XmlEncode(text); m_needsNewline = new_line; - m_needsIndent = new_line; + m_needsIndent = new_line; } return *this; } @@ -717,8 +752,8 @@ class XmlReporter : public IReporter { if (ut.log_to_report) suspendLog(); } - virtual void testCaseEnd(const TestCase& tc, std::stringbuf& sb, const TestContext& ctx, - int) override { + virtual void testCaseEnd(ZEROERR_UNUSED(const TestCase&), std::stringbuf& sb, + const TestContext& ctx, int) override { current.pop_back(); xml.scopedElement("Result") .writeAttribute("time", 0) @@ -786,6 +821,63 @@ IReporter* IReporter::create(const std::string& name, UnitTest& ut) { } +class SkipDecorator : public Decorator { + bool onExecution(const TestCase&) override { return true; } +}; + +Decorator* skip(bool isSkip) { + static SkipDecorator skip_dec; + if (isSkip) return &skip_dec; + return nullptr; +} + +class TimeoutDecorator : public Decorator { + float timeout; + +public: + TimeoutDecorator() : timeout(0) {} + TimeoutDecorator(float timeout) : timeout(timeout) {} + + bool onFinish(const TestCase& tc, const TestContext& ctx) override { + if (ctx.duration > std::chrono::duration(timeout)) { + std::cerr << FgRed << "Timeout: " << Reset << ctx.duration.count() << "s > " << timeout << "s" << std::endl; + return true; + } + return false; + } +}; + +Decorator* timeout(float timeout) { + static std::map timeout_dec; + if (timeout_dec.find(timeout) == timeout_dec.end()) { + timeout_dec[timeout] = TimeoutDecorator(timeout); + } + return &timeout_dec[timeout]; +} + +class FailureDecorator : public Decorator { +public: + enum FailureType { may_fail, should_fail }; + FailureDecorator(FailureType type) : type(type) {} + +private: + FailureType type; +}; + + +Decorator* may_fail(bool isMayFail) { + static FailureDecorator may_fail_dec(FailureDecorator::may_fail); + if (isMayFail) return &may_fail_dec; + return nullptr; +} + +Decorator* should_fail(bool isShouldFail) { + static FailureDecorator should_fail_dec(FailureDecorator::should_fail); + if (isShouldFail) return &should_fail_dec; + return nullptr; +} + + } // namespace zeroerr diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d6163085..1e33da12 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -32,23 +32,23 @@ if(CMAKE_CUDA_COMPILER AND NOT DISABLE_CUDA_BUILD) message(STATUS " CUDA ${CMAKE_CUDA_COMPILER_ID} ${CMAKE_CUDA_COMPILER_VERSION} enabled ") set(CMAKE_CUDA_STANDARD 17) - + set(CMAKE_CUDA_STANDARD_REQUIRED ON) set(CMAKE_CUDA_ARCHITECTURES 80) set(cuda_files cuda_test.cu) -else() - set(cuda_files "") + add_executable(cudatest ${cuda_files}) + target_link_libraries(cudatest zeroerr) endif() ## Test Cases set(test_files - fuzz_test.cpp - llvm_test.cpp - log_test.cpp - print_test.cpp - table_test.cpp - unit_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/fuzz_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/llvm_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/log_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/print_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/table_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/unit_test.cpp ) if (ENABLE_FUZZING) @@ -57,12 +57,14 @@ else() set(libfuzzer "") endif() -add_executable(unittest ${test_files} ${cuda_files}) +add_executable(unittest ${test_files}) target_link_libraries(unittest zeroerr ${libfuzzer} ${llvm_libs}) target_compile_options(unittest PRIVATE - $<$:/W4> + $<$:/W4 /utf-8> + $<$:/utf-8> + $<$:-fstandalone-debug> $<$>:-Wall -Wextra -Wpedantic> - ) +) ## Fuzzing Support if (ENABLE_FUZZING) diff --git a/test/fuzz_test.cpp b/test/fuzz_test.cpp index f95c9d9f..1a2dd025 100644 --- a/test/fuzz_test.cpp +++ b/test/fuzz_test.cpp @@ -67,6 +67,40 @@ FUZZ_TEST_CASE("fuzz_test4") { } + +int find_the_biggest(std::vector vec) { + if (vec.empty()) { + WARN("Empty vector, vec.size() = {size}", vec.size()); + return 0; + } + int max = 0; + for (int i = 0; i < vec.size(); ++i) { + if (vec[i] > max) {max = vec[i];} + } + return max; +} + +FUZZ_TEST_CASE("presentation") { + LOG("Run fuzz_test"); + FUZZ_FUNC([=](std::vector vec) { + int ans = find_the_biggest(vec); + // verify the result + for (int i = 0; i < vec.size(); ++i) { + CHECK(ans >= vec[i]); + } + if (vec.size() == 0) { + CHECK(ans == 0); + // verify WARN message to make sure the path is correct + CHECK(LOG_GET(find_the_biggest, + "Empty vector, vec.size() = {size}", size, size_t) == 0); + } + }) + .WithDomains(ContainerOf>( + InRange(0, 100))) + .WithSeeds({{{0, 1, 2, 3, 4, 5}}, {{1, 8, 4, 2, 3}}}) + .Run(100); +} + TEST_CASE("fuzz_serialize") { LOG("fuzz_serialize"); diff --git a/test/log_test.cpp b/test/log_test.cpp index ee97fb45..dceecfb0 100644 --- a/test/log_test.cpp +++ b/test/log_test.cpp @@ -121,6 +121,7 @@ TEST_CASE("verbose") { static void function() { LOG("function log {i}", 1); LOG("function log {sum}, {i}", 10, 1); + LOG("A: message {i}", 1); } TEST_CASE("access log in Test case") { @@ -146,6 +147,8 @@ TEST_CASE("access log in Test case") { CHECK(LOG_GET(function, "function log {i}", i, int) == 1); CHECK(LOG_GET(function, "function log {sum}, {i}", sum, int) == 9); CHECK(LOG_GET(function, "function log {sum}, {i}", i, int) == 2); + + CHECK(LOG_GET(function, "A", i, int) == 1); zeroerr::resumeLog(); } @@ -184,9 +187,10 @@ TEST_CASE("multiple log stream") { } TEST_CASE("log to dir") { - zeroerr::LogStream::getDefault().setFileLogger("./logdir", LogStream::SPLIT_BY_CATEGORY, - LogStream::SPLIT_BY_SEVERITY, - LogStream::DAILY_FILE); + zeroerr::LogStream::getDefault() + .setFileLogger("./logdir", LogStream::SPLIT_BY_CATEGORY, + LogStream::SPLIT_BY_SEVERITY, + LogStream::DAILY_FILE); LOG("log to dir {i}", 1); WARN("warn log to dir {i}", 2); zeroerr::LogStream::getDefault().setStderrLogger(); diff --git a/test/unit_test.cpp b/test/unit_test.cpp index df999fc6..a2d01eb6 100644 --- a/test/unit_test.cpp +++ b/test/unit_test.cpp @@ -3,7 +3,7 @@ #include "zeroerr/dbg.h" #include "zeroerr/print.h" #include "zeroerr/unittest.h" - +#include using namespace zeroerr; @@ -237,3 +237,21 @@ SCENARIO("vectors can be sized and resized") { }; }; } + + +TEST_CASE("Test may fail", may_fail()) { + CHECK(0 == 1); +} + +TEST_CASE("Test should fail", should_fail()) { + CHECK(0 == 1); +} + +TEST_CASE("Test skip", skip()) { + CHECK(0 == 1); +} + +TEST_CASE("Test timeout", timeout(0.01)) { + std::this_thread::sleep_for(std::chrono::duration(0.05)); +} + diff --git a/zeroerr.hpp b/zeroerr.hpp index a50d811e..21587e75 100644 --- a/zeroerr.hpp +++ b/zeroerr.hpp @@ -320,6 +320,23 @@ ZEROERR_CLANG_SUPPRESS_WARNING_POP ZEROERR_GCC_SUPPRESS_WARNING_POP \ ZEROERR_MSVC_SUPPRESS_WARNING_POP +/** + * Macro to suppress unused variable/parameter warnings + * + * This macro can be used to mark variables or parameters as intentionally unused + * while maintaining cross-compiler compatibility. It handles different compiler-specific + * attributes and warning suppressions: + * + * - For Clang/GCC: Uses __attribute__((unused)) + * - For LCLINT: Uses @unused@ comment annotation + * - For MSVC: Suppresses warning C4100 (unreferenced formal parameter) + * - For other compilers: No special handling + * + * Usage example: + * void foo(ZEROERR_UNUSED(int x)) { + * // x is marked as intentionally unused + * } + */ #if ZEROERR_CLANG || ZEROERR_GCC #define ZEROERR_UNUSED(x) x __attribute__((unused)) #elif defined(__LCLINT__) @@ -673,7 +690,13 @@ __attribute__((always_inline)) __inline__ static bool isDebuggerActive() { retur #pragma once -// Thread safety support +/** + * @brief Thread safety support + * This header provides thread-safe support for zeroerr. + * + * It defines macros for mutexes, locks, and atomic operations. + * The macros are conditionally defined based on the ZEROERR_NO_THREAD_SAFE flag. + */ #ifdef ZEROERR_NO_THREAD_SAFE #define ZEROERR_MUTEX(x) @@ -724,6 +747,25 @@ class weak_ptr; namespace zeroerr { +/** + * @brief rank is a helper class for Printer to define the priority of overloaded functions. + * @tparam N the priority of the rule. 0 is the lowest priority. The maximum priority is max_rank. + * + * You can define a rule by adding it as a function parameter with rank where N is the priority. + * For example: + * template + * void Foo(T v, rank<0>); // lowest priority + * void Foo(int v, rank<1>); // higher priority + * + * Even though in the first rule, type T can be an int, the second function will still be called due + * to the priority. + */ +template +struct rank : rank {}; +template <> +struct rank<0> {}; + + namespace detail { // C++11 void_t @@ -824,6 +866,22 @@ template struct is_array()[0])>> : std::true_type {}; +template +struct is_modifiable : std::false_type {}; + +template +struct is_modifiable>(), + // Can insert and erase elements + T().insert(T().end(), std::declval()), + T().erase(T().begin()), (void)0)>> : std::true_type {}; + + + // Check if a type has the element type as std::pair template using has_pair_type = @@ -975,6 +1033,32 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH namespace zeroerr { +/** + * @brief Interface for serializable objects + * + * IRObject (Intermediate Representation Object) provides a low-level interface for serializing + * data types into a common format. It uses a union to store different data types and provides + * type-safe access through templated getter methods. + * + * The object can store: + * - Integers (int64_t) + * - Floating point numbers (double) + * - Strings (char* for long strings, char[8] for short strings) + * - Nested objects (IRObject*) + * + * Memory management: + * - The object takes ownership of allocated strings and nested objects + * - Copy/move operations perform deep copies/moves + * - The destructor frees any owned memory + * + * Usage example: + * @code + * IRObject obj; + * obj.type = IRObject::Int; + * obj.i = 42; + * int value = obj.GetScalar(); // value = 42 + * @endcode + */ struct IRObject { IRObject() { std::memset(this, 0, sizeof(IRObject)); } @@ -1242,23 +1326,6 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH namespace zeroerr { -/** - * @brief rank is a helper class for Printer to define the priority of overloaded functions. - * @tparam N the priority of the rule. 0 is the lowest priority. The maximum priority is max_rank. - * - * You can define a rule by adding it as a function parameter with rank where N is the priority. - * For example: - * template - * void Foo(T v, rank<0>); // lowest priority - * void Foo(int v, rank<1>); // higher priority - * - * Even though in the first rule, type T can be an int, the second function will still be called due - * to the priority. - */ -template -struct rank : rank {}; -template <> -struct rank<0> {}; constexpr unsigned max_rank = 5; @@ -1378,7 +1445,7 @@ struct Printer { (void)_; } - ZEROERR_ENABLE_IF(ZEROERR_IS_CLASS&& ZEROERR_IS_POD) + ZEROERR_ENABLE_IF(ZEROERR_IS_CLASS && ZEROERR_IS_POD) print(const T& value, unsigned level, const char* lb, rank<1>) { os << tab(level) << "{"; print_struct(value, level, isCompact ? " " : line_break, @@ -1392,7 +1459,7 @@ struct Printer { os << tab(level) << (value ? "true" : "false") << lb; } - ZEROERR_ENABLE_IF(ZEROERR_IS_CLASS&& ZEROERR_IS_STREAMABLE) + ZEROERR_ENABLE_IF(ZEROERR_IS_CLASS && ZEROERR_IS_STREAMABLE) print(T value, unsigned level, const char* lb, rank<2>) { os << tab(level) << value << lb; } @@ -1407,7 +1474,7 @@ struct Printer { os << tab(level) << "}" << lb; } - ZEROERR_ENABLE_IF(ZEROERR_IS_CONTAINER&& ZEROERR_IS_ARRAY) + ZEROERR_ENABLE_IF(ZEROERR_IS_CONTAINER && ZEROERR_IS_ARRAY) print(const T& value, unsigned level, const char* lb, rank<3>) { os << tab(level) << "["; bool last = false; @@ -1427,7 +1494,7 @@ struct Printer { os << tab(level) << "<" << type(value) << " at " << value.get() << ">" << lb; } - ZEROERR_ENABLE_IF(ZEROERR_IS_CONTAINER&& ZEROERR_IS_MAP) + ZEROERR_ENABLE_IF(ZEROERR_IS_CONTAINER && ZEROERR_IS_MAP) print(const T& value, unsigned level, const char* lb, rank<4>) { os << tab(level) << "{" << (isCompact ? "" : line_break); bool last = false; @@ -2045,7 +2112,6 @@ namespace zeroerr { * but will store the list in a vector, then ValueType will be * std::list and CorpusType will be std::vector. */ - template class Domain { public: @@ -2066,6 +2132,13 @@ class Domain { // virtual unsigned CountNumberOfFields(CorpusType v) const { return 0; } }; + +/** + * @brief DomainConvertable is a base class for domains that can be converted to and from a ValueType + * + * This class provides default implementations for the GetValue and FromValue methods. + * It is used to convert between the corpus types and the value types. + */ template class DomainConvertable : public Domain { public: @@ -2087,6 +2160,23 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH namespace zeroerr { +/** + * @brief InRange is a domain that generates random values within a specified range + * + * @tparam T The numeric type to generate values for (e.g. int, float) + * + * This domain generates random values between a minimum and maximum value (inclusive). + * It supports any numeric type that can be used with arithmetic operations. + * + * Example: + * ```cpp + * // Generate integers between 1 and 100 + * auto domain = InRange(1, 100); + * + * // Generate floating point numbers between 0.0 and 1.0 + * auto domain = InRange(0.0, 1.0); + * ``` + */ template class InRange : public DomainConvertable { public: @@ -2123,6 +2213,23 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH namespace zeroerr { +/** + * @brief ElementOf is a domain that generates random values from a fixed set of elements + * + * @tparam T The type of elements to generate + * + * This domain allows generating random values by selecting from a predefined set of elements. + * The elements are provided as a vector during construction. + * + * Example: + * ```cpp + * // Generate random values from a set of strings + * auto domain = ElementOf({"red", "green", "blue"}); + * + * // Generate random values from a set of integers + * auto domain = ElementOf({1, 2, 3, 4, 5}); + * ``` + */ template class ElementOf : public Domain { public: @@ -2167,6 +2274,29 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH namespace zeroerr { +/** + * @brief ContainerOf is a domain that generates random containers filled with elements from an inner domain + * + * @tparam T The container type to generate (e.g. vector, list, set) + * @tparam InnerDomain The domain type used to generate the container elements + * + * This domain allows generating containers where each element is generated by an inner domain. + * It supports configuring the size constraints of the generated containers. + * + * Example: + * ```cpp + * // Generate vectors of ints between 0-100 + * auto domain = ContainerOf>(InRange(0, 100)); + * + * // Generate sets of strings + * auto domain = ContainerOf>(Arbitrary()); + * + * // Configure size constraints + * domain.WithMinSize(5); // At least 5 elements + * domain.WithMaxSize(10); // At most 10 elements + * domain.WithSize(7); // Exactly 7 elements + * ``` + */ struct ContainerOfBase { int min_size = 0, max_size = 100, size = -1; @@ -2328,6 +2458,30 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH namespace zeroerr { + +/** + * @brief AggregateOf is a domain that combines multiple inner domains into a tuple or aggregate type + * + * @tparam T The aggregate type to generate (e.g. struct or tuple) + * @tparam Inner The inner domain types that will generate each field + * + * This domain allows generating structured data by composing multiple inner domains. + * Each inner domain generates one field of the aggregate type. + * + * Example: + * ```cpp + * struct Point { + * int x; + * int y; + * }; + * + * auto domain = AggregateOf( + * InRange(0, 100), // Domain for x + * InRange(0, 100) // Domain for y + * ); + * ``` + */ + template class AggregateOf : public Domain> { public: @@ -2424,8 +2578,36 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH namespace zeroerr { -template -class Arbitrary { + +/** + * @brief Arbitrary is a domain that generates random values of a given type + * + * @tparam T The type to generate values for + * @tparam N Template parameter for SFINAE-based specialization selection + * + * This domain provides default random value generation for common types. + * It uses template specialization to handle different types appropriately. + * + * The base template is empty and specializations are provided for: + * - bool + * - unsigned integers + * - signed integers + * - floating point numbers + * - strings + * - containers + * + * Example: + * ```cpp + * auto domain = Arbitrary(); // Generates random integers + * auto domain = Arbitrary(); // Generates random strings + * ``` + */ + +template +class Arbitrary : public Arbitrary {}; + +template +struct Arbitrary { static_assert(detail::always_false::value, "No Arbitrary specialization for this type"); }; @@ -2446,7 +2628,7 @@ using is_unsigned_int = typename std::enable_if::value && !std::numeric_limits::is_signed, void>::type; template -class Arbitrary> : public DomainConvertable { +class Arbitrary> : public DomainConvertable { public: using ValueType = T; using CorpusType = T; @@ -2464,7 +2646,7 @@ using is_signed_int = void>::type; template -class Arbitrary> : public DomainConvertable { +class Arbitrary> : public DomainConvertable { public: using ValueType = T; using CorpusType = T; @@ -2480,7 +2662,7 @@ class Arbitrary> : public DomainConvertable { template using is_float_point = typename std::enable_if::value, void>::type; template -class Arbitrary> : public DomainConvertable { +class Arbitrary> : public DomainConvertable { public: using ValueType = T; using CorpusType = T; @@ -2500,7 +2682,7 @@ using is_string = typename std::enable_if::value>::type; template -class Arbitrary> : public Domain> { +class Arbitrary> : public Domain> { Arbitrary> impl; public: @@ -2519,23 +2701,11 @@ class Arbitrary> : public Domain -using is_modifable = - typename std::enable_if::value, - decltype( - // Iterable - T().begin(), T().end(), T().size(), - // Values are mutable - // This rejects associative containers, for example - // *T().begin() = std::declval>(), - // Can insert and erase elements - T().insert(T().end(), std::declval()), - T().erase(T().begin()), - // - (void)0)>::type; +using is_modifiable = typename std::enable_if::value>::type; + template -class Arbitrary> +class Arbitrary> : public SequenceContainerOf> { public: Arbitrary() @@ -2544,18 +2714,17 @@ class Arbitrary> }; template -class Arbitrary> +class Arbitrary, 1> : public AggregateOf< std::pair::type, typename std::remove_const::type>> {}; template -class Arbitrary> +class Arbitrary, 1> : public AggregateOf::type...>> {}; template -class Arbitrary : public Arbitrary {}; - +class Arbitrary : public Arbitrary {}; } // namespace zeroerr @@ -2576,13 +2745,14 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_POP ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH -#define ZEROERR_CREATE_BENCHMARK_FUNC(function, name) \ - static void function(zeroerr::TestContext*); \ - static zeroerr::detail::regTest ZEROERR_NAMEGEN(_zeroerr_reg)( \ - {name, __FILE__, __LINE__, function}, zeroerr::TestType::bench); \ +#define ZEROERR_CREATE_BENCHMARK_FUNC(function, name, ...) \ + static void function(zeroerr::TestContext*); \ + static zeroerr::detail::regTest ZEROERR_NAMEGEN(_zeroerr_reg)( \ + {name, __FILE__, __LINE__, function, {__VA_ARGS__}}, zeroerr::TestType::bench); \ static void function(ZEROERR_UNUSED(zeroerr::TestContext* _ZEROERR_TEST_CONTEXT)) -#define BENCHMARK(name) ZEROERR_CREATE_BENCHMARK_FUNC(ZEROERR_NAMEGEN(_zeroerr_benchmark), name) +#define BENCHMARK(name, ...) \ + ZEROERR_CREATE_BENCHMARK_FUNC(ZEROERR_NAMEGEN(_zeroerr_benchmark), name, __VA_ARGS__) namespace zeroerr { @@ -2623,7 +2793,7 @@ struct PerformanceCounter { void endMeasure(); void updateResults(uint64_t numIters); - PerfCountSet const& val() const noexcept { return _val; } + const PerfCountSet& val() const noexcept { return _val; } PerfCountSet has() const noexcept { return _has; } static PerformanceCounter& inst(); @@ -2726,10 +2896,10 @@ struct Benchmark { namespace detail { #if defined(_MSC_VER) -void doNotOptimizeAwaySink(void const*); +void doNotOptimizeAwaySink(const void*); template -void doNotOptimizeAway(T const& val) { +void doNotOptimizeAway(const T& val) { doNotOptimizeAwaySink(&val); } @@ -2740,7 +2910,7 @@ void doNotOptimizeAway(T const& val) { // Google Benchmark seemed to be the most well tested anyways. see // https://github.com/google/benchmark/blob/master/include/benchmark/benchmark.h#L307 template -void doNotOptimizeAway(T const& val) { +void doNotOptimizeAway(const T& val) { // NOLINTNEXTLINE(hicpp-no-assembler) asm volatile("" : : "r,m"(val) : "memory"); } @@ -2797,6 +2967,16 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH #define ZEROERR_G_CONTEXT_SCOPE(x) #endif + +/** + * @brief Default printer for assertion messages + * + * This macro defines the default printer for assertion messages. + * It prints the assertion message in different colors based on the assertion level. + * + * The macro can be overridden by defining ZEROERR_PRINT_ASSERT_DEFAULT_PRINTER before + * including this header. (Or undefine it and implement your own printer.) + */ #ifndef ZEROERR_PRINT_ASSERT_DEFAULT_PRINTER #define ZEROERR_PRINT_ASSERT_DEFAULT_PRINTER(cond, level, ...) \ do { \ @@ -4039,25 +4219,27 @@ class Table : public Card { +#include #include #include #include ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH -#define ZEROERR_CREATE_TEST_FUNC(function, name) \ +#define ZEROERR_CREATE_TEST_FUNC(function, name, ...) \ static void function(zeroerr::TestContext*); \ static zeroerr::detail::regTest ZEROERR_NAMEGEN(_zeroerr_reg)( \ - {name, __FILE__, __LINE__, function}); \ + {name, __FILE__, __LINE__, function, {__VA_ARGS__}}); \ static void function(ZEROERR_UNUSED(zeroerr::TestContext* _ZEROERR_TEST_CONTEXT)) -#define TEST_CASE(name) ZEROERR_CREATE_TEST_FUNC(ZEROERR_NAMEGEN(_zeroerr_testcase), name) +#define TEST_CASE(name, ...) \ + ZEROERR_CREATE_TEST_FUNC(ZEROERR_NAMEGEN(_zeroerr_testcase), name, __VA_ARGS__) -#define SUB_CASE(name) \ - zeroerr::SubCase(name, __FILE__, __LINE__, _ZEROERR_TEST_CONTEXT) \ +#define SUB_CASE(name, ...) \ + zeroerr::SubCase(name, __FILE__, __LINE__, _ZEROERR_TEST_CONTEXT, {__VA_ARGS__}) \ << [=](ZEROERR_UNUSED(zeroerr::TestContext * _ZEROERR_TEST_CONTEXT)) mutable -#define ZEROERR_CREATE_TEST_CLASS(fixture, classname, funcname, name) \ +#define ZEROERR_CREATE_TEST_CLASS(fixture, classname, funcname, name, ...) \ class classname : public fixture { \ public: \ void funcname(zeroerr::TestContext*); \ @@ -4067,12 +4249,12 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH instance.funcname(_ZEROERR_TEST_CONTEXT); \ } \ static zeroerr::detail::regTest ZEROERR_NAMEGEN(_zeroerr_reg)( \ - {name, __FILE__, __LINE__, ZEROERR_CAT(call_, funcname)}); \ + {name, __FILE__, __LINE__, ZEROERR_CAT(call_, funcname), {__VA_ARGS__}}); \ inline void classname::funcname(ZEROERR_UNUSED(zeroerr::TestContext* _ZEROERR_TEST_CONTEXT)) -#define TEST_CASE_FIXTURE(fixture, name) \ +#define TEST_CASE_FIXTURE(fixture, name, ...) \ ZEROERR_CREATE_TEST_CLASS(fixture, ZEROERR_NAMEGEN(_zeroerr_class), \ - ZEROERR_NAMEGEN(_zeroerr_test_method), name) + ZEROERR_NAMEGEN(_zeroerr_test_method), name, __VA_ARGS__) #define ZEROERR_HAVE_SAME_OUTPUT _ZEROERR_TEST_CONTEXT->save_output(); @@ -4088,6 +4270,7 @@ namespace zeroerr { class IReporter; struct TestCase; +class Decorator; /** * @brief TestContext is a class that holds the test results and reporter context. @@ -4103,8 +4286,16 @@ struct TestCase; */ class TestContext { public: - unsigned passed = 0, warning = 0, failed = 0, skipped = 0; - unsigned passed_as = 0, warning_as = 0, failed_as = 0, skipped_as = 0; + unsigned passed = 0; + unsigned warning = 0; + unsigned failed = 0; + unsigned skipped = 0; + unsigned passed_as = 0; + unsigned warning_as = 0; + unsigned failed_as = 0; + unsigned skipped_as = 0; + + std::chrono::duration duration = std::chrono::duration::zero(); IReporter& reporter; @@ -4197,7 +4388,7 @@ struct TestCase { unsigned line; std::function func; std::vector subcases; - + std::vector decorators; /** * @brief Compare the test cases. * @param rhs The test case that will be compared. @@ -4211,8 +4402,8 @@ struct TestCase { * @param file The file that the test case is defined. * @param line The line that the test case is defined. */ - TestCase(std::string name, std::string file, unsigned line) - : name(name), file(file), line(line) {} + TestCase(std::string name, std::string file, unsigned line, std::vector decorators) + : name(name), file(file), line(line), decorators(decorators) {} /** * @brief Construct a new Test Case object @@ -4220,10 +4411,11 @@ struct TestCase { * @param file The file that the test case is defined. * @param line The line that the test case is defined. * @param func The function that will be run to test the test case. + * @param decorators The decorators that will be used to decorate the test case. */ TestCase(std::string name, std::string file, unsigned line, - std::function func) - : name(name), file(file), line(line), func(func) {} + std::function func, std::vector decorators) + : name(name), file(file), line(line), func(func), decorators(decorators) {} }; @@ -4231,7 +4423,8 @@ struct TestCase { * @brief SubCase is a class that holds the subcase information. */ struct SubCase : TestCase { - SubCase(std::string name, std::string file, unsigned line, TestContext* context); + SubCase(std::string name, std::string file, unsigned line, TestContext* context, + std::vector decorators); ~SubCase() = default; TestContext* context; void operator<<(std::function op); @@ -4276,7 +4469,7 @@ class IReporter { * @brief Create the reporter object with the given name. * @param name The name of the reporter. Available reporters are: console, xml. * @param ut The unit test object that will be used to configure the test. - */ + */ static IReporter* create(const std::string& name, UnitTest& ut); IReporter(UnitTest& ut) : ut(ut) {} @@ -4321,7 +4514,7 @@ struct regReporter { * }); * test(a, b); * ``` - * + * * This will test the targetFunc with all the combinations of a and b, e.g. (1,4), (1,5), (1,6), * (2,4), (2,5) ... etc. */ @@ -4369,6 +4562,27 @@ class TestArgs { int index = 0; }; + +class Decorator { +public: + // Called when the test registered, return true can block the test registering + virtual bool onStartup(const TestCase&) { return false; } + + // Called when the test executing, return true can block the test execution + virtual bool onExecution(const TestCase&) { return false; } + + // Called on each assertion, return true can skip the assertion + virtual bool onAssertion() { return false; } + + // Called when the test finished, return true means the test containing errors + virtual bool onFinish(const TestCase&, const TestContext&) { return false; } +}; + +Decorator* skip(bool isSkip = true); +Decorator* timeout(float timeout = 0.1f); // in seconds +Decorator* may_fail(bool isMayFail = true); +Decorator* should_fail(bool isShouldFail = true); + } // namespace zeroerr ZEROERR_SUPPRESS_COMMON_WARNINGS_POP @@ -4394,13 +4608,14 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_POP ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH -#define ZEROERR_CREATE_FUZZ_TEST_FUNC(function, name) \ - static void function(zeroerr::TestContext*); \ - static zeroerr::detail::regTest ZEROERR_NAMEGEN(_zeroerr_reg)( \ - {name, __FILE__, __LINE__, function}, zeroerr::TestType::fuzz_test); \ +#define ZEROERR_CREATE_FUZZ_TEST_FUNC(function, name, ...) \ + static void function(zeroerr::TestContext*); \ + static zeroerr::detail::regTest ZEROERR_NAMEGEN(_zeroerr_reg)( \ + {name, __FILE__, __LINE__, function, {__VA_ARGS__}}, zeroerr::TestType::fuzz_test); \ static void function(ZEROERR_UNUSED(zeroerr::TestContext* _ZEROERR_TEST_CONTEXT)) -#define FUZZ_TEST_CASE(name) ZEROERR_CREATE_FUZZ_TEST_FUNC(ZEROERR_NAMEGEN(_zeroerr_testcase), name) +#define FUZZ_TEST_CASE(name, ...) \ + ZEROERR_CREATE_FUZZ_TEST_FUNC(ZEROERR_NAMEGEN(_zeroerr_testcase), name, __VA_ARGS__) #define FUZZ_FUNC(func) zeroerr::FuzzFunction(func, _ZEROERR_TEST_CONTEXT) @@ -4917,7 +5132,7 @@ LogInfo::LogInfo(const char* filename, const char* function, const char* message const char* q = p + 1; while (*q && *q != '}') q++; if (*q == '}') { - std::string N(p + 1, (size_t)(q-p-1)); + std::string N(p + 1, (size_t)(q - p - 1)); names[N] = static_cast(names.size()); p = q; } @@ -5040,10 +5255,15 @@ void* LogStream::getRawLog(std::string func, unsigned line, std::string name) { return nullptr; } +static bool startWith(const std::string& str, const std::string& prefix) { + return str.rfind(prefix, 0) == 0; +} + void* LogStream::getRawLog(std::string func, std::string msg, std::string name) { for (DataBlock* p = first; p; p = p->next) for (auto q = p->begin(); q < p->end(); q = moveBytes(q, q->info->size)) - if (msg == q->info->message && func == q->info->function) return q->getRawLog(name); + if (startWith(q->info->message, msg) && func == q->info->function) + return q->getRawLog(name); return nullptr; } @@ -5085,7 +5305,7 @@ LogIterator& LogIterator::operator++() { } bool LogIterator::check_filter() { - if (!message_filter.empty() && q->info->message != message_filter) return false; + if (!message_filter.empty() && startWith(q->info->message, message_filter)) return false; if (!function_name_filter.empty() && q->info->function != function_name_filter) return false; if (line_filter != -1 && static_cast(q->info->line) != line_filter) return false; return true; @@ -5771,8 +5991,9 @@ static inline std::string getFileName(std::string file) { return fileName; } -SubCase::SubCase(std::string name, std::string file, unsigned line, TestContext* context) - : TestCase(name, file, line), context(context) {} +SubCase::SubCase(std::string name, std::string file, unsigned line, TestContext* context, + std::vector decorators) + : TestCase(name, file, line, decorators), context(context) {} void SubCase::operator<<(std::function op) { func = op; @@ -5931,6 +6152,20 @@ bool UnitTest::run_filter(const TestCase& tc) { return true; } +static bool runOnExecution(const TestCase& tc) { + for (auto& decorator : tc.decorators) { + if (decorator->onExecution(tc)) return true; + } + return false; +} + +static bool runOnFinish(const TestCase& tc, const TestContext& ctx) { + for (auto& decorator : tc.decorators) { + if (decorator->onFinish(tc, ctx)) return true; + } + return false; +} + int UnitTest::run() { IReporter* reporter = IReporter::create(reporter_name, *this); if (!reporter) reporter = IReporter::create("console", *this); @@ -5946,11 +6181,16 @@ int UnitTest::run() { for (auto& tc : test_cases) { if (!run_filter(tc)) continue; + if (runOnExecution(tc)) { + sum.skipped += 1; + continue; + } reporter->testCaseStart(tc, new_buf); if (!list_test_cases) { std::streambuf* orig_buf = std::cerr.rdbuf(); std::cerr.rdbuf(&new_buf); std::cerr << std::endl; + auto start = std::chrono::high_resolution_clock::now(); try { tc.func(&context); // run the test case } catch (const AssertionData&) { @@ -5961,9 +6201,17 @@ int UnitTest::run() { context.failed_as = 1; } } + auto end = std::chrono::high_resolution_clock::now(); + context.duration = end - start; std::cerr.rdbuf(orig_buf); } int type = sum.add(context); + if (runOnFinish(tc, context)) { + if (type != 2) { + sum.failed += 1; + type = 2; + } + } reporter->testCaseEnd(tc, new_buf, context, type); context.reset(); new_buf.str(""); @@ -6001,7 +6249,12 @@ static std::set getRegisteredTests(unsigned type) { return result; } -regTest::regTest(const TestCase& tc, TestType type) { getTestSet(type).insert(tc); } +regTest::regTest(const TestCase& tc, TestType type) { + for (auto& decorator : tc.decorators) { + if (decorator->onStartup(tc)) return; + } + getTestSet(type).insert(tc); +} static std::set& getRegisteredReporters() { static std::set data; @@ -6287,8 +6540,8 @@ XmlWriter::ScopedElement::~ScopedElement() { if (m_writer) m_writer->endElement(); } -XmlWriter::ScopedElement& XmlWriter::ScopedElement::writeText(const std::string& text, - bool indent, bool new_line) { +XmlWriter::ScopedElement& XmlWriter::ScopedElement::writeText(const std::string& text, bool indent, + bool new_line) { m_writer->writeText(text, indent, new_line); return *this; } @@ -6322,8 +6575,10 @@ XmlWriter& XmlWriter::endElement() { m_os << "/>"; m_tagIsOpen = false; } else { - if (m_needsIndent) m_os << m_indent; - else m_needsIndent = true; + if (m_needsIndent) + m_os << m_indent; + else + m_needsIndent = true; m_os << ""; } m_os << std::endl; @@ -6355,7 +6610,7 @@ XmlWriter& XmlWriter::writeText(const std::string& text, bool indent, bool new_l if (tagWasOpen && indent) m_os << m_indent; m_os << XmlEncode(text); m_needsNewline = new_line; - m_needsIndent = new_line; + m_needsIndent = new_line; } return *this; } @@ -6413,8 +6668,8 @@ class XmlReporter : public IReporter { if (ut.log_to_report) suspendLog(); } - virtual void testCaseEnd(const TestCase& tc, std::stringbuf& sb, const TestContext& ctx, - int) override { + virtual void testCaseEnd(ZEROERR_UNUSED(const TestCase&), std::stringbuf& sb, + const TestContext& ctx, int) override { current.pop_back(); xml.scopedElement("Result") .writeAttribute("time", 0) @@ -6482,6 +6737,63 @@ IReporter* IReporter::create(const std::string& name, UnitTest& ut) { } +class SkipDecorator : public Decorator { + bool onExecution(const TestCase&) override { return true; } +}; + +Decorator* skip(bool isSkip) { + static SkipDecorator skip_dec; + if (isSkip) return &skip_dec; + return nullptr; +} + +class TimeoutDecorator : public Decorator { + float timeout; + +public: + TimeoutDecorator() : timeout(0) {} + TimeoutDecorator(float timeout) : timeout(timeout) {} + + bool onFinish(const TestCase& tc, const TestContext& ctx) override { + if (ctx.duration > std::chrono::duration(timeout)) { + std::cerr << FgRed << "Timeout: " << Reset << ctx.duration.count() << "s > " << timeout << "s" << std::endl; + return true; + } + return false; + } +}; + +Decorator* timeout(float timeout) { + static std::map timeout_dec; + if (timeout_dec.find(timeout) == timeout_dec.end()) { + timeout_dec[timeout] = TimeoutDecorator(timeout); + } + return &timeout_dec[timeout]; +} + +class FailureDecorator : public Decorator { +public: + enum FailureType { may_fail, should_fail }; + FailureDecorator(FailureType type) : type(type) {} + +private: + FailureType type; +}; + + +Decorator* may_fail(bool isMayFail) { + static FailureDecorator may_fail_dec(FailureDecorator::may_fail); + if (isMayFail) return &may_fail_dec; + return nullptr; +} + +Decorator* should_fail(bool isShouldFail) { + static FailureDecorator should_fail_dec(FailureDecorator::should_fail); + if (isShouldFail) return &should_fail_dec; + return nullptr; +} + + } // namespace zeroerr @@ -6550,6 +6862,13 @@ void RunFuzzTest(IFuzzTest& fuzz_test, int seed, int runs, int max_len, int time }); current_fuzz_test = nullptr; +#else + (void) fuzz_test; + (void) seed; + (void) runs; + (void) max_len; + (void) timeout; + (void) len_control; #endif } @@ -6557,6 +6876,164 @@ void RunFuzzTest(IFuzzTest& fuzz_test, int seed, int runs, int max_len, int time +#include +namespace zeroerr { + +IRObject* IRObject::alloc(size_t size) { + IRObject* list = new IRObject[size + 1]; + list[0].i = size; + return list + 1; +} + +char* IRObject::alloc_str(size_t size) { + char* s = new char[size + 1]; + s[size] = 0; + return s; +} + +static std::string escape(std::string str) { + std::string result; + for (char c : str) { + switch (c) { + case ' ': result += "\\s"; break; + case '\n': result += "\\n"; break; + case '\t': result += "\\t"; break; + case '\r': result += "\\r"; break; + case '\f': result += "\\f"; break; + case '\v': result += "\\v"; break; + case '\\': result += "\\\\"; break; + case '"': result += "\\\""; break; + default: { + if (c < 32 || c > 126) { + result += "\\x"; + result += "0123456789abcdef"[c >> 4]; + result += "0123456789abcdef"[c & 15]; + continue; + } + result += c; + } + } + } + return result; +} + +static std::string unescape(std::string str) { + std::string result; + for (size_t i = 0; i < str.size(); ++i) { + if (str[i] == '\\') { + switch (str[++i]) { + case 's': result += ' '; break; + case 'n': result += '\n'; break; + case 't': result += '\t'; break; + case 'r': result += '\r'; break; + case 'f': result += '\f'; break; + case 'v': result += '\v'; break; + case '\\': result += '\\'; break; + case '"': result += '"'; break; + case 'x': { + char c = 0; + for (int j = 0; j < 2; ++j) { + c *= 16; + if (str[i + 1] >= '0' && str[i + 1] <= '9') { + c += str[i + 1] - '0'; + } else if (str[i + 1] >= 'a' && str[i + 1] <= 'f') { + c += str[i + 1] - 'a' + 10; + } else if (str[i + 1] >= 'A' && str[i + 1] <= 'F') { + c += str[i + 1] - 'A' + 10; + } + } + result += c; + i += 2; + } + } + } else { + result += str[i]; + } + } + return result; +} + +static void to_string(IRObject obj, std::stringstream& ss) { + switch (obj.type) { + case IRObject::Type::Int: ss << obj.i; break; + case IRObject::Type::Float: ss << obj.f << 'f'; break; + case IRObject::Type::String: ss << '"' << escape(obj.s) << '"'; break; + case IRObject::Type::ShortString: ss << '"' << escape(obj.ss) << '"'; break; + case IRObject::Type::Object: + ss << "{ "; + auto c = obj.GetChildren(); + for (unsigned i = 0; i < c.size; ++i) { + to_string(*(c.children + i), ss); + ss << " "; + } + ss << "}"; + break; + } +} + +static IRObject from_string(std::stringstream& ss, std::string& token) { + IRObject obj; + if (token == "{") { + std::vector children; + while (ss >> token) { + if (token == "}") break; + IRObject child = from_string(ss, token); + if (child.type == IRObject::Type::Undefined) + return obj; + children.push_back(child); + } + IRObject* child = IRObject::alloc(children.size()); + for (unsigned i = 0; i < children.size(); ++i) { + child[i] = children[i]; + } + obj.SetChildren(child); + return obj; + } + if (token[0] == '"') { + CHECK(token.size() > 1 AND token.back() == '"'); + obj.SetScalar(unescape(token.substr(1, token.size() - 2))); + return obj; + } + if (token.back() == 'f') { + obj.SetScalar(std::stod(token.substr(0, token.size() - 1))); + return obj; + } + if (token[0] == '-' || (token[0] >= '0' && token[0] <= '9')) { + obj.SetScalar(std::stoll(token)); + return obj; + } + return obj; +} + +std::string IRObject::ToString(IRObject obj) { + std::stringstream ss; + to_string(obj, ss); + return ss.str(); +} + +IRObject IRObject::FromString(std::string str) { + std::stringstream ss(str); + std::string token; + ss >> token; + return from_string(ss, token); +} + +std::vector IRObject::ToBinary(IRObject obj) { + std::vector bin; + return bin; +} + +IRObject IRObject::FromBinary(std::vector bin) { + IRObject obj; + return obj; +} + + +} // namespace zeroerr + + + + #include #include