diff --git a/.clang-format b/.clang-format index 422c0bda..2d88602c 100755 --- a/.clang-format +++ b/.clang-format @@ -41,4 +41,6 @@ PointerAlignment: Left MaxEmptyLinesToKeep: 2 SortIncludes: true -SortUsingDeclarations: true \ No newline at end of file +SortUsingDeclarations: true + +QualifierAlignment: Left \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 643a8877..cd9c9554 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,6 +12,14 @@ "args": [], "cwd": "${workspaceFolder}" }, + { + "type": "lldb", + "request": "launch", + "name": "Windows reporter", + "program": "${workspaceFolder}/build/windows/test/Debug/unittest.exe", + "args": ["--no-color", "--log-to-report", "--reporters=xml"], + "cwd": "${workspaceFolder}" + }, { "type": "lldb", "request": "launch", @@ -20,6 +28,14 @@ "args": [], "cwd": "${workspaceFolder}" }, + { + "type": "lldb", + "request": "launch", + "name": "Linux reporter", + "program": "${workspaceFolder}/build/linux/test/unittest", + "args": ["--no-color", "--log-to-report", "--reporters=xml"], + "cwd": "${workspaceFolder}" + }, { "type": "lldb", "request": "launch", @@ -36,5 +52,14 @@ "args": ["-f", "--testcase=fuzz_test.*"], "cwd": "${workspaceFolder}" } + , + { + "type": "lldb", + "request": "launch", + "name": "Linux-speed", + "program": "${workspaceFolder}/build/linux/test/unittest", + "args": ["-b", "--testcase=speedtest"], + "cwd": "${workspaceFolder}" + } ] } \ No newline at end of file diff --git a/Makefile b/Makefile index e1de4833..38ad9a0f 100644 --- a/Makefile +++ b/Makefile @@ -30,9 +30,25 @@ fuzz: linux linux-test: linux cd build/linux/test && ./unittest +reporter: linux + cd build/linux/test && ./unittest --no-color --log-to-report --reporters=xml + windows-test: windows cd build/windows/test && ./Debug/unittest.exe + +build/linux-release/Makefile: Makefile + mkdir -p build/linux-release + cmake -B build/linux-release -S . -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_STANDARD=11 \ + -DBUILD_EXAMPLES=ON -DBUILD_TEST=ON -DUSE_MOLD=ON -DENABLE_FUZZING=ON \ + -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang + +linux-release: build/linux-release/Makefile + cmake --build build/linux-release -j `nproc` + +bench: linux-release + cd build/linux-release/test && ./unittest -b --testcase=speedtest + doc-dev: yarn run cmake:dev diff --git a/Readme.en.md b/Readme.en.md deleted file mode 100644 index 9b1ec386..00000000 --- a/Readme.en.md +++ /dev/null @@ -1,248 +0,0 @@ -# ZeroErr - -[![Standard](https://img.shields.io/badge/C%2B%2B%2FCUDA-11%2F14%2F17%2F20-blue)](https://en.wikipedia.org/wiki/C%2B%2B#Standardization) [![download](https://img.shields.io/badge/-Download-brightgreen)](https://raw.githubusercontent.com/sunxfancy/zeroerr/master/zeroerr.hpp) [![Chinese-Readme](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-Readme-blue)](./Readme.md) - - -Hope you get 0 errors and 0 warnings everyday! - -![](./docs/fig/zeroerr.jpg) - - -ZeroErr is a smart assertion library, a lightweight unit testing framework and a structure logging framework. It integrates those features and provided an unite and clear interface for separated usage or combined usage. - -[English Documentation](https://sunxfancy.github.io/zeroerr/en/) | [项目文档](https://sunxfancy.github.io/zeroerr/zh/) - -Note: The project is currently in the experimental stage, and the API may change significantly. It is not recommended to use it in a production environment. - - -## Why we need another unit testing framework - -The current popular unit testing frameworks, e.g. Catch2, doctest, Boost.Test and cpputest are mature and well-established which covers common cases during development. The logger libraries like glog and spdlog are also easy to use. However, there are still some issues: - -### 1. Generic Printing - -Most unit testing frameworks and logger libraries can not provide a generic printing for user customized types. Especially, when using containers, structures and pointers (including smart pointers), user have to manually write code to generate the log message or print those information during unit testing failed cases. - -This library `zeroerr` gives you an ability to print generically for all types: - -```c++ -TEST_CASE("Try logging") { - std::vector data = {1, 2, 3}; - LOG_IF(1 == 1, "data = {data}", data); -} -``` - -Similar to other C++ unit testing frameworks, `zeroerr` will convert this piece of code into a function and register it to automatically run once you link the main function and the library. Here, we can log the data in `vector` template directly without writing any code. - -![case1](docs/fig/case1.png) - -For the custom struct type with override `std::ostream& operator<<(std::ostream&, Type)` stream output, you can use it not only for this type but also all contains using this type, including multiple recursive contains: - -```c++ -struct Node { - std::string name; - int id; -}; - -std::ostream& operator<<(std::ostream& out, Node n) { - out << n.id << '.' << n.name; - return out; -} - -TEST_CASE("Try logging with custom type") { - std::map> data = { - {"user1", {{"a",1}, {"b",2}}}, {"user2", {{"c",3}, {"d",4}}} - }; - LOG("data = {data}", data); -} -``` - -![case2](docs/fig/case2.png) - -Of cause, in many cases, some third-party libraries may not use `<<` operators. For those cases, we can write own rules to create a generic way for printing. For example, LLVM `llvm::Function*` type can not be streamed into std::ostream, we can write code to handle it. However, it will be more simple if we can write a rule for all the sub-classes of `llvm::Value` and `llvm::Type` since we can call the `print` method to print the output. Here we use a `dbg` marco defined in `zeroerr` to quickly print any type. This is very similar to the `dbg` marco in rust. - - -```c++ -namespace zeroerr { // must defined in namespace zeroerr - -template -typename std::enable_if< - std::is_base_of::value || std::is_base_of::value, void>::type -PrinterExt(Printer& P, T* s, unsigned level, const char* lb, rank<2>) { - if (s == nullptr) { - P.os << P.tab(level) << "nullptr" << lb; - } else { - llvm::raw_os_ostream os(P.os); - s->print(os); - } -} -} - -TEST_CASE("customize printing of LLVM pointers") { - llvm::LLVMContext context; - std::vector args = {llvm::Type::getInt32Ty(context)}; - llvm::Module* module = new llvm::Module("test_module", context); - - auto* f = - llvm::Function::Create(llvm::FunctionType::get(llvm::Type::getVoidTy(context), args, false), - llvm::GlobalValue::ExternalLinkage, "test", module); - dbg(dbg(f)->getType()); -} -``` - -This functin `PrintExt` will match all the class who's base class is `Value` and `Type`. Then, it will create a stream ``llvm::raw_os_ostream` for output. - -![case3-llvm](./docs/fig/case3.png) - -### 2. Combined usage of assert, log and unit testing - -If you use one logging framework, an unit testing framework and an assertion library, it's not a easy work to combine them together. There is a lot of benefits to use assertion, logging and unit testing together. In `zeroerr`, if an assertion is failed, the logger will receive an event and stored the event in your log file. If you are using an assertion in unit testing, the assertion failure, logged fatal events can be recorded and reported. - -```c++ -int fib(int n) { - REQUIRE(n >= 0, "n must be non-negative"); - REQUIRE(n < 20, "n must be less than 20"); - if (n <= 2) { - return 1; - } - return fib(n - 1) + fib(n - 2); -} - -TEST_CASE("fib function test") { - CHECK(fib(0) == 0); - CHECK(fib(1) == 1); - CHECK(fib(2) == 1); - CHECK(fib(3) == 2); - CHECK(fib(4) == 3); - CHECK(fib(5) == 5); - CHECK(fib(20) == 6765); -} -``` - -![joint1](docs/fig/joint1.png) - -For the logging system, the unit testing can access the log data to ensure that the function has executed the expected logic and results. - -```c++ -118 static void function() { -119 int k = system_call(); -120 LOG_IF(k != 0, "System call failed, error code = {k}", k); -121 } -... - -TEST_CASE("access log in Test case") { - zeroerr::suspendLog(); - function(); - CHECK(LOG_GET(function, 120, k, int) == ERROR_CODE); - zeroerr::resumeLog(); -} -``` - -In order to access the log, we need to pause the log system first, to avoid the data being output to the file, then call the function, access the data in the log through the `LOG_GET` macro, and finally resume the log system. (Currently experimental, only the first call of each log point can be accessed) - - -Further more, the unit testing can check the logged result if it matches the previous running result (a golden file) to avoid writing any code in the test case. - -```c++ -TEST_CASE("match ostream") { - // match output can be done in the following workflow - // 1. user mark the test case which are comparing output use 'ZEROERR_HAVE_SAME_OUTPUT' - // 2. If the output is not exist, the result will be store to the disk. - // 3. If the output is exist, compare with it and report error if output is not match. - std::cerr << "a = 100" << std::endl; - - ZEROERR_HAVE_SAME_OUTPUT; -} -``` - -Once you set `ZEROERR_HAVE_SAME_OUTPUT` marco, the system will check the output stream and save the first run result into a file. Then, the next run will compare the result to see if it the same. (Currently experimental) - - -## 3. Fuzzing Support - -Most Unit Testing frameworks do not support fuzzing. However, it's a powerful feature to automatically detect faults in the software and can greatly reduce the work to write test cases. - -Different than other fuzzing framework, `zeroerr` can also support logging and assertion in the code, so the fuzzing result not only contains corpus but also with the logging and assertion information. - -Here is an example of using `zeroerr` to do structured fuzzing: - -```c++ -FUZZ_TEST_CASE("fuzz_test") { - LOG("Run fuzz_test"); - FUZZ_FUNC([=](int k, std::string num) { - int t = atoi(num.c_str()); - LOG("k: {k}, num:{num}, t: {t}", k, num, t); - REQUIRE(k == t); - }) - .WithDomains(InRange(0, 10), Arbitrary()) - .WithSeeds({{5, "Foo"}, {10, "Bar"}}) - .Run(10); -} -``` - -Inspired by [fuzztest](https://github.com/google/fuzztest), Domain is a concept to specify the input data range (or patterns) for the target function. Here, we use `InRange` to specify the range of `k` is 0 to 10, and `Arbitrary` to specify the data of `num` can be any random string. Then, we use `WithSeeds` to specify the initial seeds for the fuzzing. - -The macro `FUZZ_TEST_CASE` will generate a test case which can connect with `libFuzzer` to run the fuzzing. Finally, we use `Run(10)` to call `libFuzzer` to run the target for 10 times. - -To build the test case with fuzzing, you need to use `clang++` to compile the code and with `-fsanitize=fuzzer-no-link` and link the `-lclang_rt.fuzzer_no_main-x86_64` which is a version of libFuzzer without main function. You can find this runtime library by calling `clang++ -print-runtime-dir`. Here is the complete command to build the test case with fuzzing support: - -```bash -clang++ -std=c++11 -fsanitize=fuzzer-no-link -L=`clang++ -print-runtime-dir` -lclang_rt.fuzzer_no_main-x86_64 -o test_fuzz test_fuzz.cpp -``` - - -## 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. - -2. Optional thread safety -You can choose to build with/without thread safety. - -3. Fastest log -Multiple level of log writing policies. You can choose to only write to disk with the most important events. - -4. Customized print / log / assert printing format -You can customize your printing format for everything. There is a templated callback function for the printing. - -5. Quickly debug something -You can use dbg macro to quickly see the output, it can be applied to any expression. - -6. Colorful output -You can have default colorful output to terminal and no color for file output - -7. Print struct/stl/pointers without any extra code - -8. Doctest like assertion and unit test feature -You can use your unit test as a documentation of function behavior. The output of unittest can be a documented report. - -9. Lazy logging for assertion -After assertion failed, the logging result will print automatically even if you didn't redirect to your error stream - -10. Logging Category -Logging information can have customized category and only display one category based on your assertion or configuration - -11. Logging for Unit Testing -You can use a correct logging result as your unit testing golden file. So you just need to manually verify your log once and save it. The unit testing framework will use the golden file to verify your unit testing result. - -12. Structured Logging -We can support output structured information directly into plain text or lisp format (json, logfmt, or other custom format should be the next step to support) - -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 - -Thanks to the `tiv` tool: -https://github.com/stefanhaustein/TerminalImageViewer \ No newline at end of file diff --git a/Readme.md b/Readme.md index 70cc337d..584ea700 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,7 @@ -# ZeroErr 零误框架 +# ZeroErr -[![Standard](https://img.shields.io/badge/C%2B%2B-11%2F14%2F17%2F20-blue)](https://en.wikipedia.org/wiki/C%2B%2B#Standardization) [![download](https://img.shields.io/badge/-Download-brightgreen)](https://raw.githubusercontent.com/sunxfancy/zeroerr/master/zeroerr.hpp) [![Eng-Readme](https://img.shields.io/badge/English-Readme-blue)](./Readme.en.md) +[![Standard](https://img.shields.io/badge/C%2B%2B%2FCUDA-11%2F14%2F17%2F20-blue)](https://en.wikipedia.org/wiki/C%2B%2B#Standardization) [![download](https://img.shields.io/badge/-Download-brightgreen)](https://raw.githubusercontent.com/sunxfancy/zeroerr/master/zeroerr.hpp) [![Chinese-Readme](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-Readme-blue)](./Readme.zh.md) +[![TryItOnline](https://img.shields.io/badge/TryItOnline-purple)](https://replit.com/@sunxfancy/ZeroErr-Demo#main.cpp) Hope you get 0 errors and 0 warnings everyday! @@ -8,20 +9,22 @@ Hope you get 0 errors and 0 warnings everyday! ![](./docs/fig/zeroerr.jpg) -ZeroErr 零误框架是一款轻量级C++单元测试框架,同时也集成了断言库,日志库,打印调试等诸多功能,实现了以上功能的整合与协同工作。既可以选择整体使用,也可以单独使用其中的部分功能。 +ZeroErr is a smart assertion library, a lightweight unit testing framework and a structure logging framework. It integrates those features and provided an unite and clear interface for separated usage or combined usage. -[项目文档](https://sunxfancy.github.io/zeroerr/zh/) | [English Documentation](https://sunxfancy.github.io/zeroerr/en/) +[English Documentation](https://sunxfancy.github.io/zeroerr/en/) | [项目文档](https://sunxfancy.github.io/zeroerr/zh/) -注:目前项目处于实验阶段,API可能会有较大变动,不建议在生产环境中使用。 +Note: The project is currently in the experimental stage, and the API may change significantly. It is not recommended to use it in a production environment. -## 为何要开发一款新的测试框架 -目前业界主流的测试框架,Catch2, doctest, Boost.Test, cpputest 功能完善成熟,基本满足大多数应用场景。glog,spdlog等日志库使用也很简便。但这其中还存在一些问题: +## Why we need another unit testing framework +The current popular unit testing frameworks, e.g. Catch2, doctest, Boost.Test and cpputest are mature and well-established which covers common cases during development. The logger libraries like glog and spdlog are also easy to use. However, there are still some issues: -### 1. 泛型打印支持 +### 1. Generic Printing -以上主流框架对任意类型数据的打印支持不够,大多数测试框架,断言库,日志库,大多缺乏泛型打印支持,对于容器和用户自定义类型缺乏直接打印的能力,ZeroErr解决了这一问题。 +Most unit testing frameworks and logger libraries can not provide a generic printing for user customized types. Especially, when using containers, structures and pointers (including smart pointers), user have to manually write code to generate the log message or print those information during unit testing failed cases. + +This library `zeroerr` gives you an ability to print generically for all types: ```c++ TEST_CASE("Try logging") { @@ -30,13 +33,11 @@ TEST_CASE("Try logging") { } ``` -类似于其他C++单元测试框架,ZeroErr可以将这段宏注册的单元测试代码编译成自动运行的函数,执行后结果如下,这里我们无需定义任何规则,就可以使用LOG宏打印`vector`模板: - +Similar to other C++ unit testing frameworks, `zeroerr` will convert this piece of code into a function and register it to automatically run once you link the main function and the library. Here, we can log the data in `vector` template directly without writing any code. ![case1](docs/fig/case1.png) - -对于带有 `std::ostream& operator<<(std::ostream&, Type)` 流输出重载的自定义类型,可以不加修改直接打印。并且还支持容器类型的嵌套。 +For the custom struct type with override `std::ostream& operator<<(std::ostream&, Type)` stream output, you can use it not only for this type but also all contains using this type, including multiple recursive contains: ```c++ struct Node { @@ -59,8 +60,8 @@ TEST_CASE("Try logging with custom type") { ![case2](docs/fig/case2.png) +Of cause, in many cases, some third-party libraries may not use `<<` operators. For those cases, we can write own rules to create a generic way for printing. For example, LLVM `llvm::Function*` type can not be streamed into std::ostream, we can write code to handle it. However, it will be more simple if we can write a rule for all the sub-classes of `llvm::Value` and `llvm::Type` since we can call the `print` method to print the output. Here we use a `dbg` marco defined in `zeroerr` to quickly print any type. This is very similar to the `dbg` marco in rust. -当然,很多情况下,第三方库并没有重载我们预期的`<<`操作符。对于较复杂的情况,我们可以编写泛型打印函数来处理,这大大增强了系统对不同接口的处置能力。例如,我们对这个LLVM的 `llvm::Function*` 类型,可以使用如下方式用`dbg`函数打印,这里`dbg`类似于rust的`dbg`宏,用来快速打印检查任意类型,并且可以嵌套使用: ```c++ namespace zeroerr { // must defined in namespace zeroerr @@ -90,13 +91,13 @@ TEST_CASE("customize printing of LLVM pointers") { } ``` -这个泛型函数会匹配所有基类为`Value`和`Type`的llvm类,然后打印时创建一个`llvm::raw_os_ostream`输出流,并对其进行调用`print`方法打印。 +This functin `PrintExt` will match all the class who's base class is `Value` and `Type`. Then, it will create a stream ``llvm::raw_os_ostream` for output. ![case3-llvm](./docs/fig/case3.png) -### 2. 断言、日志、单元测试的联合使用 +### 2. Combined usage of assert, log and unit testing -对于使用多个不同的库实现上述功能,断言、日志、单元测试的各种功能无法协同使用。而在ZeroErr中,断言出错时,可以被日志系统捕获,可以输出到文件中保存,断言在单元测试中,可以被报告系统记录,并在最终输出中统计共有哪些断言失败。上述功能可以联合使用,也可以单独使用某一项,用法非常灵活。 +If you use one logging framework, an unit testing framework and an assertion library, it's not a easy work to combine them together. There is a lot of benefits to use assertion, logging and unit testing together. In `zeroerr`, if an assertion is failed, the logger will receive an event and stored the event in your log file. If you are using an assertion in unit testing, the assertion failure, logged fatal events can be recorded and reported. ```c++ int fib(int n) { @@ -121,53 +122,50 @@ TEST_CASE("fib function test") { ![joint1](docs/fig/joint1.png) +For the logging system, the unit testing can access the log data to ensure that the function has executed the expected logic and results. + +```c++ +118 static void function() { +119 int k = system_call(); +120 LOG_IF(k != 0, "System call failed, error code = {k}", k); +121 } +... + +TEST_CASE("access log in Test case") { + zeroerr::suspendLog(); + function(); + CHECK(LOG_GET(function, 120, k, int) == ERROR_CODE); + zeroerr::resumeLog(); +} +``` + +In order to access the log, we need to pause the log system first, to avoid the data being output to the file, then call the function, access the data in the log through the `LOG_GET` macro, and finally resume the log system. (Currently experimental, only the first call of each log point can be accessed) -更进一步,单元测试甚至可以通过比较log结果是否与之前正确的结果相同,从而避免很多复杂的单元测试编写,粗略检查代码的正确性。 +Further more, the unit testing can check the logged result if it matches the previous running result (a golden file) to avoid writing any code in the test case. ```c++ TEST_CASE("match ostream") { // match output can be done in the following workflow - // 1. user mark the test case which are comparing output use 'have_same_output' - // 2. If the output is not exist, the result has been used as a correct verifier. + // 1. user mark the test case which are comparing output use 'ZEROERR_HAVE_SAME_OUTPUT' + // 2. If the output is not exist, the result will be store to the disk. // 3. If the output is exist, compare with it and report error if output is not match. std::cerr << "a = 100" << std::endl; ZEROERR_HAVE_SAME_OUTPUT; } ``` -通过设置 `ZEROERR_HAVE_SAME_OUTPUT` 宏,系统会自动检查该测试点的output stream输出,第一次执行时的结果会自动保存起来,而之后每次执行,都会将输出与第一次输出进行对比,相同则正确,否则该点错误。用户可以第一次手动观察输出是否符合预期,若是修改了实现后,想清除保存的结果,只需要将测试目录下的 `output.txt` 缓存文件删除即可。(目前仍是实验功能) +Once you set `ZEROERR_HAVE_SAME_OUTPUT` marco, the system will check the output stream and save the first run result into a file. Then, the next run will compare the result to see if it the same. (Currently experimental) -最后,对于日志系统,单元测试不但能够访问日志数据,以确保函数按照预期逻辑执行出来了结果。 -还可以在逻辑出错时,自动捕获函数中的断言和相关打印信息,以便于后续的调试。 -```c++ -118 static void function() { -119 LOG("function log {i}", 1); -120 LOG("function log {sum}, {i}", 10, 1); -121 } -... +## 3. Fuzzing Support -TEST_CASE("access log in Test case") { - zeroerr::suspendLog(); - function(); - CHECK(LOG_GET(function, 119, i, int) == 1); - CHECK(LOG_GET(function, 120, sum, int) == 10); - CHECK(LOG_GET(function, 120, i, int) == 1); - zeroerr::resumeLog(); -} -``` - -为了访问log,我们首先要暂停log系统,避免数据被输出到文件中,然后调用函数,通过`LOG_GET`宏访问log中的数据,最后再恢复log系统的运行。(目前,暂时仅能获取到每个Log点第一次调用的数据,仍是实验功能)。 - -## 3. Fuzzing的支持 +Most Unit Testing frameworks do not support fuzzing. However, it's a powerful feature to automatically detect faults in the software and can greatly reduce the work to write test cases. -大多数单元测试框架不支持fuzzing。然而,Fuzzing功能强大,可以自动检测软件中的错误,并且可以大大减少编写测试用例的工作量。 +Different than other fuzzing framework, `zeroerr` can also support logging and assertion in the code, so the fuzzing result not only contains corpus but also with the logging and assertion information. -不同于其他fuzzing框架,`zeroerr`可以支持在代码中使用日志和断言,因此fuzzing的结果不仅包含了输入数据,还包含了日志和断言的信息。 - -使用方法: +Here is an example of using `zeroerr` to do structured fuzzing: ```c++ FUZZ_TEST_CASE("fuzz_test") { @@ -183,36 +181,69 @@ FUZZ_TEST_CASE("fuzz_test") { } ``` -受到 [fuzztest](https://github.com/google/fuzztest)的启发,我们使用Domain这个概念,用于指定目标函数的输入数据范围(或模式)。在这里,我们使用 `InRange` 来指定 `k` 的范围是0到10,使用 `Arbitrary` 来指定 `num` 的数据可以是任意随机字符串。然后,我们使用 `WithSeeds` 来指定fuzzing的初始种子。最后,我们使用 `Run` 来指定fuzzing的次数。 +Inspired by [fuzztest](https://github.com/google/fuzztest), Domain is a concept to specify the input data range (or patterns) for the target function. Here, we use `InRange` to specify the range of `k` is 0 to 10, and `Arbitrary` to specify the data of `num` can be any random string. Then, we use `WithSeeds` to specify the initial seeds for the fuzzing. -宏 `FUZZ_TEST_CASE` 会生成一个测试用例,可以连接到 `libFuzzer` 来运行fuzzing。最后,我们使用 `Run(10)` 来调用 `libFuzzer` 来运行目标10次。 +The macro `FUZZ_TEST_CASE` will generate a test case which can connect with `libFuzzer` to run the fuzzing. Finally, we use `Run(10)` to call `libFuzzer` to run the target for 10 times. -为了构建带有fuzzing的测试用例,您需要使用 `clang++` 编译代码,并使用 `-fsanitize=fuzzer-no-link` 并链接 `-lclang_rt.fuzzer_no_main-x86_64`,这是一个没有main函数的libFuzzer版本。您可以通过调用 `clang++ -print-runtime-dir` 来找到这个运行时库。以下是带有fuzzing支持的测试用例的完整构建命令: +To build the test case with fuzzing, you need to use `clang++` to compile the code and with `-fsanitize=fuzzer-no-link` and link the `-lclang_rt.fuzzer_no_main-x86_64` which is a version of libFuzzer without main function. You can find this runtime library by calling `clang++ -print-runtime-dir`. Here is the complete command to build the test case with fuzzing support: ```bash clang++ -std=c++11 -fsanitize=fuzzer-no-link -L=`clang++ -print-runtime-dir` -lclang_rt.fuzzer_no_main-x86_64 -o test_fuzz test_fuzz.cpp ``` -## 项目构建 +## Other Good Features -本项目使用CMake构建,您可以直接将整个目录引入为子项目,也可以选择下载我们提前打包好的整合文件 `zeroerr.hpp`。项目的构建可以在项目根目录下使用如下指令: -```sh -mkdir build -cmake -DCMAKE_BUILD_TYPE=Release -B ./build -S . -cmake --build ./build -``` +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. + +2. Optional thread safety +You can choose to build with/without thread safety. + +3. Fastest log +Multiple level of log writing policies. You can choose to only write to disk with the most important events. + +4. Customized print / log / assert printing format +You can customize your printing format for everything. There is a templated callback function for the printing. + +5. Quickly debug something +You can use dbg macro to quickly see the output, it can be applied to any expression. + +6. Colorful output +You can have default colorful output to terminal and no color for file output + +7. Print struct/stl/pointers without any extra code + +8. Doctest like assertion and unit test feature +You can use your unit test as a documentation of function behavior. The output of unittest can be a documented report. + +9. Lazy logging for assertion +After assertion failed, the logging result will print automatically even if you didn't redirect to your error stream + +10. Logging Category +Logging information can have customized category and only display one category based on your assertion or configuration + +11. Logging for Unit Testing +You can use a correct logging result as your unit testing golden file. So you just need to manually verify your log once and save it. The unit testing framework will use the golden file to verify your unit testing result. + +12. Structured Logging +We can support output structured information directly into plain text or lisp format (json, logfmt, or other custom format should be the next step to support) + +13. Automatic Tracing with logging +While logging at the end, we can record the time consuming for this function. -可选的构建参数 +## Header-only libraries -| 构建参数 | 选项 | 含义 | -| ------------------ | ----------------- | ------------------------------------ | -| COLORFUL_OUTPUT | **AUTO**, ON, OFF | 采用彩色输出,此功能依赖特定操作系统 | -| ENABLE_THREAD_SAFE | **ON**, OFF | 启用线程安全支持 | -| ENABLE_AUTO_INIT | **ON**, OFF | 自动在进程启动时初始化一些环境检测 | -| USE_MOLD | ON, **OFF** | 使用 mold linker 链接 | -| BUILD_EXAMPLES | **ON**, OFF | 构建示例代码 | +* dbg +* print (without use extern functions) +* assert +* color (if always enabled) +## The logo generation +Thanks to the `tiv` tool: +https://github.com/stefanhaustein/TerminalImageViewer \ No newline at end of file diff --git a/Readme.zh.md b/Readme.zh.md new file mode 100644 index 00000000..6dceb4c0 --- /dev/null +++ b/Readme.zh.md @@ -0,0 +1,218 @@ +# ZeroErr 零误框架 + +[![Standard](https://img.shields.io/badge/C%2B%2B-11%2F14%2F17%2F20-blue)](https://en.wikipedia.org/wiki/C%2B%2B#Standardization) [![download](https://img.shields.io/badge/-Download-brightgreen)](https://raw.githubusercontent.com/sunxfancy/zeroerr/master/zeroerr.hpp) [![Eng-Readme](https://img.shields.io/badge/English-Readme-blue)](./Readme.md) +[![TryItOnline](https://img.shields.io/badge/TryItOnline-purple)](https://replit.com/@sunxfancy/ZeroErr-Demo#main.cpp) + +Hope you get 0 errors and 0 warnings everyday! + +![](./docs/fig/zeroerr.jpg) + + +ZeroErr 零误框架是一款轻量级C++单元测试框架,同时也集成了断言库,日志库,打印调试等诸多功能,实现了以上功能的整合与协同工作。既可以选择整体使用,也可以单独使用其中的部分功能。 + +[项目文档](https://sunxfancy.github.io/zeroerr/zh/) | [English Documentation](https://sunxfancy.github.io/zeroerr/en/) + +注:目前项目处于实验阶段,API可能会有较大变动,不建议在生产环境中使用。 + +## 为何要开发一款新的测试框架 + +目前业界主流的测试框架,Catch2, doctest, Boost.Test, cpputest 功能完善成熟,基本满足大多数应用场景。glog,spdlog等日志库使用也很简便。但这其中还存在一些问题: + + +### 1. 泛型打印支持 + +以上主流框架对任意类型数据的打印支持不够,大多数测试框架,断言库,日志库,大多缺乏泛型打印支持,对于容器和用户自定义类型缺乏直接打印的能力,ZeroErr解决了这一问题。 + +```c++ +TEST_CASE("Try logging") { + std::vector data = {1, 2, 3}; + LOG_IF(1 == 1, "data = {data}", data); +} +``` + +类似于其他C++单元测试框架,ZeroErr可以将这段宏注册的单元测试代码编译成自动运行的函数,执行后结果如下,这里我们无需定义任何规则,就可以使用LOG宏打印`vector`模板: + + +![case1](docs/fig/case1.png) + + +对于带有 `std::ostream& operator<<(std::ostream&, Type)` 流输出重载的自定义类型,可以不加修改直接打印。并且还支持容器类型的嵌套。 + +```c++ +struct Node { + std::string name; + int id; +}; + +std::ostream& operator<<(std::ostream& out, Node n) { + out << n.id << '.' << n.name; + return out; +} + +TEST_CASE("Try logging with custom type") { + std::map> data = { + {"user1", {{"a",1}, {"b",2}}}, {"user2", {{"c",3}, {"d",4}}} + }; + LOG("data = {data}", data); +} +``` + +![case2](docs/fig/case2.png) + + +当然,很多情况下,第三方库并没有重载我们预期的`<<`操作符。对于较复杂的情况,我们可以编写泛型打印函数来处理,这大大增强了系统对不同接口的处置能力。例如,我们对这个LLVM的 `llvm::Function*` 类型,可以使用如下方式用`dbg`函数打印,这里`dbg`类似于rust的`dbg`宏,用来快速打印检查任意类型,并且可以嵌套使用: + +```c++ +namespace zeroerr { // must defined in namespace zeroerr + +template +typename std::enable_if< + std::is_base_of::value || std::is_base_of::value, void>::type +PrinterExt(Printer& P, T* s, unsigned level, const char* lb, rank<2>) { + if (s == nullptr) { + P.os << P.tab(level) << "nullptr" << lb; + } else { + llvm::raw_os_ostream os(P.os); + s->print(os); + } +} +} + +TEST_CASE("customize printing of LLVM pointers") { + llvm::LLVMContext context; + std::vector args = {llvm::Type::getInt32Ty(context)}; + llvm::Module* module = new llvm::Module("test_module", context); + + auto* f = + llvm::Function::Create(llvm::FunctionType::get(llvm::Type::getVoidTy(context), args, false), + llvm::GlobalValue::ExternalLinkage, "test", module); + dbg(dbg(f)->getType()); +} +``` + +这个泛型函数会匹配所有基类为`Value`和`Type`的llvm类,然后打印时创建一个`llvm::raw_os_ostream`输出流,并对其进行调用`print`方法打印。 + +![case3-llvm](./docs/fig/case3.png) + +### 2. 断言、日志、单元测试的联合使用 + +对于使用多个不同的库实现上述功能,断言、日志、单元测试的各种功能无法协同使用。而在ZeroErr中,断言出错时,可以被日志系统捕获,可以输出到文件中保存,断言在单元测试中,可以被报告系统记录,并在最终输出中统计共有哪些断言失败。上述功能可以联合使用,也可以单独使用某一项,用法非常灵活。 + +```c++ +int fib(int n) { + REQUIRE(n >= 0, "n must be non-negative"); + REQUIRE(n < 20, "n must be less than 20"); + if (n <= 2) { + return 1; + } + return fib(n - 1) + fib(n - 2); +} + +TEST_CASE("fib function test") { + CHECK(fib(0) == 0); + CHECK(fib(1) == 1); + CHECK(fib(2) == 1); + CHECK(fib(3) == 2); + CHECK(fib(4) == 3); + CHECK(fib(5) == 5); + CHECK(fib(20) == 6765); +} +``` + +![joint1](docs/fig/joint1.png) + + +更进一步,单元测试甚至可以通过比较log结果是否与之前正确的结果相同,从而避免很多复杂的单元测试编写,粗略检查代码的正确性。 + + +```c++ +TEST_CASE("match ostream") { + // match output can be done in the following workflow + // 1. user mark the test case which are comparing output use 'have_same_output' + // 2. If the output is not exist, the result has been used as a correct verifier. + // 3. If the output is exist, compare with it and report error if output is not match. + std::cerr << "a = 100" << std::endl; + + ZEROERR_HAVE_SAME_OUTPUT; +} +``` +通过设置 `ZEROERR_HAVE_SAME_OUTPUT` 宏,系统会自动检查该测试点的output stream输出,第一次执行时的结果会自动保存起来,而之后每次执行,都会将输出与第一次输出进行对比,相同则正确,否则该点错误。用户可以第一次手动观察输出是否符合预期,若是修改了实现后,想清除保存的结果,只需要将测试目录下的 `output.txt` 缓存文件删除即可。(目前仍是实验功能) + + +最后,对于日志系统,单元测试不但能够访问日志数据,以确保函数按照预期逻辑执行出来了结果。 +还可以在逻辑出错时,自动捕获函数中的断言和相关打印信息,以便于后续的调试。 + +```c++ +118 static void function() { +119 LOG("function log {i}", 1); +120 LOG("function log {sum}, {i}", 10, 1); +121 } +... + +TEST_CASE("access log in Test case") { + zeroerr::suspendLog(); + function(); + CHECK(LOG_GET(function, 119, i, int) == 1); + CHECK(LOG_GET(function, 120, sum, int) == 10); + CHECK(LOG_GET(function, 120, i, int) == 1); + zeroerr::resumeLog(); +} +``` + +为了访问log,我们首先要暂停log系统,避免数据被输出到文件中,然后调用函数,通过`LOG_GET`宏访问log中的数据,最后再恢复log系统的运行。(目前,暂时仅能获取到每个Log点第一次调用的数据,仍是实验功能)。 + +## 3. Fuzzing的支持 + +大多数单元测试框架不支持fuzzing。然而,Fuzzing功能强大,可以自动检测软件中的错误,并且可以大大减少编写测试用例的工作量。 + +不同于其他fuzzing框架,`zeroerr`可以支持在代码中使用日志和断言,因此fuzzing的结果不仅包含了输入数据,还包含了日志和断言的信息。 + +使用方法: + +```c++ +FUZZ_TEST_CASE("fuzz_test") { + LOG("Run fuzz_test"); + FUZZ_FUNC([=](int k, std::string num) { + int t = atoi(num.c_str()); + LOG("k: {k}, num:{num}, t: {t}", k, num, t); + REQUIRE(k == t); + }) + .WithDomains(InRange(0, 10), Arbitrary()) + .WithSeeds({{5, "Foo"}, {10, "Bar"}}) + .Run(10); +} +``` + +受到 [fuzztest](https://github.com/google/fuzztest)的启发,我们使用Domain这个概念,用于指定目标函数的输入数据范围(或模式)。在这里,我们使用 `InRange` 来指定 `k` 的范围是0到10,使用 `Arbitrary` 来指定 `num` 的数据可以是任意随机字符串。然后,我们使用 `WithSeeds` 来指定fuzzing的初始种子。最后,我们使用 `Run` 来指定fuzzing的次数。 + +宏 `FUZZ_TEST_CASE` 会生成一个测试用例,可以连接到 `libFuzzer` 来运行fuzzing。最后,我们使用 `Run(10)` 来调用 `libFuzzer` 来运行目标10次。 + +为了构建带有fuzzing的测试用例,您需要使用 `clang++` 编译代码,并使用 `-fsanitize=fuzzer-no-link` 并链接 `-lclang_rt.fuzzer_no_main-x86_64`,这是一个没有main函数的libFuzzer版本。您可以通过调用 `clang++ -print-runtime-dir` 来找到这个运行时库。以下是带有fuzzing支持的测试用例的完整构建命令: + +```bash +clang++ -std=c++11 -fsanitize=fuzzer-no-link -L=`clang++ -print-runtime-dir` -lclang_rt.fuzzer_no_main-x86_64 -o test_fuzz test_fuzz.cpp +``` + + +## 项目构建 + +本项目使用CMake构建,您可以直接将整个目录引入为子项目,也可以选择下载我们提前打包好的整合文件 `zeroerr.hpp`。项目的构建可以在项目根目录下使用如下指令: + +```sh +mkdir build +cmake -DCMAKE_BUILD_TYPE=Release -B ./build -S . +cmake --build ./build +``` + +可选的构建参数 + +| 构建参数 | 选项 | 含义 | +| ------------------ | ----------------- | ------------------------------------ | +| COLORFUL_OUTPUT | **AUTO**, ON, OFF | 采用彩色输出,此功能依赖特定操作系统 | +| ENABLE_THREAD_SAFE | **ON**, OFF | 启用线程安全支持 | +| ENABLE_AUTO_INIT | **ON**, OFF | 自动在进程启动时初始化一些环境检测 | +| USE_MOLD | ON, **OFF** | 使用 mold linker 链接 | +| BUILD_EXAMPLES | **ON**, OFF | 构建示例代码 | + + + diff --git a/docs/en/mix-functions.md b/docs/en/mix-functions.md new file mode 100644 index 00000000..c3cbf52c --- /dev/null +++ b/docs/en/mix-functions.md @@ -0,0 +1,119 @@ +Mix Assertion, Logging, Unit Testing and Fuzzing +================================================= + +## Issues in Unit Tesing + +Almost every project needs unit testing. This is most important way to build a strong software but it sometimes may be hard to check boundary cases. Let's consider an example: + +```c++ +Object* create(int param) +{ + Object* obj = NULL; + // try create using method A + // do sth. + obj = ...; + if (obj) { + return obj + } + + // try create using method B + // do sth. + obj = ...; + if (obj) { + return obj; + } + return obj; +} +``` + +The function `create` try to create object using different ways, if one is failed and then try another. But how should we write test cases to check it. You didn't know this is from method A or B and the output looks the same. + +A good news is we can add log. This is a way to know some information for the function we just ran: + +```c++ +Object* create(int param) +{ + Object* obj = NULL; + // try create using method A + // do sth. + obj = ...; + if (obj) { + LOG(INFO) << "create object using method A."; + return obj + } + + // try create using method B + // do sth. + obj = ...; + if (obj) { + LOG(INFO) << "create object using method B."; + return obj; + } + return obj; +} +``` + +After running the function, we can see the output to know where it came from. However, there is no way to check the log in the unit testing. +Yes, maybe you could read the file and see what output it wrote to the file but it's very complicated and not realiable. + +I am thinking, maybe we could let the log data become **accessable**. + +## Structure Logging + +There is way to log the data, + + + + +## Another problem with assertion + +It may also conflict with assertion in the function. + +```c++ + +void target(int x) +{ + assert(x != 0); +} + +TEST_CASE("A unit test case") +{ + SUB_CASE("case1") { + target(0); + } + + SUB_CASE("case2") { + target(1); + } +} +``` + +All assertions follow a simple rule - once failed, exit. This will block the following test cases execution. This tell us that assertion should be awared by the unit testing, or at least, it should throw an exception instead exit the program. + + + + + + +## Design a Smart Assertion Library + +Have you even though the assertion macro is not power enough? For example, when you write an assertion, you may want to have some message to make it clear. + +```c++ +void compute(int x) +{ + assert(x != -1 && "x can not be -1 since ..."); + assert(x > 0 && "x must larger than 0 since ..."); +} +``` + +However, the error message still not include what value of x could be. You may need to use a format library to create the message: + +```c++ + assert(x > 0 && format("x (which is {}) must larger than 0 since ...", x)); +``` + +This looks not that good and is hard to print if you want to have more complicated values (e.g. vector, tuple...). + + + diff --git a/docs/en/simplifying-fuzz.md b/docs/en/simplifying-fuzz.md new file mode 100644 index 00000000..8441d0ff --- /dev/null +++ b/docs/en/simplifying-fuzz.md @@ -0,0 +1,62 @@ +ZeroErr: Simplifying Fuzz Testing in C++ +=========================================== + +Fuzz testing is a critical technique in software development, designed to uncover hidden bugs and vulnerabilities by providing random, unexpected, or invalid inputs to a program. [ZeroErr](https://github.com/sunxfancy/zeroerr/blob/master/Readme.en.md), a lightweight C++ framework, significantly simplifies the implementation of fuzz testing, integrating it seamlessly with its assertion and logging functionalities. + +Key Features of ZeroErr Fuzz Testing +* Easy Setup and Integration: ZeroErr allows developers to quickly set up fuzz tests using its intuitive macros. The framework handles the complexities of random input generation and test execution, letting you focus on defining the behavior you want to test. + +* Flexible Input Domains: The framework supports defining specific input domains, such as ranges of integers or arbitrary strings, ensuring that fuzz tests cover a wide variety of scenarios. For instance, you can specify integer ranges or create complex data structures like vectors and maps filled with random values. + +* Seeding for Reproducibility: ZeroErr allows you to seed your fuzz tests with specific values. This is useful for ensuring that particular edge cases are always tested, enhancing the reliability of your tests. + +* Comprehensive Logging: Integrated logging captures detailed information about each test case, including input values and the results of assertions. This makes it easier to diagnose and fix issues when they arise. + +* Error Detection and Reporting: ZeroErr's assertions work in tandem with fuzz testing, immediately identifying and reporting mismatches or unexpected behaviors. This combination ensures that even subtle bugs are detected and logged. + +### Example Fuzz Test Case + +```cpp +#include "zeroerr/fuzztest.h" +#include "zeroerr/assert.h" +#include "zeroerr/log.h" +#include +#include +#include + +using namespace zeroerr; + +FUZZ_TEST_CASE("fuzz_test") { + LOG("Run fuzz_test"); + FUZZ_FUNC([=](int k, std::string num) { + int t = atoi(num.c_str()); + LOG("k: {k}, num: {num}, t: {t}", k, num, t); + REQUIRE(k == t); + }) + .WithDomains(InRange(0, 10), Arbitrary()) + .WithSeeds({{5, "Foo"}, {10, "Bar"}}) + .Run(10); +} + +FUZZ_TEST_CASE("fuzz_test2") { + LOG("Run fuzz_test2"); + FUZZ_FUNC([=](int k, std::vector num) { + int t = num.size(); + LOG("k: {k}, t: {t}", k, t); + }) + .WithDomains(InRange(0, 10), ContainerOf(Arbitrary())) + .Run(10); +} +``` + +In the examples above, ZeroErr defines fuzz tests that handle different data types, including integers, strings, vectors, and maps. The framework's flexibility and powerful logging capabilities make it an invaluable tool for C++ developers aiming to improve code quality through effective fuzz testing. + +For more information and to get started with ZeroErr, visit the [ZeroErr GitHub repository](https://github.com/sunxfancy/zeroerr/blob/master/Readme.en.md). + + + + + + + + diff --git a/docs/zh/get-started.md b/docs/zh/get-started.md index 20ea8c1f..5772b70d 100644 --- a/docs/zh/get-started.md +++ b/docs/zh/get-started.md @@ -14,7 +14,7 @@ ZeroErr可以采用两种方式引用,其一是直接下载 `zeroerr.hpp` 文 下面我们用一些简单的示例来说明如何使用框架。 -### 单元测试 +### 单元测试和断言 `TEST_CASE` 宏是最基础的定义单元测试的宏,如果您熟悉 catch2 或 doctest, 您应该对这种写法非常熟悉。我们给该测试一个名字,然后在接下来的函数体中编写测试代码。 @@ -60,6 +60,9 @@ TEST_CASE("fib function test") { - CHECK_GE(a, b) 判断是否大于等于 `>=` +所有的断言宏,不但可以在单元测试中使用,也可以在任意函数中使用。 + + ### log 日志系统 日志系统提供了一组宏: @@ -75,7 +78,10 @@ TEST_CASE("fib function test") { LOG("message {n1} {n2}", data1, ...) ``` -首先是消息的格式化字符串,这个字符串必须是const char*类型的原始字符串,不能是由string转换的,或者自己拼接构造的,因为这个字符串的地址还将被用于索引该LOG发生的位置。 +本系统提供的是一种结构化的日志记录方式,使用消息 + 参数的方式来表示一个事件。我们并不会立刻将日志格式化成字符串,而是将参数记录下来,等到需要输出时,再进行格式化,或者直接将二进制的参数保存下来。这样可以提高日志的效率,并且给我们在记录日志后,用编程的方式访问日志数据的可能性。 + +首先是消息的格式化字符串,这个字符串必须是const char*类型的原始字符串,不能是由string转换的,或者自己拼接构造的,因为这个字符串的地址还将被用于索引该LOG发生的位置。同时,要注意,所有的参数必须是可以拷贝的,这样才能保证在日志输出时不出现问题。 + `{n1}` 表示一个任意类型的参数,名称是`n1`。这种设计的原因是,log一般用来记录一些事件的发生,如果我们立即格式化字符串将log写入文件,往往会消耗一定时间,然而,如果我们只是将参数记录下来,那么效率就能大大提升,设置一个名称,也可以方便对log的数据进行检索,从而在系统中快速找出需要的log条目。 @@ -102,8 +108,73 @@ LOG_IF_EVERY_(N, condition, "message {n1} {n2}", data1, ...) -`LOG_FIRST` 只记录首次运行到此 +`LOG_FIRST` 只记录首次运行到此的事件信息 ``` LOG_FIRST("message {n1} {n2}", data1, ...) ``` + + +`LOG` 系列的宏会自动将日志写到标准错误流(stderr)中,如果您希望其输出到文件,可以使用 `setFileLogger` 函数进行设置: + +``` +zeroerr::LogStream::getDefault().setFileLogger("log.txt"); +``` + +您也可以在任意时刻将日志再重新定向到标准错误流: + +``` +zeroerr::LogStream::getDefault().setStderrLogger(); +``` + + +您也可以创建多个`LogStream`对象,在LOG时指定输出流,每个对象可以有自己的日志文件,这样可以实现多个日志文件的输出。 + +``` +zeroerr::LogStream log1; +log1.setFileLogger("log1.txt"); + +LOG("message {n1} {n2}", log1, data1, data2); +``` + +### 开启Fuzzing支持 + +Fuzzing是一种常用的自动化测试手段,通过不断随机生成测试数据,来测试程序的稳定性。ZeroErr提供了一种简单的libfuzzer集成方式,只需要使用 `clang++` 编译代码,并使用 `-fsanitize=fuzzer-no-link` 并链接 `-lclang_rt.fuzzer_no_main-x86_64`,这是一个没有main函数的libFuzzer版本。您可以通过调用 `clang++ -print-runtime-dir` 来找到这个运行时库。 + +``` +> clang++ -print-runtime-dir /mnt/h/Workspace/zeroerr(dev✗)@xiaofan-pc +/usr/lib/llvm-14/lib/clang/14.0.0/lib/linux +``` + +类似于 `TEST_CASE` 宏,我们提供了 `FUZZ_TEST_CASE` 宏,用于定义一个fuzzing测试用例,您可以在其中使用 `FUZZ_FUNC` 宏,来定义一个fuzzing测试的函数,该函数接受一个或多个参数,这些参数可以通过 `WithDomains` 来指定,`WithSeeds` 来指定初始种子,`Run` 来指定运行次数。 + + +``` +// test_fuzz.cpp + +#define ZEROERR_IMPLEMENTATION +#include "zeroerr.hpp" + + +FUZZ_TEST_CASE("fuzz_test") { + LOG("Run fuzz_test"); + FUZZ_FUNC([=](int k, std::string num) { + int t = atoi(num.c_str()); + LOG("k: {k}, num:{num}, t: {t}", k, num, t); + REQUIRE(k == t); + }) + .WithDomains(InRange(0, 10), Arbitrary()) + .WithSeeds({{5, "Foo"}, {10, "Bar"}}) + .Run(10); +} +``` + + +以下是带有fuzzing支持的测试用例的完整构建命令: + +``` +clang++ -std=c++11 -I -fsanitize=fuzzer-no-link -L=`clang++ -print-runtime-dir` -lclang_rt.fuzzer_no_main-x86_64 -o test_fuzz test_fuzz.cpp +``` + + + diff --git a/docs/zh/introduction.md b/docs/zh/introduction.md index 00b8475d..d0f6ab30 100644 --- a/docs/zh/introduction.md +++ b/docs/zh/introduction.md @@ -147,3 +147,36 @@ TEST_CASE("access log in Test case") { ``` 为了访问log,我们首先要暂停log系统,避免数据被输出到文件中,然后调用函数,通过`LOG_GET`宏访问log中的数据,最后再恢复log系统的运行。(目前,暂时仅能获取到每个Log点第一次调用的数据,仍是实验功能)。 + + +#### 3. Fuzzing的支持 + +大多数单元测试框架不支持fuzzing。然而,Fuzzing功能强大,可以自动检测软件中的错误,并且可以大大减少编写测试用例的工作量。 + +不同于其他fuzzing框架,`zeroerr`可以支持在代码中使用日志和断言,因此fuzzing的结果不仅包含了输入数据,还包含了日志和断言的信息。 + +使用方法: + +``` +FUZZ_TEST_CASE("fuzz_test") { + LOG("Run fuzz_test"); + FUZZ_FUNC([=](int k, std::string num) { + int t = atoi(num.c_str()); + LOG("k: {k}, num:{num}, t: {t}", k, num, t); + REQUIRE(k == t); + }) + .WithDomains(InRange(0, 10), Arbitrary()) + .WithSeeds({{5, "Foo"}, {10, "Bar"}}) + .Run(10); +} +``` + +受到 [fuzztest](https://github.com/google/fuzztest)的启发,我们使用Domain这个概念,用于指定目标函数的输入数据范围(或模式)。在这里,我们使用 `InRange` 来指定 `k` 的范围是0到10,使用 `Arbitrary` 来指定 `num` 的数据可以是任意随机字符串。然后,我们使用 `WithSeeds` 来指定fuzzing的初始种子。最后,我们使用 `Run` 来指定fuzzing的次数。 + +宏 `FUZZ_TEST_CASE` 会生成一个测试用例,可以连接到 `libFuzzer` 来运行fuzzing。最后,我们使用 `Run(10)` 来调用 `libFuzzer` 来运行目标10次。 + +为了构建带有fuzzing的测试用例,您需要使用 `clang++` 编译代码,并使用 `-fsanitize=fuzzer-no-link` 并链接 `-lclang_rt.fuzzer_no_main-x86_64`,这是一个没有main函数的libFuzzer版本。您可以通过调用 `clang++ -print-runtime-dir` 来找到这个运行时库。以下是带有fuzzing支持的测试用例的完整构建命令: + +``` +clang++ -std=c++11 -fsanitize=fuzzer-no-link -L=`clang++ -print-runtime-dir` -lclang_rt.fuzzer_no_main-x86_64 -o test_fuzz test_fuzz.cpp +``` diff --git a/include/zeroerr/assert.h b/include/zeroerr/assert.h index c66c1d23..8f296d91 100644 --- a/include/zeroerr/assert.h +++ b/include/zeroerr/assert.h @@ -53,13 +53,17 @@ ZEROERR_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wgnu-zero-variadic-macro-arguments") ZEROERR_CLANG_SUPPRESS_WARNING_POP #endif -#define ZEROERR_ASSERT_EXP(cond, level, throws, is_false, ...) \ +#define ZEROERR_ASSERT_EXP(cond, level, expect_throw, is_false, ...) \ ZEROERR_FUNC_SCOPE_BEGIN { \ zeroerr::assert_info info{zeroerr::assert_level::ZEROERR_CAT(level, _l), \ - zeroerr::assert_throw::throws, is_false}; \ + zeroerr::assert_throw::expect_throw, is_false}; \ \ zeroerr::AssertionData assertion_data(__FILE__, __LINE__, #cond, info); \ - assertion_data.setResult(zeroerr::ExpressionDecomposer() << cond); \ + try { \ + assertion_data.setResult(zeroerr::ExpressionDecomposer() << cond); \ + } catch (const std::exception& e) { \ + assertion_data.setException(e); \ + } \ zeroerr::detail::context_helper< \ decltype(_ZEROERR_TEST_CONTEXT), \ std::is_same, rhs, ZEROERR_WARN, no_throw, false, __VA_ARGS__) #define ZEROERR_CHECK_GE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >=, rhs, ZEROERR_WARN, no_throw, false, __VA_ARGS__) -#define ZEROERR_REQUIRE_EQ(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, ==, rhs, ZEROERR_ERROR, throws, false, __VA_ARGS__) -#define ZEROERR_REQUIRE_NE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, !=, rhs, ZEROERR_ERROR, throws, false, __VA_ARGS__) -#define ZEROERR_REQUIRE_LT(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, <, rhs, ZEROERR_ERROR, throws, false, __VA_ARGS__) -#define ZEROERR_REQUIRE_LE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, <=, rhs, ZEROERR_ERROR, throws, false, __VA_ARGS__) -#define ZEROERR_REQUIRE_GT(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >, rhs, ZEROERR_ERROR, throws, false, __VA_ARGS__) -#define ZEROERR_REQUIRE_GE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >=, rhs, ZEROERR_ERROR, throws, false, __VA_ARGS__) +#define ZEROERR_REQUIRE_EQ(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, ==, rhs, ZEROERR_ERROR, no_throw, false, __VA_ARGS__) +#define ZEROERR_REQUIRE_NE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, !=, rhs, ZEROERR_ERROR, no_throw, false, __VA_ARGS__) +#define ZEROERR_REQUIRE_LT(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, <, rhs, ZEROERR_ERROR, no_throw, false, __VA_ARGS__) +#define ZEROERR_REQUIRE_LE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, <=, rhs, ZEROERR_ERROR, no_throw, false, __VA_ARGS__) +#define ZEROERR_REQUIRE_GT(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >, rhs, ZEROERR_ERROR, no_throw, false, __VA_ARGS__) +#define ZEROERR_REQUIRE_GE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >=, rhs, ZEROERR_ERROR, no_throw, false, __VA_ARGS__) -#define ZEROERR_ASSERT_EQ(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, ==, rhs, ZEROERR_FATAL, throws, false, __VA_ARGS__) -#define ZEROERR_ASSERT_NE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, !=, rhs, ZEROERR_FATAL, throws, false, __VA_ARGS__) -#define ZEROERR_ASSERT_LT(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, <, rhs, ZEROERR_FATAL, throws, false, __VA_ARGS__) -#define ZEROERR_ASSERT_LE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, <=, rhs, ZEROERR_FATAL, throws, false, __VA_ARGS__) -#define ZEROERR_ASSERT_GT(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >, rhs, ZEROERR_FATAL, throws, false, __VA_ARGS__) -#define ZEROERR_ASSERT_GE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >=, rhs, ZEROERR_FATAL, throws, false, __VA_ARGS__) +#define ZEROERR_ASSERT_EQ(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, ==, rhs, ZEROERR_FATAL, no_throw, false, __VA_ARGS__) +#define ZEROERR_ASSERT_NE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, !=, rhs, ZEROERR_FATAL, no_throw, false, __VA_ARGS__) +#define ZEROERR_ASSERT_LT(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, <, rhs, ZEROERR_FATAL, no_throw, false, __VA_ARGS__) +#define ZEROERR_ASSERT_LE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, <=, rhs, ZEROERR_FATAL, no_throw, false, __VA_ARGS__) +#define ZEROERR_ASSERT_GT(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >, rhs, ZEROERR_FATAL, no_throw, false, __VA_ARGS__) +#define ZEROERR_ASSERT_GE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >=, rhs, ZEROERR_FATAL, no_throw, false, __VA_ARGS__) #define CHECK_EQ(...) ZEROERR_SUPPRESS_VARIADIC_MACRO ZEROERR_EXPAND(ZEROERR_CHECK_EQ(__VA_ARGS__)) ZEROERR_SUPPRESS_VARIADIC_MACRO_POP #define CHECK_NE(...) ZEROERR_SUPPRESS_VARIADIC_MACRO ZEROERR_EXPAND(ZEROERR_CHECK_NE(__VA_ARGS__)) ZEROERR_SUPPRESS_VARIADIC_MACRO_POP @@ -224,9 +241,9 @@ struct AssertionData : std::exception { AssertionData(const char* file, unsigned line, const char* cond, assert_info info) : file(file), line(line), info(info) { - std::string cond_str(cond); - std::string pattern = "zeroerr::ExpressionDecomposer() << "; - size_t pos = cond_str.find(pattern); + static std::string pattern = "zeroerr::ExpressionDecomposer() << "; + std::string cond_str(cond); + size_t pos = cond_str.find(pattern); if (pos != std::string::npos) cond_str.replace(pos, pos + pattern.size(), ""); this->cond = cond_str; } @@ -240,6 +257,11 @@ struct AssertionData : std::exception { message = r.decomp; } + void setException(const std::exception& e) { + passed = info.throw_type == assert_throw::throws; + message = e.what(); + } + std::string log() { std::stringstream ss; ss << " " << cond << " expands to " << message << std::endl; @@ -247,14 +269,13 @@ struct AssertionData : std::exception { return ss.str(); } - // throw the exception void operator()() { if (passed) return; if (shouldThrow()) throw *this; } - bool shouldThrow() { return info.throw_type == assert_throw::throws; } + bool shouldThrow() { return info.level != assert_level::ZEROERR_WARN_l; } }; namespace detail { diff --git a/include/zeroerr/internal/config.h b/include/zeroerr/internal/config.h index 34be37a8..d3af5faa 100644 --- a/include/zeroerr/internal/config.h +++ b/include/zeroerr/internal/config.h @@ -1,8 +1,8 @@ #pragma once #define ZEROERR_VERSION_MAJOR 0 -#define ZEROERR_VERSION_MINOR 2 -#define ZEROERR_VERSION_PATCH 1 +#define ZEROERR_VERSION_MINOR 3 +#define ZEROERR_VERSION_PATCH 0 #define ZEROERR_VERSION \ (ZEROERR_VERSION_MAJOR * 10000 + ZEROERR_VERSION_MINOR * 100 + ZEROERR_VERSION_PATCH) diff --git a/include/zeroerr/internal/threadsafe.h b/include/zeroerr/internal/threadsafe.h index 340e2786..fb49a271 100644 --- a/include/zeroerr/internal/threadsafe.h +++ b/include/zeroerr/internal/threadsafe.h @@ -7,11 +7,14 @@ #define ZEROERR_MUTEX(x) #define ZEROERR_LOCK(x) #define ZEROERR_ATOMIC(x) x +#define ZEROERR_LOAD(x) x + #else #define ZEROERR_MUTEX(x) static std::mutex x; #define ZEROERR_LOCK(x) std::lock_guard lock(x); #define ZEROERR_ATOMIC(x) std::atomic +#define ZEROERR_LOAD(x) x.load() #include #include diff --git a/include/zeroerr/internal/typetraits.h b/include/zeroerr/internal/typetraits.h index 900589f5..12045b48 100644 --- a/include/zeroerr/internal/typetraits.h +++ b/include/zeroerr/internal/typetraits.h @@ -132,6 +132,35 @@ struct ele_type_is_pair< T, void_t().first), decltype(std::declval().second)>> : std::true_type {}; +template +struct to_store_type { + using type = T; +}; + +template <> +struct to_store_type { + using type = std::string; +}; + +template <> +struct to_store_type { + using type = std::string; +}; + + +template +struct to_store_type::value>::type> { + using type = T; +}; + +template +struct to_store_type { + using type = T; +}; + +template +using to_store_type_t = typename to_store_type::type; + template struct visit_impl { diff --git a/include/zeroerr/log.h b/include/zeroerr/log.h index 7b6367aa..82271cfe 100644 --- a/include/zeroerr/log.h +++ b/include/zeroerr/log.h @@ -1,5 +1,7 @@ #pragma once #include "zeroerr/internal/config.h" + +#include "zeroerr/internal/threadsafe.h" #include "zeroerr/internal/typetraits.h" #include "zeroerr/dbg.h" @@ -7,14 +9,8 @@ #include "zeroerr/print.h" #include -#include -#include -#include -#include #include -#include #include -#include #include ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH @@ -27,12 +23,13 @@ class mutex; namespace zeroerr { - +// clang-format off #define ZEROERR_INFO(...) ZEROERR_SUPPRESS_VARIADIC_MACRO ZEROERR_EXPAND(ZEROERR_INFO_(__VA_ARGS__)) ZEROERR_SUPPRESS_VARIADIC_MACRO_POP #define ZEROERR_LOG(...) ZEROERR_SUPPRESS_VARIADIC_MACRO ZEROERR_EXPAND(ZEROERR_LOG_(LOG_l, __VA_ARGS__)) ZEROERR_SUPPRESS_VARIADIC_MACRO_POP #define ZEROERR_WARN(...) ZEROERR_SUPPRESS_VARIADIC_MACRO ZEROERR_EXPAND(ZEROERR_LOG_(WARN_l, __VA_ARGS__)) ZEROERR_SUPPRESS_VARIADIC_MACRO_POP #define ZEROERR_ERROR(...) ZEROERR_SUPPRESS_VARIADIC_MACRO ZEROERR_EXPAND(ZEROERR_LOG_(ERROR_l, __VA_ARGS__)) ZEROERR_SUPPRESS_VARIADIC_MACRO_POP #define ZEROERR_FATAL(...) ZEROERR_SUPPRESS_VARIADIC_MACRO ZEROERR_EXPAND(ZEROERR_LOG_(FATAL_l, __VA_ARGS__)) ZEROERR_SUPPRESS_VARIADIC_MACRO_POP +// clang-format on #ifdef ZEROERR_USE_SHORT_LOG_MACRO @@ -67,7 +64,9 @@ namespace zeroerr { #define FATAL(...) ZEROERR_FATAL(__VA_ARGS__) #define VERBOSE(v) ZEROERR_VERBOSE(v) -#endif +#define LOG_GET(func, id, name, type) ZEROERR_LOG_GET(func, id, name, type) + +#endif // ZEROERR_USE_SHORT_LOG_MACRO #define ZEROERR_LOG_IF(condition, ACTION, ...) \ do { \ @@ -155,22 +154,21 @@ extern int _ZEROERR_G_VERBOSE; #define ZEROERR_VERBOSE(v) if (zeroerr::_ZEROERR_G_VERBOSE >= (v)) -#define ZEROERR_LOG_(severity, message, ...) \ - do { \ - ZEROERR_G_CONTEXT_SCOPE(true); \ - auto msg = zeroerr::LogStream::getDefault().push(__VA_ARGS__); \ - \ - static zeroerr::LogInfo log_info{__FILE__, \ - __func__, \ - message, \ - ZEROERR_LOG_CATEGORY, \ - __LINE__, \ - msg.size, \ - zeroerr::LogSeverity::severity}; \ - msg.log->info = &log_info; \ - if (zeroerr::LogStream::getDefault().flush_mode == \ - zeroerr::LogStream::FlushMode::FLUSH_AT_ONCE) \ - zeroerr::LogStream::getDefault().flush(); \ +#define ZEROERR_LOG_(severity, message, ...) \ + do { \ + ZEROERR_G_CONTEXT_SCOPE(true); \ + auto msg = zeroerr::log(__VA_ARGS__); \ + \ + static zeroerr::LogInfo log_info{__FILE__, \ + __func__, \ + message, \ + ZEROERR_LOG_CATEGORY, \ + __LINE__, \ + msg.size, \ + zeroerr::LogSeverity::severity}; \ + msg.log->info = &log_info; \ + if (msg.stream.getFlushMode() == zeroerr::LogStream::FlushMode::FLUSH_AT_ONCE) \ + msg.stream.flush(); \ } while (0) #define ZEROERR_INFO_(...) \ @@ -203,8 +201,8 @@ extern int _ZEROERR_G_VERBOSE; // This macro can access the log in memory -#define LOG_GET(func, line, name, type) \ - zeroerr::LogStream::getDefault().getLog(#func, line, #name) +#define ZEROERR_LOG_GET(func, id, name, type) \ + zeroerr::LogStream::getDefault().getLog(#func, id, #name) namespace detail { @@ -259,6 +257,8 @@ struct LogMessage { virtual std::string str() const = 0; virtual void* getRawLog(std::string name) const = 0; + virtual std::map getData() const = 0; + // meta data of this log message const LogInfo* info; @@ -267,17 +267,8 @@ struct LogMessage { }; -// This is a helper class to get the raw pointer of the tuple -struct GetTuplePtr { - void* ptr = nullptr; - template - void operator()(H& v) { - ptr = (void*)&v; - } -}; - template -struct LogMessageImpl : LogMessage { +struct LogMessageImpl final : LogMessage { std::tuple args; LogMessageImpl(T... args) : LogMessage(), args(args...) {} @@ -285,30 +276,116 @@ struct LogMessageImpl : LogMessage { return gen_str(info->message, args, detail::gen_seq{}); } + // This is a helper class to get the raw pointer of the tuple + struct GetTuplePtr { + void* ptr = nullptr; + template + void operator()(H& v) { + ptr = (void*)&v; + } + }; + void* getRawLog(std::string name) const override { GetTuplePtr f; detail::visit_at(args, info->names.at(name), f); return f.ptr; } + + struct PrintTupleData { + std::map data; + Printer print; + std::string name; + + PrintTupleData() : print() { + print.isCompact = true; + print.line_break = ""; + } + + template + void operator()(H& v) { + data[name] = print(v); + } + }; + + std::map getData() const override { + PrintTupleData printer; + for (auto it = info->names.begin(); it != info->names.end(); ++it) { + printer.name = it->first; + detail::visit_at(args, it->second, printer); + } + return printer.data; + } }; struct DataBlock; +class LogStream; + class Logger { public: virtual ~Logger() = default; virtual void flush(DataBlock*) = 0; }; +struct PushResult { + LogMessage* log; + unsigned size; + LogStream& stream; +}; + +class LogIterator { +public: + LogIterator() : p(nullptr), q(nullptr) {} + LogIterator(LogStream& stream, std::string message = "", std::string function_name = "", + int line = -1); + LogIterator(const LogIterator& rhs) : p(rhs.p), q(rhs.q) {} + LogIterator& operator=(const LogIterator& rhs) { + p = rhs.p; + q = rhs.q; + return *this; + } + + LogIterator& operator++(); + LogIterator operator++(int) { + LogIterator tmp = *this; + ++*this; + return tmp; + } + + template + T get(std::string name) { + void* data = q->getRawLog(name); + if (data) return *(T*)(data); + return T{}; + } + + bool operator==(const LogIterator& rhs) const { return p == rhs.p && q == rhs.q; } + bool operator!=(const LogIterator& rhs) const { return !(*this == rhs); } + + LogMessage& get() const { return *q; } + LogMessage& operator*() const { return *q; } + LogMessage* operator->() const { return q; } + + void check_at_safe_pos(); + + friend class LogStream; + +protected: + bool check_filter(); + void next(); + + DataBlock* p; + LogMessage* q; + + std::string function_name_filter; + std::string message_filter; + int line_filter = -1; +}; + class LogStream { public: LogStream(); virtual ~LogStream(); - struct PushResult { - LogMessage* log; - unsigned size; - }; - enum FlushMode { FLUSH_AT_ONCE, FLUSH_WHEN_FULL, FLUSH_MANUALLY }; enum LogMode { ASYNC, SYNC }; enum DirMode { @@ -320,10 +397,16 @@ class LogStream { template PushResult push(T&&... args) { - unsigned size = sizeof(LogMessageImpl); - void* p = alloc_block(size); - LogMessage* msg = new (p) LogMessageImpl(std::forward(args)...); - return {msg, size}; + // unsigned size = sizeof(LogMessageImpl); + unsigned size = sizeof(LogMessageImpl...>); + void* p; + if (use_lock_free) + p = alloc_block_lockfree(size); + else + p = alloc_block(size); + // LogMessage* msg = new (p) LogMessageImpl(std::forward(args)...); + LogMessage* msg = new (p) LogMessageImpl...>(args...); + return {msg, size, *this}; } template @@ -333,28 +416,70 @@ class LogStream { return T{}; } + template + T getLog(std::string func, std::string msg, std::string name) { + void* data = getRawLog(func, msg, name); + if (data) return *(T*)(data); + return T{}; + } + void* getRawLog(std::string func, unsigned line, std::string name); + void* getRawLog(std::string func, std::string msg, std::string name); + + LogIterator begin(std::string message = "", std::string function_name = "", int line = -1) { + return LogIterator(*this, message, function_name, line); + } + LogIterator end() { return LogIterator(); } + LogIterator current(std::string message = "", std::string function_name = "", int line = -1); void flush(); - void setFileLogger(std::string name); + void setFileLogger(std::string name, DirMode mode1 = SINGLE_FILE, DirMode mode2 = SINGLE_FILE, + DirMode mode3 = SINGLE_FILE); void setStdoutLogger(); void setStderrLogger(); static LogStream& getDefault(); - FlushMode flush_mode = FLUSH_AT_ONCE; - LogMode log_mode = SYNC; - DirMode dir_mode = SINGLE_FILE; + + void setFlushAtOnce() { flush_mode = FLUSH_AT_ONCE; } + void setFlushWhenFull() { flush_mode = FLUSH_WHEN_FULL; } + void setFlushManually() { flush_mode = FLUSH_MANUALLY; } + void setAsyncLog() { log_mode = ASYNC; } + void setSyncLog() { log_mode = SYNC; } + + FlushMode getFlushMode() const { return flush_mode; } + void setFlushMode(FlushMode mode) { flush_mode = mode; } + LogMode getLogMode() const { return log_mode; } + void setLogMode(LogMode mode) { log_mode = mode; } + + bool use_lock_free = true; + + friend class LogIterator; private: - DataBlock *first, *last; - Logger* logger = nullptr; + DataBlock *first, *prepare; + ZEROERR_ATOMIC(DataBlock*) m_last; + Logger* logger = nullptr; + FlushMode flush_mode = FLUSH_AT_ONCE; + LogMode log_mode = SYNC; #ifndef ZEROERR_NO_THREAD_SAFE std::mutex* mutex; #endif void* alloc_block(unsigned size); + void* alloc_block_lockfree(unsigned size); }; +template +PushResult log(T&&... args) { + return LogStream::getDefault().push(std::forward(args)...); +} + +template +PushResult log(LogStream& stream, T&&... args) { + return stream.push(std::forward(args)...); +} + + /** * @brief ContextScope is a helper class created in each basic block where you use INFO(). * The context scope can has lazy evaluated function F(std::ostream&) that is called when the diff --git a/include/zeroerr/print.h b/include/zeroerr/print.h index 64f12823..78829272 100644 --- a/include/zeroerr/print.h +++ b/include/zeroerr/print.h @@ -249,7 +249,11 @@ struct Printer { #endif } - std::string str() const { return static_cast(os).str(); } + std::string str() const { + if (use_stringstream == false) + throw std::runtime_error("Printer is not using stringstream"); + return static_cast(os).str(); + } operator std::string() const { return str(); } friend std::ostream& operator<<(std::ostream& os, const Printer& P) { diff --git a/include/zeroerr/unittest.h b/include/zeroerr/unittest.h index 79149c73..a00deff1 100644 --- a/include/zeroerr/unittest.h +++ b/include/zeroerr/unittest.h @@ -68,15 +68,17 @@ struct TestCase; struct UnitTest { UnitTest& parseArgs(int argc, const char** argv); int run(); - bool run_filiter(const TestCase& tc); + bool run_filter(const TestCase& tc); bool silent = false; bool run_bench = false; bool run_fuzz = false; bool list_test_cases = false; + bool no_color = false; + bool log_to_report = false; std::string correct_output_path; std::string reporter_name = "console"; std::string binary; - struct Filiters* filiters; + struct Filters* filters; }; struct TestCase { diff --git a/src/benchmark.cpp b/src/benchmark.cpp index 86bba5c4..ed32418c 100644 --- a/src/benchmark.cpp +++ b/src/benchmark.cpp @@ -245,10 +245,10 @@ void Benchmark::report() { Table output; output.set_header(headers); for (auto& row : result) { - auto result = row.average(); + auto row_avg = row.average(); std::vector values; for (int j = 0; j < 7; ++j) - if (row.has.data[j]) values.push_back(result.data[j]); + if (row.has.data[j]) values.push_back(row_avg.data[j]); output.add_row(row.name, values); } std::cerr << output.str() << std::endl; @@ -544,7 +544,8 @@ struct WindowsPerformanceCounter { // https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ke/profobj/kprofile_source.htm // TotalIssues TotalCycles CacheMisses BranchMispredictions unsigned long perf_counter[4] = {0x02, 0x13, 0x0A, 0x0B}; - TraceSetInformation(mTraceHandle, TracePmcCounterListInfo, perf_counter, sizeof(perf_counter)); + TraceSetInformation(mTraceHandle, TracePmcCounterListInfo, perf_counter, + sizeof(perf_counter)); cleanup: if (mTraceHandle) { @@ -630,6 +631,8 @@ void PerformanceCounter::endMeasure() { void PerformanceCounter::updateResults(uint64_t numIters) { #ifdef ZEROERR_PERF _perf->updateResults(numIters); +#else + (void)numIters; #endif } diff --git a/src/log.cpp b/src/log.cpp index 5db7b931..82a5bb24 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -1,6 +1,15 @@ #include "zeroerr/log.h" #include "zeroerr/internal/threadsafe.h" +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + const char* ZEROERR_LOG_CATEGORY = "default"; @@ -23,33 +32,37 @@ LogInfo::LogInfo(const char* filename, const char* function, const char* message category(category), line(line), size(size), - severity(severity) { + severity(severity), + names() { for (const char* p = message; *p; p++) if (*p == '{') { const char* q = p + 1; while (*q && *q != '}') q++; if (*q == '}') { - std::string N(p + 1, q); - names[N] = names.size(); + std::string N(p + 1, (size_t)(q-p-1)); + names[N] = static_cast(names.size()); p = q; } } } -constexpr size_t LogStreamMaxSize = 1024 * 1024 - 16; +constexpr size_t LogStreamMaxSize = 1 * 1024 - 16; struct DataBlock { - size_t size = 0; + ZEROERR_ATOMIC(size_t) size; DataBlock* next = nullptr; char data[LogStreamMaxSize]; + DataBlock() : size(0) {} + LogMessage* begin() { return (LogMessage*)data; } LogMessage* end() { return (LogMessage*)&data[size]; } }; LogStream::LogStream() { - first = last = new DataBlock(); + first = m_last = new DataBlock(); + prepare = new DataBlock(); #ifndef ZEROERR_NO_THREAD_SAFE mutex = new std::mutex(); #endif @@ -74,6 +87,7 @@ void* LogStream::alloc_block(unsigned size) { throw std::runtime_error("LogStream::push: size > LogStreamMaxSize"); } ZEROERR_LOCK(*mutex); + auto* last = ZEROERR_LOAD(m_last); if (last->size + size > LogStreamMaxSize) { if (flush_mode == FLUSH_WHEN_FULL) { logger->flush(last); @@ -88,8 +102,44 @@ void* LogStream::alloc_block(unsigned size) { return p; } +void* LogStream::alloc_block_lockfree(unsigned size) { +#ifndef ZEROERR_NO_THREAD_SAFE + if (size > LogStreamMaxSize) { + throw std::runtime_error("LogStream::push: size > LogStreamMaxSize"); + } + DataBlock* last = ZEROERR_LOAD(m_last); + while (true) { + size_t p = last->size.load(); + if (p <= (LogStreamMaxSize - size)) { + if (last->size.compare_exchange_strong(p, p + size)) return last->data + p; + } else { + if (m_last.compare_exchange_strong(last, prepare)) { + if (flush_mode == FLUSH_WHEN_FULL) { + logger->flush(last); + last->size = 0; + prepare = last; + } else { + prepare->next = last; + prepare = new DataBlock(); + } + } + } + } +#else + return alloc_block(size); +#endif +} +LogIterator LogStream::current(std::string message, std::string function_name, int line) { + LogIterator iter(*this, message, function_name, line); + DataBlock* last = ZEROERR_LOAD(m_last); + iter.p = last; + iter.q = reinterpret_cast(&(last->data[ZEROERR_LOAD(last->size)])); + return iter; +} + void LogStream::flush() { ZEROERR_LOCK(*mutex); + DataBlock* last = ZEROERR_LOAD(m_last); for (DataBlock* p = first; p != last; p = p->next) { logger->flush(p); delete p; @@ -112,6 +162,57 @@ void* LogStream::getRawLog(std::string func, unsigned line, std::string name) { return nullptr; } +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); + return nullptr; +} + +LogIterator::LogIterator(LogStream& stream, std::string message, std::string function_name, + int line) + : p(stream.first), + q(stream.first->begin()), + message_filter(message), + function_name_filter(function_name), + line_filter(line) { + while (!check_filter() && p) next(); +} + +void LogIterator::check_at_safe_pos() { + if (static_cast((char*)q - p->data) >= ZEROERR_LOAD(p->size)) { + p = p->next; + q = reinterpret_cast(p->data); + } +} + +void LogIterator::next() { + if (q < p->end()) { + q = moveBytes(q, q->info->size); + if (q >= p->end()) next(); + } else { + p = p->next; + if (p) + q = p->begin(); + else + q = nullptr; + } +} + +LogIterator& LogIterator::operator++() { + do { + next(); + } while (p && !check_filter()); + return *this; +} + +bool LogIterator::check_filter() { + if (!message_filter.empty() && 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; +} + class FileLogger : public Logger { public: FileLogger(std::string name) { file = fopen(name.c_str(), "w"); } @@ -132,6 +233,126 @@ class FileLogger : public Logger { FILE* file; }; + +#ifdef _WIN32 +static char split = '\\'; +#else +static char split = '/'; +#endif + +static void make_dir(std::string path) { + size_t pos = 0; + do { + pos = path.find_first_of(split, pos + 1); + std::string sub = path.substr(0, pos); +#ifdef _WIN32 + CreateDirectory(sub.c_str(), NULL); +#else + struct stat st; + if (stat(sub.c_str(), &st) == -1) { + mkdir(sub.c_str(), 0755); + } +#endif + } while (pos != std::string::npos); +} + +struct FileCache { + std::map files; + ~FileCache() { + for (auto& p : files) { + fclose(p.second); + } + } + + FILE* get(const std::string& name) { + auto it = files.find(name); + if (it != files.end()) return it->second; + auto p = name.find_last_of(split); + std::string path = name.substr(0, p); + make_dir(path.c_str()); + + FILE* file = fopen(name.c_str(), "w"); + if (file) { + files[name] = file; + return file; + } + return nullptr; + } +}; + + +class DirectoryLogger : public Logger { +public: + DirectoryLogger(std::string path, LogStream::DirMode dir_mode[3]) : dirpath(path) { + make_dir(path.c_str()); + for (int i = 0; i < 3; i++) { + this->dir_mode[i] = dir_mode[i]; + } + } + ~DirectoryLogger() {} + + void flush(DataBlock* msg) override { + FileCache cache; + for (auto p = msg->begin(); p < msg->end(); p = moveBytes(p, p->info->size)) { + auto ss = log_custom_callback(*p, false); + + std::stringstream path; + path << dirpath; + if (dirpath.back() != split) path << split; + + int last = 0; + for (int i = 0; i < 3; i++) { + if (last != 0 && dir_mode[i] != 0) path << split; + switch (dir_mode[i]) { + case LogStream::DAILY_FILE: { + std::time_t t = std::chrono::system_clock::to_time_t(p->time); + std::tm tm = *std::localtime(&t); + path << std::put_time(&tm, "%Y-%m-%d"); + break; + } + case LogStream::SPLIT_BY_SEVERITY: { + path << to_string(p->info->severity); + break; + } + case LogStream::SPLIT_BY_CATEGORY: { + path << to_category(p->info->category); + break; + } + default: continue; + } + last = 1; + } + std::cerr << path.str() << std::endl; + + FILE* file = cache.get(path.str()); + fwrite(ss.c_str(), ss.size(), 1, file); + } + } + +protected: + std::string to_string(LogSeverity severity) { + switch (severity) { + case INFO_l: return "INFO"; + case LOG_l: return "LOG"; + case WARN_l: return "WARN"; + case ERROR_l: return "ERROR"; + case FATAL_l: return "FATAL"; + } + return ""; + } + + std::string to_category(const char* category) { + std::string cat = category; + for (auto& c : cat) { + if (c == '/') c = split; + } + return cat; + } + + LogStream::DirMode dir_mode[3]; + std::string dirpath; +}; + class OStreamLogger : public Logger { public: OStreamLogger(std::ostream& os) : os(os) {} @@ -153,9 +374,16 @@ LogStream& LogStream::getDefault() { return stream; } -void LogStream::setFileLogger(std::string name) { +void LogStream::setFileLogger(std::string name, DirMode mode1, DirMode mode2, DirMode mode3) { if (logger) delete logger; - logger = new FileLogger(name); + + if (mode1 == 0 || mode2 == 0 || mode3 == 0) + logger = new FileLogger(name); + else { + LogStream::DirMode dir_mode_group[3] = {mode1, mode2, mode3}; + + logger = new DirectoryLogger(name, dir_mode_group); + } } void LogStream::setStdoutLogger() { @@ -194,15 +422,15 @@ void setLogCategory(const char* categories) { } } -static LogStream::FlushMode saved_flush_mode; +static LogStream::FlushMode saved_flush_mode = LogStream::FlushMode::FLUSH_AT_ONCE; void suspendLog() { - saved_flush_mode = LogStream::getDefault().flush_mode; - LogStream::getDefault().flush_mode = LogStream::FLUSH_MANUALLY; + saved_flush_mode = LogStream::getDefault().getFlushMode(); + LogStream::getDefault().setFlushMode(LogStream::FLUSH_MANUALLY); } void resumeLog() { - LogStream::getDefault().flush_mode = saved_flush_mode; + LogStream::getDefault().setFlushMode(saved_flush_mode); LogStream::getDefault().flush(); } diff --git a/src/table.cpp b/src/table.cpp index f7af7e0e..45ad2297 100644 --- a/src/table.cpp +++ b/src/table.cpp @@ -271,7 +271,7 @@ inline std::string _rept(unsigned k, std::string j, Table::Style&) { } #define rep(k, t) _rept(k, t, s) -#define remain(k) (col_width[i] - k.size()) +#define remain(k) (col_width[i] - static_cast(k.size())) std::string Table::str(Config c, Table::Style s) { std::stringstream ss; @@ -282,12 +282,12 @@ std::string Table::str(Config c, Table::Style s) { if (col_width.size() == 0) { for (size_t i = 0; i < header.size(); ++i) { - unsigned max_width = 0; + size_t max_width = 0; for (auto& row : cells) { - max_width = std::max(row[i].size(), max_width); + max_width = std::max(row[i].size(), max_width); } - max_width = std::max(max_width, header[i].size()); - col_width.push_back(max_width); + max_width = std::max(max_width, header[i].size()); + col_width.push_back(static_cast(max_width)); } } diff --git a/src/unittest.cpp b/src/unittest.cpp index d7828475..34dd9eb7 100644 --- a/src/unittest.cpp +++ b/src/unittest.cpp @@ -3,6 +3,7 @@ #include "zeroerr/color.h" #include "zeroerr/fuzztest.h" #include "zeroerr/internal/threadsafe.h" +#include "zeroerr/log.h" #include #include @@ -87,7 +88,7 @@ void SubCase::operator<<(std::function op) { try { op(&local); } catch (const AssertionData&) { - } catch (const FuzzFinishedException& e) { + } catch (const FuzzFinishedException&) { } catch (const std::exception& e) { std::cerr << e.what() << std::endl; if (local.failed_as == 0) { @@ -100,13 +101,13 @@ void SubCase::operator<<(std::function op) { context->reporter.subCaseEnd(*this, new_buf, local, type); } -struct Filiters { +struct Filters { std::vector name, name_exclude; std::vector file, file_exclude; }; UnitTest& UnitTest::parseArgs(int argc, const char** argv) { - filiters = new Filiters(); + filters = new Filters(); auto convert_to_vec = [=]() { std::vector result; for (int i = 1; i < argc; i++) { @@ -161,24 +162,31 @@ UnitTest& UnitTest::parseArgs(int argc, const char** argv) { if (arg == "list-test-cases") { this->list_test_cases = true; } + if (arg == "no-color") { + this->no_color = true; + disableColorOutput(); + } + if (arg == "log-to-report") { + this->log_to_report = true; + } if (arg.substr(0, 9) == "reporters") { this->reporter_name = arg.substr(10); return true; } if (arg.substr(0, 8) == "testcase") { - filiters->name.push_back(std::regex(arg.substr(9))); + filters->name.push_back(std::regex(arg.substr(9))); return true; } if (arg.substr(0, 14) == "testcase-exclude") { - filiters->name_exclude.push_back(std::regex(arg.substr(15))); + filters->name_exclude.push_back(std::regex(arg.substr(15))); return true; } if (arg.substr(0, 5) == "file") { - filiters->file.push_back(std::regex(arg.substr(6))); + filters->file.push_back(std::regex(arg.substr(6))); return true; } if (arg.substr(0, 11) == "file-exclude") { - filiters->file_exclude.push_back(std::regex(arg.substr(12))); + filters->file_exclude.push_back(std::regex(arg.substr(12))); return true; } return false; @@ -214,15 +222,15 @@ static std::string insertIndentation(std::string str) { return result.str(); } -bool UnitTest::run_filiter(const TestCase& tc) { - if (filiters == nullptr) return true; - for (auto& r : filiters->name) +bool UnitTest::run_filter(const TestCase& tc) { + if (filters == nullptr) return true; + for (auto& r : filters->name) if (!std::regex_match(tc.name, r)) return false; - for (auto& r : filiters->name_exclude) + for (auto& r : filters->name_exclude) if (std::regex_match(tc.name, r)) return false; - for (auto& r : filiters->file) + for (auto& r : filters->file) if (!std::regex_match(tc.file, r)) return false; - for (auto& r : filiters->file_exclude) + for (auto& r : filters->file_exclude) if (std::regex_match(tc.file, r)) return false; return true; } @@ -230,6 +238,7 @@ bool UnitTest::run_filiter(const TestCase& tc) { int UnitTest::run() { IReporter* reporter = IReporter::create(reporter_name, *this); if (!reporter) reporter = IReporter::create("console", *this); + TestContext context(*reporter), sum(*reporter); reporter->testStart(); std::stringbuf new_buf; @@ -237,10 +246,10 @@ int UnitTest::run() { unsigned types = TestType::test_case; if (run_bench) types |= TestType::bench; if (run_fuzz) types |= TestType::fuzz_test; - std::set testcases = detail::getRegisteredTests(types); + std::set test_cases = detail::getRegisteredTests(types); - for (auto& tc : testcases) { - if (!run_filiter(tc)) continue; + for (auto& tc : test_cases) { + if (!run_filter(tc)) continue; reporter->testCaseStart(tc, new_buf); if (!list_test_cases) { std::streambuf* orig_buf = std::cerr.rdbuf(); @@ -249,7 +258,7 @@ int UnitTest::run() { try { tc.func(&context); // run the test case } catch (const AssertionData&) { - } catch (const FuzzFinishedException& e) { + } catch (const FuzzFinishedException&) { } catch (const std::exception& e) { std::cerr << e.what() << std::endl; if (context.failed_as == 0) { @@ -283,6 +292,7 @@ std::set& getTestSet(TestType type) { case TestType::fuzz_test: return fuzz_set; case TestType::sub_case: return test_set; } + throw std::runtime_error("Invalid test type"); } static std::set getRegisteredTests(unsigned type) { @@ -362,9 +372,9 @@ namespace detail { class XmlEncode { public: enum ForWhat { ForTextNodes, ForAttributes }; - XmlEncode(std::string const& str, ForWhat forWhat = ForTextNodes); + XmlEncode(const std::string& str, ForWhat forWhat = ForTextNodes); void encodeTo(std::ostream& os) const; - friend std::ostream& operator<<(std::ostream& os, XmlEncode const& xmlEncode); + friend std::ostream& operator<<(std::ostream& os, const XmlEncode& xmlEncode); private: std::string m_str; @@ -380,10 +390,10 @@ class XmlWriter { ScopedElement& operator=(ScopedElement&& other) noexcept; ~ScopedElement(); - ScopedElement& writeText(std::string const& text, bool indent = true); + ScopedElement& writeText(const std::string& text, bool indent = true, bool new_line = true); template - ScopedElement& writeAttribute(std::string const& name, T const& attribute) { + ScopedElement& writeAttribute(const std::string& name, const T& attribute) { m_writer->writeAttribute(name, attribute); return *this; } @@ -395,27 +405,27 @@ class XmlWriter { XmlWriter(std::ostream& os = std::cerr); ~XmlWriter(); - XmlWriter(XmlWriter const&) = delete; - XmlWriter& operator=(XmlWriter const&) = delete; + XmlWriter(const XmlWriter&) = delete; + XmlWriter& operator=(const XmlWriter&) = delete; - XmlWriter& startElement(std::string const& name); - ScopedElement scopedElement(std::string const& name); + XmlWriter& startElement(const std::string& name); + ScopedElement scopedElement(const std::string& 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); + XmlWriter& writeAttribute(const std::string& name, const std::string& attribute); + XmlWriter& writeAttribute(const std::string& name, const char* attribute); + XmlWriter& writeAttribute(const std::string& name, bool attribute); template - XmlWriter& writeAttribute(std::string const& name, T const& attribute) { + XmlWriter& writeAttribute(const std::string& name, const T& attribute) { std::stringstream rss; rss << attribute; return writeAttribute(name, rss.str()); } - XmlWriter& writeText(std::string const& text, bool indent = true); + XmlWriter& writeText(const std::string& text, bool indent = true, bool new_line = true); - void ensureTagClosed(); + void ensureTagClosed(bool new_line = true); void writeDeclaration(); private: @@ -423,6 +433,7 @@ class XmlWriter { bool m_tagIsOpen = false; bool m_needsNewline = false; + bool m_needsIndent = false; std::vector m_tags; std::string m_indent; std::ostream& m_os; @@ -463,7 +474,7 @@ static void hexEscapeChar(std::ostream& os, unsigned char c) { os.flags(f); } -XmlEncode::XmlEncode(std::string const& str, ForWhat forWhat) : m_str(str), m_forWhat(forWhat) {} +XmlEncode::XmlEncode(const std::string& 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 @@ -556,7 +567,7 @@ void XmlEncode::encodeTo(std::ostream& os) const { } } -std::ostream& operator<<(std::ostream& os, XmlEncode const& xmlEncode) { +std::ostream& operator<<(std::ostream& os, const XmlEncode& xmlEncode) { xmlEncode.encodeTo(os); return os; } @@ -580,9 +591,9 @@ 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); +XmlWriter::ScopedElement& XmlWriter::ScopedElement::writeText(const std::string& text, + bool indent, bool new_line) { + m_writer->writeText(text, indent, new_line); return *this; } @@ -592,7 +603,7 @@ XmlWriter::~XmlWriter() { while (!m_tags.empty()) endElement(); } -XmlWriter& XmlWriter::startElement(std::string const& name) { +XmlWriter& XmlWriter::startElement(const std::string& name) { ensureTagClosed(); newlineIfNecessary(); m_os << m_indent << '<' << name; @@ -602,7 +613,7 @@ XmlWriter& XmlWriter::startElement(std::string const& name) { return *this; } -XmlWriter::ScopedElement XmlWriter::scopedElement(std::string const& name) { +XmlWriter::ScopedElement XmlWriter::scopedElement(const std::string& name) { ScopedElement scoped(this); startElement(name); return scoped; @@ -615,44 +626,48 @@ XmlWriter& XmlWriter::endElement() { m_os << "/>"; m_tagIsOpen = false; } else { - m_os << m_indent << ""; + if (m_needsIndent) m_os << m_indent; + else m_needsIndent = true; + m_os << ""; } m_os << std::endl; m_tags.pop_back(); return *this; } -XmlWriter& XmlWriter::writeAttribute(std::string const& name, std::string const& attribute) { +XmlWriter& XmlWriter::writeAttribute(const std::string& name, const std::string& 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) { +XmlWriter& XmlWriter::writeAttribute(const std::string& 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) { +XmlWriter& XmlWriter::writeAttribute(const std::string& name, bool attribute) { m_os << ' ' << name << "=\"" << (attribute ? "true" : "false") << '"'; return *this; } -XmlWriter& XmlWriter::writeText(std::string const& text, bool indent) { +XmlWriter& XmlWriter::writeText(const std::string& text, bool indent, bool new_line) { if (!text.empty()) { bool tagWasOpen = m_tagIsOpen; - ensureTagClosed(); + ensureTagClosed(new_line); if (tagWasOpen && indent) m_os << m_indent; m_os << XmlEncode(text); - m_needsNewline = true; + m_needsNewline = new_line; + m_needsIndent = new_line; } return *this; } -void XmlWriter::ensureTagClosed() { +void XmlWriter::ensureTagClosed(bool new_line) { if (m_tagIsOpen) { - m_os << ">" << std::endl; + m_os << ">"; + if (new_line) m_os << std::endl; m_tagIsOpen = false; } } @@ -675,74 +690,89 @@ class XmlReporter : public IReporter { public: detail::XmlWriter xml; - struct TestCaseData { - struct TestCase { - std::string filename, name; - unsigned line; - double time; - TestContext context; - }; + struct TestCaseTemp { + const TestCase* tc; + }; - std::vector testcases; - double total_time; - } tc_data; - std::vector current; + std::vector current; virtual std::string getName() const override { return "xml"; } // There are a list of events - virtual void testStart() override { xml.writeDeclaration(); } + virtual void testStart() override { + xml.writeDeclaration(); + xml.startElement("ZeroErr") + .writeAttribute("binary", ut.binary) + .writeAttribute("version", ZEROERR_VERSION_STR); + xml.startElement("TestSuite"); + } virtual void testCaseStart(const TestCase& tc, std::stringbuf&) override { - current.push_back(&tc); + current.push_back({&tc}); + xml.startElement("TestCase") + .writeAttribute("name", tc.name) + .writeAttribute("filename", tc.file) + .writeAttribute("line", tc.line) + .writeAttribute("skipped", "false"); + if (ut.log_to_report) suspendLog(); } - virtual void testCaseEnd(const TestCase& tc, std::stringbuf&, const TestContext& ctx, + virtual void testCaseEnd(const TestCase& tc, std::stringbuf& sb, const TestContext& ctx, int) override { - tc_data.testcases.push_back({tc.file, tc.name, tc.line, 0.0, ctx}); current.pop_back(); + xml.scopedElement("Result") + .writeAttribute("time", 0) + .writeAttribute("passed", ctx.passed) + .writeAttribute("warnings", ctx.warning) + .writeAttribute("failed", ctx.failed) + .writeAttribute("skipped", ctx.skipped); + xml.scopedElement("ResultAsserts") + .writeAttribute("passed", ctx.passed_as) + .writeAttribute("warnings", ctx.warning_as) + .writeAttribute("failed", ctx.failed_as) + .writeAttribute("skipped", ctx.skipped_as); + xml.scopedElement("Output").writeText(sb.str()); + + if (ut.log_to_report) { + xml.startElement("Log"); + LogIterator begin = LogStream::getDefault().begin(); + LogIterator end = LogStream::getDefault().end(); + for (auto p = begin; p != end; ++p) { + xml.startElement("LogEntry") + .writeAttribute("function", p->info->function) + .writeAttribute("line", p->info->line) + .writeAttribute("message", p->info->message) + .writeAttribute("category", p->info->category) + .writeAttribute("severity", p->info->severity); + for (auto pair : p->getData()) { + xml.scopedElement(pair.first).writeText(pair.second, false, false); + } + xml.endElement(); + } + xml.endElement(); + resumeLog(); + } + xml.endElement(); } - virtual void subCaseStart(const TestCase& tc, std::stringbuf&) override { - current.push_back(&tc); + virtual void subCaseStart(const TestCase& tc, std::stringbuf& sb) override { + testCaseStart(tc, sb); } - virtual void subCaseEnd(const TestCase& tc, std::stringbuf&, const TestContext& ctx, + virtual void subCaseEnd(const TestCase& tc, std::stringbuf& sb, const TestContext& ctx, int) override { - tc_data.testcases.push_back({tc.file, tc.name, tc.line, 0.0, ctx}); - current.pop_back(); + testCaseEnd(tc, sb, ctx, 0); } virtual void testEnd(const TestContext& tc) override { - xml.startElement("ZeroErr") - .writeAttribute("binary", ut.binary) - .writeAttribute("version", ZEROERR_VERSION_STR); + xml.endElement(); + xml.startElement("OverallResults") .writeAttribute("errors", tc.failed_as) .writeAttribute("failures", tc.failed) .writeAttribute("tests", tc.passed + tc.failed + tc.warning); xml.endElement(); - xml.startElement("TestSuite"); - for (const auto& testCase : tc_data.testcases) { - xml.startElement("TestCase") - .writeAttribute("name", testCase.name) - .writeAttribute("filename", testCase.filename) - .writeAttribute("line", testCase.line) - .writeAttribute("skipped", "false"); - xml.scopedElement("Result") - .writeAttribute("time", testCase.time) - .writeAttribute("passed", testCase.context.passed) - .writeAttribute("warnings", testCase.context.warning) - .writeAttribute("failed", testCase.context.failed) - .writeAttribute("skipped", testCase.context.skipped); - xml.scopedElement("ResultAsserts") - .writeAttribute("passed", testCase.context.passed_as) - .writeAttribute("warnings", testCase.context.warning_as) - .writeAttribute("failed", testCase.context.failed_as) - .writeAttribute("skipped", testCase.context.skipped_as); - xml.endElement(); - } - xml.endElement(); + xml.endElement(); } @@ -762,5 +792,4 @@ IReporter* IReporter::create(const std::string& name, UnitTest& ut) { int main(int argc, const char** argv) { zeroerr::UnitTest().parseArgs(argc, argv).run(); std::_Exit(0); - return 0; } diff --git a/test/fuzz_test.cpp b/test/fuzz_test.cpp index 0d27c15b..f95c9d9f 100644 --- a/test/fuzz_test.cpp +++ b/test/fuzz_test.cpp @@ -36,7 +36,7 @@ FUZZ_TEST_CASE("fuzz_test2") { FUZZ_TEST_CASE("fuzz_test3") { LOG("Run fuzz_test3"); FUZZ_FUNC([=](int k, std::vector num) { - int t = num.size(); + int t = static_cast(num.size()); LOG("k: {k}, t: {t}", k, t); }) .WithDomains(InRange(0, 10), ContainerOf(Arbitrary())) @@ -46,7 +46,7 @@ FUZZ_TEST_CASE("fuzz_test3") { FUZZ_TEST_CASE("fuzz_test4") { LOG("Run fuzz_test4"); FUZZ_FUNC([=](int k, std::map num) { - int t = num.size(); + int t = static_cast(num.size()); LOG("k: {k}, t: {t}", k, t); }) .WithDomains(InRange(0, 10), ContainerOf>( @@ -58,7 +58,7 @@ FUZZ_TEST_CASE("fuzz_test4") { FUZZ_TEST_CASE("fuzz_test4") { LOG("Run fuzz_test4"); FUZZ_FUNC([=](int k, std::map num) { - int t = num.size(); + int t = static_cast(num.size()); LOG("k: {k}, t: {t}", k, t); }) .WithDomains(InRange(0, 10), ContainerOf>( diff --git a/test/log_test.cpp b/test/log_test.cpp index 1207dabc..ee97fb45 100644 --- a/test/log_test.cpp +++ b/test/log_test.cpp @@ -38,7 +38,7 @@ TEST_CASE("lazy evaluation") { } } #ifdef ZEROERR_ENABLE_SPEED_TEST -BENCHMARK("speed test") { +BENCHMARK("speedtest") { #ifdef ZEROERR_OS_UNIX uint64_t* data = new uint64_t[1000000]; FILE* file = fmemopen(data, 1000000 * sizeof(uint64_t), "w"); @@ -46,19 +46,22 @@ BENCHMARK("speed test") { FILE* oldstderr = stderr; stdout = stderr = file; #endif - Benchmark bench("log speed test"); - bench + zeroerr::LogStream::getDefault().setFlushManually(); + std::stringstream ss; + Benchmark bench("log speed test"); + bench.run("spdlog", [] { spdlog::info("hello world {:03.2f}", 1.1); }) .run("stringstream", - [] { - std::stringstream ss; + [&] { ss << "hello world " << 1.1; doNotOptimizeAway(ss); }) .run("log", [] { LOG("hello world {value}", 1.1); }) - .run("spdlog", [] { spdlog::info("hello world {:03.2f}", 1.1); }) .report(); + zeroerr::LogStream::getDefault().setFlushAtOnce(); + #ifdef ZEROERR_OS_UNIX - stdout = oldstdout; stderr = oldstderr; + stdout = oldstdout; + stderr = oldstderr; fclose(file); delete[] data; #endif @@ -86,7 +89,7 @@ static void test_feature(int k) { TEST_CASE("cross function info") { int k = 0; INFO("k = ", k); - + test_feature(k); k = 1; test_feature(k); @@ -98,7 +101,7 @@ TEST_CASE("cross function info") { TEST_CASE("debug log") { int sum = 0; DLOG(LOG_FIRST, sum < 5, "debug log i = {i}", 1); - DLOG(WARN_IF,sum < 5, "debug log i = {i}, sum = {sum}", 2, sum); + DLOG(WARN_IF, sum < 5, "debug log i = {i}, sum = {sum}", 2, sum); } TEST_CASE("log to file") { @@ -123,12 +126,68 @@ static void function() { TEST_CASE("access log in Test case") { zeroerr::suspendLog(); function(); - std::cerr << LOG_GET(function, 119, i, int) << std::endl; - std::cerr << LOG_GET(function, 120, sum, int) << std::endl; - std::cerr << LOG_GET(function, 120, i, int) << std::endl; + std::cerr << LOG_GET(function, 122, i, int) << std::endl; + std::cerr << LOG_GET(function, 123, sum, int) << std::endl; + std::cerr << LOG_GET(function, 123, i, int) << std::endl; - CHECK(LOG_GET(function, 119, i, int) == 1); - CHECK(LOG_GET(function, 120, sum, int) == 9); - CHECK(LOG_GET(function, 120, i, int) == 2); + CHECK(LOG_GET(function, 122, i, int) == 1); + CHECK(LOG_GET(function, 123, sum, int) == 9); + CHECK(LOG_GET(function, 123, i, int) == 2); zeroerr::resumeLog(); +} + +TEST_CASE("access log in Test case") { + zeroerr::suspendLog(); + function(); + std::cerr << LOG_GET(function, "function log {i}", i, int) << std::endl; + std::cerr << LOG_GET(function, "function log {sum}, {i}", sum, int) << std::endl; + std::cerr << LOG_GET(function, "function log {sum}, {i}", i, int) << std::endl; + + 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); + zeroerr::resumeLog(); +} + +TEST_CASE("iterate log stream") { + zeroerr::suspendLog(); + function(); + function(); + function(); + + auto& stream = zeroerr::LogStream::getDefault(); + for (auto p = stream.begin(); p != stream.end(); ++p) { + if (p->info->function == std::string("function") && p->info->line == 122) { + std::cerr << "p.get(\"i\") = " << p.get("i") << std::endl; + CHECK(p.get("i") == 1); + } + } + + 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(); +} + + +TEST_CASE("multiple log stream") { + zeroerr::LogStream stream1, stream2; + stream1.setFileLogger("log1.txt"); + stream2.setFileLogger("log2.txt"); + + LOG("log stream {i}", stream1, 1); + LOG("log stream {i}", stream2, 2); +} + +TEST_CASE("log to dir") { + 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(); } \ No newline at end of file diff --git a/test/unit_test.cpp b/test/unit_test.cpp index 61fe81ff..df999fc6 100644 --- a/test/unit_test.cpp +++ b/test/unit_test.cpp @@ -66,6 +66,21 @@ TEST_CASE("assert in if") { } } +int f() { + throw std::runtime_error("error"); + return 0; +} + +int g() { + return 0; +} + + +TEST_CASE("check exception") { + REQUIRE_THROWS(f()); + CHECK_THROWS(g()); +} + class test_fixture { public: diff --git a/zeroerr.hpp b/zeroerr.hpp index 189e2686..d05d9e25 100644 --- a/zeroerr.hpp +++ b/zeroerr.hpp @@ -4,8 +4,8 @@ #pragma once #define ZEROERR_VERSION_MAJOR 0 -#define ZEROERR_VERSION_MINOR 2 -#define ZEROERR_VERSION_PATCH 1 +#define ZEROERR_VERSION_MINOR 3 +#define ZEROERR_VERSION_PATCH 0 #define ZEROERR_VERSION \ (ZEROERR_VERSION_MAJOR * 10000 + ZEROERR_VERSION_MINOR * 100 + ZEROERR_VERSION_PATCH) @@ -679,11 +679,14 @@ __attribute__((always_inline)) __inline__ static bool isDebuggerActive() { retur #define ZEROERR_MUTEX(x) #define ZEROERR_LOCK(x) #define ZEROERR_ATOMIC(x) x +#define ZEROERR_LOAD(x) x + #else #define ZEROERR_MUTEX(x) static std::mutex x; #define ZEROERR_LOCK(x) std::lock_guard lock(x); #define ZEROERR_ATOMIC(x) std::atomic +#define ZEROERR_LOAD(x) x.load() #include #include @@ -824,6 +827,35 @@ struct ele_type_is_pair< T, void_t().first), decltype(std::declval().second)>> : std::true_type {}; +template +struct to_store_type { + using type = T; +}; + +template <> +struct to_store_type { + using type = std::string; +}; + +template <> +struct to_store_type { + using type = std::string; +}; + + +template +struct to_store_type::value>::type> { + using type = T; +}; + +template +struct to_store_type { + using type = T; +}; + +template +using to_store_type_t = typename to_store_type::type; + template struct visit_impl { @@ -1423,7 +1455,11 @@ struct Printer { #endif } - std::string str() const { return static_cast(os).str(); } + std::string str() const { + if (use_stringstream == false) + throw std::runtime_error("Printer is not using stringstream"); + return static_cast(os).str(); + } operator std::string() const { return str(); } friend std::ostream& operator<<(std::ostream& os, const Printer& P) { @@ -2739,13 +2775,17 @@ ZEROERR_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wgnu-zero-variadic-macro-arguments") ZEROERR_CLANG_SUPPRESS_WARNING_POP #endif -#define ZEROERR_ASSERT_EXP(cond, level, throws, is_false, ...) \ +#define ZEROERR_ASSERT_EXP(cond, level, expect_throw, is_false, ...) \ ZEROERR_FUNC_SCOPE_BEGIN { \ zeroerr::assert_info info{zeroerr::assert_level::ZEROERR_CAT(level, _l), \ - zeroerr::assert_throw::throws, is_false}; \ + zeroerr::assert_throw::expect_throw, is_false}; \ \ zeroerr::AssertionData assertion_data(__FILE__, __LINE__, #cond, info); \ - assertion_data.setResult(zeroerr::ExpressionDecomposer() << cond); \ + try { \ + assertion_data.setResult(zeroerr::ExpressionDecomposer() << cond); \ + } catch (const std::exception& e) { \ + assertion_data.setException(e); \ + } \ zeroerr::detail::context_helper< \ decltype(_ZEROERR_TEST_CONTEXT), \ std::is_same, rhs, ZEROERR_WARN, no_throw, false, __VA_ARGS__) #define ZEROERR_CHECK_GE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >=, rhs, ZEROERR_WARN, no_throw, false, __VA_ARGS__) -#define ZEROERR_REQUIRE_EQ(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, ==, rhs, ZEROERR_ERROR, throws, false, __VA_ARGS__) -#define ZEROERR_REQUIRE_NE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, !=, rhs, ZEROERR_ERROR, throws, false, __VA_ARGS__) -#define ZEROERR_REQUIRE_LT(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, <, rhs, ZEROERR_ERROR, throws, false, __VA_ARGS__) -#define ZEROERR_REQUIRE_LE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, <=, rhs, ZEROERR_ERROR, throws, false, __VA_ARGS__) -#define ZEROERR_REQUIRE_GT(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >, rhs, ZEROERR_ERROR, throws, false, __VA_ARGS__) -#define ZEROERR_REQUIRE_GE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >=, rhs, ZEROERR_ERROR, throws, false, __VA_ARGS__) +#define ZEROERR_REQUIRE_EQ(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, ==, rhs, ZEROERR_ERROR, no_throw, false, __VA_ARGS__) +#define ZEROERR_REQUIRE_NE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, !=, rhs, ZEROERR_ERROR, no_throw, false, __VA_ARGS__) +#define ZEROERR_REQUIRE_LT(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, <, rhs, ZEROERR_ERROR, no_throw, false, __VA_ARGS__) +#define ZEROERR_REQUIRE_LE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, <=, rhs, ZEROERR_ERROR, no_throw, false, __VA_ARGS__) +#define ZEROERR_REQUIRE_GT(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >, rhs, ZEROERR_ERROR, no_throw, false, __VA_ARGS__) +#define ZEROERR_REQUIRE_GE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >=, rhs, ZEROERR_ERROR, no_throw, false, __VA_ARGS__) -#define ZEROERR_ASSERT_EQ(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, ==, rhs, ZEROERR_FATAL, throws, false, __VA_ARGS__) -#define ZEROERR_ASSERT_NE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, !=, rhs, ZEROERR_FATAL, throws, false, __VA_ARGS__) -#define ZEROERR_ASSERT_LT(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, <, rhs, ZEROERR_FATAL, throws, false, __VA_ARGS__) -#define ZEROERR_ASSERT_LE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, <=, rhs, ZEROERR_FATAL, throws, false, __VA_ARGS__) -#define ZEROERR_ASSERT_GT(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >, rhs, ZEROERR_FATAL, throws, false, __VA_ARGS__) -#define ZEROERR_ASSERT_GE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >=, rhs, ZEROERR_FATAL, throws, false, __VA_ARGS__) +#define ZEROERR_ASSERT_EQ(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, ==, rhs, ZEROERR_FATAL, no_throw, false, __VA_ARGS__) +#define ZEROERR_ASSERT_NE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, !=, rhs, ZEROERR_FATAL, no_throw, false, __VA_ARGS__) +#define ZEROERR_ASSERT_LT(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, <, rhs, ZEROERR_FATAL, no_throw, false, __VA_ARGS__) +#define ZEROERR_ASSERT_LE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, <=, rhs, ZEROERR_FATAL, no_throw, false, __VA_ARGS__) +#define ZEROERR_ASSERT_GT(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >, rhs, ZEROERR_FATAL, no_throw, false, __VA_ARGS__) +#define ZEROERR_ASSERT_GE(lhs, rhs, ...) ZEROERR_ASSERT_CMP(lhs, >=, rhs, ZEROERR_FATAL, no_throw, false, __VA_ARGS__) #define CHECK_EQ(...) ZEROERR_SUPPRESS_VARIADIC_MACRO ZEROERR_EXPAND(ZEROERR_CHECK_EQ(__VA_ARGS__)) ZEROERR_SUPPRESS_VARIADIC_MACRO_POP #define CHECK_NE(...) ZEROERR_SUPPRESS_VARIADIC_MACRO ZEROERR_EXPAND(ZEROERR_CHECK_NE(__VA_ARGS__)) ZEROERR_SUPPRESS_VARIADIC_MACRO_POP @@ -2910,9 +2963,9 @@ struct AssertionData : std::exception { AssertionData(const char* file, unsigned line, const char* cond, assert_info info) : file(file), line(line), info(info) { - std::string cond_str(cond); - std::string pattern = "zeroerr::ExpressionDecomposer() << "; - size_t pos = cond_str.find(pattern); + static std::string pattern = "zeroerr::ExpressionDecomposer() << "; + std::string cond_str(cond); + size_t pos = cond_str.find(pattern); if (pos != std::string::npos) cond_str.replace(pos, pos + pattern.size(), ""); this->cond = cond_str; } @@ -2926,6 +2979,11 @@ struct AssertionData : std::exception { message = r.decomp; } + void setException(const std::exception& e) { + passed = info.throw_type == assert_throw::throws; + message = e.what(); + } + std::string log() { std::stringstream ss; ss << " " << cond << " expands to " << message << std::endl; @@ -2933,14 +2991,13 @@ struct AssertionData : std::exception { return ss.str(); } - // throw the exception void operator()() { if (passed) return; if (shouldThrow()) throw *this; } - bool shouldThrow() { return info.throw_type == assert_throw::throws; } + bool shouldThrow() { return info.level != assert_level::ZEROERR_WARN_l; } }; namespace detail { @@ -3090,15 +3147,11 @@ std::string format(const char* fmt, T... args) { + + #include -#include -#include -#include -#include #include -#include #include -#include #include ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH @@ -3111,12 +3164,13 @@ class mutex; namespace zeroerr { - +// clang-format off #define ZEROERR_INFO(...) ZEROERR_SUPPRESS_VARIADIC_MACRO ZEROERR_EXPAND(ZEROERR_INFO_(__VA_ARGS__)) ZEROERR_SUPPRESS_VARIADIC_MACRO_POP #define ZEROERR_LOG(...) ZEROERR_SUPPRESS_VARIADIC_MACRO ZEROERR_EXPAND(ZEROERR_LOG_(LOG_l, __VA_ARGS__)) ZEROERR_SUPPRESS_VARIADIC_MACRO_POP #define ZEROERR_WARN(...) ZEROERR_SUPPRESS_VARIADIC_MACRO ZEROERR_EXPAND(ZEROERR_LOG_(WARN_l, __VA_ARGS__)) ZEROERR_SUPPRESS_VARIADIC_MACRO_POP #define ZEROERR_ERROR(...) ZEROERR_SUPPRESS_VARIADIC_MACRO ZEROERR_EXPAND(ZEROERR_LOG_(ERROR_l, __VA_ARGS__)) ZEROERR_SUPPRESS_VARIADIC_MACRO_POP #define ZEROERR_FATAL(...) ZEROERR_SUPPRESS_VARIADIC_MACRO ZEROERR_EXPAND(ZEROERR_LOG_(FATAL_l, __VA_ARGS__)) ZEROERR_SUPPRESS_VARIADIC_MACRO_POP +// clang-format on #ifdef ZEROERR_USE_SHORT_LOG_MACRO @@ -3151,7 +3205,9 @@ namespace zeroerr { #define FATAL(...) ZEROERR_FATAL(__VA_ARGS__) #define VERBOSE(v) ZEROERR_VERBOSE(v) -#endif +#define LOG_GET(func, id, name, type) ZEROERR_LOG_GET(func, id, name, type) + +#endif // ZEROERR_USE_SHORT_LOG_MACRO #define ZEROERR_LOG_IF(condition, ACTION, ...) \ do { \ @@ -3239,22 +3295,21 @@ extern int _ZEROERR_G_VERBOSE; #define ZEROERR_VERBOSE(v) if (zeroerr::_ZEROERR_G_VERBOSE >= (v)) -#define ZEROERR_LOG_(severity, message, ...) \ - do { \ - ZEROERR_G_CONTEXT_SCOPE(true); \ - auto msg = zeroerr::LogStream::getDefault().push(__VA_ARGS__); \ - \ - static zeroerr::LogInfo log_info{__FILE__, \ - __func__, \ - message, \ - ZEROERR_LOG_CATEGORY, \ - __LINE__, \ - msg.size, \ - zeroerr::LogSeverity::severity}; \ - msg.log->info = &log_info; \ - if (zeroerr::LogStream::getDefault().flush_mode == \ - zeroerr::LogStream::FlushMode::FLUSH_AT_ONCE) \ - zeroerr::LogStream::getDefault().flush(); \ +#define ZEROERR_LOG_(severity, message, ...) \ + do { \ + ZEROERR_G_CONTEXT_SCOPE(true); \ + auto msg = zeroerr::log(__VA_ARGS__); \ + \ + static zeroerr::LogInfo log_info{__FILE__, \ + __func__, \ + message, \ + ZEROERR_LOG_CATEGORY, \ + __LINE__, \ + msg.size, \ + zeroerr::LogSeverity::severity}; \ + msg.log->info = &log_info; \ + if (msg.stream.getFlushMode() == zeroerr::LogStream::FlushMode::FLUSH_AT_ONCE) \ + msg.stream.flush(); \ } while (0) #define ZEROERR_INFO_(...) \ @@ -3287,8 +3342,8 @@ extern int _ZEROERR_G_VERBOSE; // This macro can access the log in memory -#define LOG_GET(func, line, name, type) \ - zeroerr::LogStream::getDefault().getLog(#func, line, #name) +#define ZEROERR_LOG_GET(func, id, name, type) \ + zeroerr::LogStream::getDefault().getLog(#func, id, #name) namespace detail { @@ -3343,6 +3398,8 @@ struct LogMessage { virtual std::string str() const = 0; virtual void* getRawLog(std::string name) const = 0; + virtual std::map getData() const = 0; + // meta data of this log message const LogInfo* info; @@ -3351,17 +3408,8 @@ struct LogMessage { }; -// This is a helper class to get the raw pointer of the tuple -struct GetTuplePtr { - void* ptr = nullptr; - template - void operator()(H& v) { - ptr = (void*)&v; - } -}; - template -struct LogMessageImpl : LogMessage { +struct LogMessageImpl final : LogMessage { std::tuple args; LogMessageImpl(T... args) : LogMessage(), args(args...) {} @@ -3369,30 +3417,116 @@ struct LogMessageImpl : LogMessage { return gen_str(info->message, args, detail::gen_seq{}); } + // This is a helper class to get the raw pointer of the tuple + struct GetTuplePtr { + void* ptr = nullptr; + template + void operator()(H& v) { + ptr = (void*)&v; + } + }; + void* getRawLog(std::string name) const override { GetTuplePtr f; detail::visit_at(args, info->names.at(name), f); return f.ptr; } + + struct PrintTupleData { + std::map data; + Printer print; + std::string name; + + PrintTupleData() : print() { + print.isCompact = true; + print.line_break = ""; + } + + template + void operator()(H& v) { + data[name] = print(v); + } + }; + + std::map getData() const override { + PrintTupleData printer; + for (auto it = info->names.begin(); it != info->names.end(); ++it) { + printer.name = it->first; + detail::visit_at(args, it->second, printer); + } + return printer.data; + } }; struct DataBlock; +class LogStream; + class Logger { public: virtual ~Logger() = default; virtual void flush(DataBlock*) = 0; }; +struct PushResult { + LogMessage* log; + unsigned size; + LogStream& stream; +}; + +class LogIterator { +public: + LogIterator() : p(nullptr), q(nullptr) {} + LogIterator(LogStream& stream, std::string message = "", std::string function_name = "", + int line = -1); + LogIterator(const LogIterator& rhs) : p(rhs.p), q(rhs.q) {} + LogIterator& operator=(const LogIterator& rhs) { + p = rhs.p; + q = rhs.q; + return *this; + } + + LogIterator& operator++(); + LogIterator operator++(int) { + LogIterator tmp = *this; + ++*this; + return tmp; + } + + template + T get(std::string name) { + void* data = q->getRawLog(name); + if (data) return *(T*)(data); + return T{}; + } + + bool operator==(const LogIterator& rhs) const { return p == rhs.p && q == rhs.q; } + bool operator!=(const LogIterator& rhs) const { return !(*this == rhs); } + + LogMessage& get() const { return *q; } + LogMessage& operator*() const { return *q; } + LogMessage* operator->() const { return q; } + + void check_at_safe_pos(); + + friend class LogStream; + +protected: + bool check_filter(); + void next(); + + DataBlock* p; + LogMessage* q; + + std::string function_name_filter; + std::string message_filter; + int line_filter = -1; +}; + class LogStream { public: LogStream(); virtual ~LogStream(); - struct PushResult { - LogMessage* log; - unsigned size; - }; - enum FlushMode { FLUSH_AT_ONCE, FLUSH_WHEN_FULL, FLUSH_MANUALLY }; enum LogMode { ASYNC, SYNC }; enum DirMode { @@ -3404,10 +3538,16 @@ class LogStream { template PushResult push(T&&... args) { - unsigned size = sizeof(LogMessageImpl); - void* p = alloc_block(size); - LogMessage* msg = new (p) LogMessageImpl(std::forward(args)...); - return {msg, size}; + // unsigned size = sizeof(LogMessageImpl); + unsigned size = sizeof(LogMessageImpl...>); + void* p; + if (use_lock_free) + p = alloc_block_lockfree(size); + else + p = alloc_block(size); + // LogMessage* msg = new (p) LogMessageImpl(std::forward(args)...); + LogMessage* msg = new (p) LogMessageImpl...>(args...); + return {msg, size, *this}; } template @@ -3417,28 +3557,70 @@ class LogStream { return T{}; } + template + T getLog(std::string func, std::string msg, std::string name) { + void* data = getRawLog(func, msg, name); + if (data) return *(T*)(data); + return T{}; + } + void* getRawLog(std::string func, unsigned line, std::string name); + void* getRawLog(std::string func, std::string msg, std::string name); + + LogIterator begin(std::string message = "", std::string function_name = "", int line = -1) { + return LogIterator(*this, message, function_name, line); + } + LogIterator end() { return LogIterator(); } + LogIterator current(std::string message = "", std::string function_name = "", int line = -1); void flush(); - void setFileLogger(std::string name); + void setFileLogger(std::string name, DirMode mode1 = SINGLE_FILE, DirMode mode2 = SINGLE_FILE, + DirMode mode3 = SINGLE_FILE); void setStdoutLogger(); void setStderrLogger(); static LogStream& getDefault(); - FlushMode flush_mode = FLUSH_AT_ONCE; - LogMode log_mode = SYNC; - DirMode dir_mode = SINGLE_FILE; + + void setFlushAtOnce() { flush_mode = FLUSH_AT_ONCE; } + void setFlushWhenFull() { flush_mode = FLUSH_WHEN_FULL; } + void setFlushManually() { flush_mode = FLUSH_MANUALLY; } + void setAsyncLog() { log_mode = ASYNC; } + void setSyncLog() { log_mode = SYNC; } + + FlushMode getFlushMode() const { return flush_mode; } + void setFlushMode(FlushMode mode) { flush_mode = mode; } + LogMode getLogMode() const { return log_mode; } + void setLogMode(LogMode mode) { log_mode = mode; } + + bool use_lock_free = true; + + friend class LogIterator; private: - DataBlock *first, *last; - Logger* logger = nullptr; + DataBlock *first, *prepare; + ZEROERR_ATOMIC(DataBlock*) m_last; + Logger* logger = nullptr; + FlushMode flush_mode = FLUSH_AT_ONCE; + LogMode log_mode = SYNC; #ifndef ZEROERR_NO_THREAD_SAFE std::mutex* mutex; #endif void* alloc_block(unsigned size); + void* alloc_block_lockfree(unsigned size); }; +template +PushResult log(T&&... args) { + return LogStream::getDefault().push(std::forward(args)...); +} + +template +PushResult log(LogStream& stream, T&&... args) { + return stream.push(std::forward(args)...); +} + + /** * @brief ContextScope is a helper class created in each basic block where you use INFO(). * The context scope can has lazy evaluated function F(std::ostream&) that is called when the @@ -3677,15 +3859,17 @@ struct TestCase; struct UnitTest { UnitTest& parseArgs(int argc, const char** argv); int run(); - bool run_filiter(const TestCase& tc); + bool run_filter(const TestCase& tc); bool silent = false; bool run_bench = false; bool run_fuzz = false; bool list_test_cases = false; + bool no_color = false; + bool log_to_report = false; std::string correct_output_path; std::string reporter_name = "console"; std::string binary; - struct Filiters* filiters; + struct Filters* filters; }; struct TestCase { @@ -4279,6 +4463,15 @@ TerminalSize getTerminalWidth() { +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + const char* ZEROERR_LOG_CATEGORY = "default"; @@ -4301,33 +4494,37 @@ LogInfo::LogInfo(const char* filename, const char* function, const char* message category(category), line(line), size(size), - severity(severity) { + severity(severity), + names() { for (const char* p = message; *p; p++) if (*p == '{') { const char* q = p + 1; while (*q && *q != '}') q++; if (*q == '}') { - std::string N(p + 1, q); - names[N] = names.size(); + std::string N(p + 1, (size_t)(q-p-1)); + names[N] = static_cast(names.size()); p = q; } } } -constexpr size_t LogStreamMaxSize = 1024 * 1024 - 16; +constexpr size_t LogStreamMaxSize = 1 * 1024 - 16; struct DataBlock { - size_t size = 0; + ZEROERR_ATOMIC(size_t) size; DataBlock* next = nullptr; char data[LogStreamMaxSize]; + DataBlock() : size(0) {} + LogMessage* begin() { return (LogMessage*)data; } LogMessage* end() { return (LogMessage*)&data[size]; } }; LogStream::LogStream() { - first = last = new DataBlock(); + first = m_last = new DataBlock(); + prepare = new DataBlock(); #ifndef ZEROERR_NO_THREAD_SAFE mutex = new std::mutex(); #endif @@ -4352,6 +4549,7 @@ void* LogStream::alloc_block(unsigned size) { throw std::runtime_error("LogStream::push: size > LogStreamMaxSize"); } ZEROERR_LOCK(*mutex); + auto* last = ZEROERR_LOAD(m_last); if (last->size + size > LogStreamMaxSize) { if (flush_mode == FLUSH_WHEN_FULL) { logger->flush(last); @@ -4366,8 +4564,44 @@ void* LogStream::alloc_block(unsigned size) { return p; } +void* LogStream::alloc_block_lockfree(unsigned size) { +#ifndef ZEROERR_NO_THREAD_SAFE + if (size > LogStreamMaxSize) { + throw std::runtime_error("LogStream::push: size > LogStreamMaxSize"); + } + DataBlock* last = ZEROERR_LOAD(m_last); + while (true) { + size_t p = last->size.load(); + if (p <= (LogStreamMaxSize - size)) { + if (last->size.compare_exchange_strong(p, p + size)) return last->data + p; + } else { + if (m_last.compare_exchange_strong(last, prepare)) { + if (flush_mode == FLUSH_WHEN_FULL) { + logger->flush(last); + last->size = 0; + prepare = last; + } else { + prepare->next = last; + prepare = new DataBlock(); + } + } + } + } +#else + return alloc_block(size); +#endif +} +LogIterator LogStream::current(std::string message, std::string function_name, int line) { + LogIterator iter(*this, message, function_name, line); + DataBlock* last = ZEROERR_LOAD(m_last); + iter.p = last; + iter.q = reinterpret_cast(&(last->data[ZEROERR_LOAD(last->size)])); + return iter; +} + void LogStream::flush() { ZEROERR_LOCK(*mutex); + DataBlock* last = ZEROERR_LOAD(m_last); for (DataBlock* p = first; p != last; p = p->next) { logger->flush(p); delete p; @@ -4390,6 +4624,57 @@ void* LogStream::getRawLog(std::string func, unsigned line, std::string name) { return nullptr; } +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); + return nullptr; +} + +LogIterator::LogIterator(LogStream& stream, std::string message, std::string function_name, + int line) + : p(stream.first), + q(stream.first->begin()), + message_filter(message), + function_name_filter(function_name), + line_filter(line) { + while (!check_filter() && p) next(); +} + +void LogIterator::check_at_safe_pos() { + if (static_cast((char*)q - p->data) >= ZEROERR_LOAD(p->size)) { + p = p->next; + q = reinterpret_cast(p->data); + } +} + +void LogIterator::next() { + if (q < p->end()) { + q = moveBytes(q, q->info->size); + if (q >= p->end()) next(); + } else { + p = p->next; + if (p) + q = p->begin(); + else + q = nullptr; + } +} + +LogIterator& LogIterator::operator++() { + do { + next(); + } while (p && !check_filter()); + return *this; +} + +bool LogIterator::check_filter() { + if (!message_filter.empty() && 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; +} + class FileLogger : public Logger { public: FileLogger(std::string name) { file = fopen(name.c_str(), "w"); } @@ -4410,6 +4695,126 @@ class FileLogger : public Logger { FILE* file; }; + +#ifdef _WIN32 +static char split = '\\'; +#else +static char split = '/'; +#endif + +static void make_dir(std::string path) { + size_t pos = 0; + do { + pos = path.find_first_of(split, pos + 1); + std::string sub = path.substr(0, pos); +#ifdef _WIN32 + CreateDirectory(sub.c_str(), NULL); +#else + struct stat st; + if (stat(sub.c_str(), &st) == -1) { + mkdir(sub.c_str(), 0755); + } +#endif + } while (pos != std::string::npos); +} + +struct FileCache { + std::map files; + ~FileCache() { + for (auto& p : files) { + fclose(p.second); + } + } + + FILE* get(const std::string& name) { + auto it = files.find(name); + if (it != files.end()) return it->second; + auto p = name.find_last_of(split); + std::string path = name.substr(0, p); + make_dir(path.c_str()); + + FILE* file = fopen(name.c_str(), "w"); + if (file) { + files[name] = file; + return file; + } + return nullptr; + } +}; + + +class DirectoryLogger : public Logger { +public: + DirectoryLogger(std::string path, LogStream::DirMode dir_mode[3]) : dirpath(path) { + make_dir(path.c_str()); + for (int i = 0; i < 3; i++) { + this->dir_mode[i] = dir_mode[i]; + } + } + ~DirectoryLogger() {} + + void flush(DataBlock* msg) override { + FileCache cache; + for (auto p = msg->begin(); p < msg->end(); p = moveBytes(p, p->info->size)) { + auto ss = log_custom_callback(*p, false); + + std::stringstream path; + path << dirpath; + if (dirpath.back() != split) path << split; + + int last = 0; + for (int i = 0; i < 3; i++) { + if (last != 0 && dir_mode[i] != 0) path << split; + switch (dir_mode[i]) { + case LogStream::DAILY_FILE: { + std::time_t t = std::chrono::system_clock::to_time_t(p->time); + std::tm tm = *std::localtime(&t); + path << std::put_time(&tm, "%Y-%m-%d"); + break; + } + case LogStream::SPLIT_BY_SEVERITY: { + path << to_string(p->info->severity); + break; + } + case LogStream::SPLIT_BY_CATEGORY: { + path << to_category(p->info->category); + break; + } + default: continue; + } + last = 1; + } + std::cerr << path.str() << std::endl; + + FILE* file = cache.get(path.str()); + fwrite(ss.c_str(), ss.size(), 1, file); + } + } + +protected: + std::string to_string(LogSeverity severity) { + switch (severity) { + case INFO_l: return "INFO"; + case LOG_l: return "LOG"; + case WARN_l: return "WARN"; + case ERROR_l: return "ERROR"; + case FATAL_l: return "FATAL"; + } + return ""; + } + + std::string to_category(const char* category) { + std::string cat = category; + for (auto& c : cat) { + if (c == '/') c = split; + } + return cat; + } + + LogStream::DirMode dir_mode[3]; + std::string dirpath; +}; + class OStreamLogger : public Logger { public: OStreamLogger(std::ostream& os) : os(os) {} @@ -4431,9 +4836,16 @@ LogStream& LogStream::getDefault() { return stream; } -void LogStream::setFileLogger(std::string name) { +void LogStream::setFileLogger(std::string name, DirMode mode1, DirMode mode2, DirMode mode3) { if (logger) delete logger; - logger = new FileLogger(name); + + if (mode1 == 0 || mode2 == 0 || mode3 == 0) + logger = new FileLogger(name); + else { + LogStream::DirMode dir_mode_group[3] = {mode1, mode2, mode3}; + + logger = new DirectoryLogger(name, dir_mode_group); + } } void LogStream::setStdoutLogger() { @@ -4472,15 +4884,15 @@ void setLogCategory(const char* categories) { } } -static LogStream::FlushMode saved_flush_mode; +static LogStream::FlushMode saved_flush_mode = LogStream::FlushMode::FLUSH_AT_ONCE; void suspendLog() { - saved_flush_mode = LogStream::getDefault().flush_mode; - LogStream::getDefault().flush_mode = LogStream::FLUSH_MANUALLY; + saved_flush_mode = LogStream::getDefault().getFlushMode(); + LogStream::getDefault().setFlushMode(LogStream::FLUSH_MANUALLY); } void resumeLog() { - LogStream::getDefault().flush_mode = saved_flush_mode; + LogStream::getDefault().setFlushMode(saved_flush_mode); LogStream::getDefault().flush(); } @@ -4789,7 +5201,7 @@ inline std::string _rept(unsigned k, std::string j, Table::Style&) { } #define rep(k, t) _rept(k, t, s) -#define remain(k) (col_width[i] - k.size()) +#define remain(k) (col_width[i] - static_cast(k.size())) std::string Table::str(Config c, Table::Style s) { std::stringstream ss; @@ -4800,12 +5212,12 @@ std::string Table::str(Config c, Table::Style s) { if (col_width.size() == 0) { for (size_t i = 0; i < header.size(); ++i) { - unsigned max_width = 0; + size_t max_width = 0; for (auto& row : cells) { - max_width = std::max(row[i].size(), max_width); + max_width = std::max(row[i].size(), max_width); } - max_width = std::max(max_width, header[i].size()); - col_width.push_back(max_width); + max_width = std::max(max_width, header[i].size()); + col_width.push_back(static_cast(max_width)); } } @@ -4872,6 +5284,7 @@ std::string Table::str(Config c, Table::Style s) { + #include #include #include @@ -4955,7 +5368,7 @@ void SubCase::operator<<(std::function op) { try { op(&local); } catch (const AssertionData&) { - } catch (const FuzzFinishedException& e) { + } catch (const FuzzFinishedException&) { } catch (const std::exception& e) { std::cerr << e.what() << std::endl; if (local.failed_as == 0) { @@ -4968,13 +5381,13 @@ void SubCase::operator<<(std::function op) { context->reporter.subCaseEnd(*this, new_buf, local, type); } -struct Filiters { +struct Filters { std::vector name, name_exclude; std::vector file, file_exclude; }; UnitTest& UnitTest::parseArgs(int argc, const char** argv) { - filiters = new Filiters(); + filters = new Filters(); auto convert_to_vec = [=]() { std::vector result; for (int i = 1; i < argc; i++) { @@ -5029,24 +5442,31 @@ UnitTest& UnitTest::parseArgs(int argc, const char** argv) { if (arg == "list-test-cases") { this->list_test_cases = true; } + if (arg == "no-color") { + this->no_color = true; + disableColorOutput(); + } + if (arg == "log-to-report") { + this->log_to_report = true; + } if (arg.substr(0, 9) == "reporters") { this->reporter_name = arg.substr(10); return true; } if (arg.substr(0, 8) == "testcase") { - filiters->name.push_back(std::regex(arg.substr(9))); + filters->name.push_back(std::regex(arg.substr(9))); return true; } if (arg.substr(0, 14) == "testcase-exclude") { - filiters->name_exclude.push_back(std::regex(arg.substr(15))); + filters->name_exclude.push_back(std::regex(arg.substr(15))); return true; } if (arg.substr(0, 5) == "file") { - filiters->file.push_back(std::regex(arg.substr(6))); + filters->file.push_back(std::regex(arg.substr(6))); return true; } if (arg.substr(0, 11) == "file-exclude") { - filiters->file_exclude.push_back(std::regex(arg.substr(12))); + filters->file_exclude.push_back(std::regex(arg.substr(12))); return true; } return false; @@ -5082,15 +5502,15 @@ static std::string insertIndentation(std::string str) { return result.str(); } -bool UnitTest::run_filiter(const TestCase& tc) { - if (filiters == nullptr) return true; - for (auto& r : filiters->name) +bool UnitTest::run_filter(const TestCase& tc) { + if (filters == nullptr) return true; + for (auto& r : filters->name) if (!std::regex_match(tc.name, r)) return false; - for (auto& r : filiters->name_exclude) + for (auto& r : filters->name_exclude) if (std::regex_match(tc.name, r)) return false; - for (auto& r : filiters->file) + for (auto& r : filters->file) if (!std::regex_match(tc.file, r)) return false; - for (auto& r : filiters->file_exclude) + for (auto& r : filters->file_exclude) if (std::regex_match(tc.file, r)) return false; return true; } @@ -5098,6 +5518,7 @@ bool UnitTest::run_filiter(const TestCase& tc) { int UnitTest::run() { IReporter* reporter = IReporter::create(reporter_name, *this); if (!reporter) reporter = IReporter::create("console", *this); + TestContext context(*reporter), sum(*reporter); reporter->testStart(); std::stringbuf new_buf; @@ -5105,10 +5526,10 @@ int UnitTest::run() { unsigned types = TestType::test_case; if (run_bench) types |= TestType::bench; if (run_fuzz) types |= TestType::fuzz_test; - std::set testcases = detail::getRegisteredTests(types); + std::set test_cases = detail::getRegisteredTests(types); - for (auto& tc : testcases) { - if (!run_filiter(tc)) continue; + for (auto& tc : test_cases) { + if (!run_filter(tc)) continue; reporter->testCaseStart(tc, new_buf); if (!list_test_cases) { std::streambuf* orig_buf = std::cerr.rdbuf(); @@ -5117,7 +5538,7 @@ int UnitTest::run() { try { tc.func(&context); // run the test case } catch (const AssertionData&) { - } catch (const FuzzFinishedException& e) { + } catch (const FuzzFinishedException&) { } catch (const std::exception& e) { std::cerr << e.what() << std::endl; if (context.failed_as == 0) { @@ -5151,6 +5572,7 @@ std::set& getTestSet(TestType type) { case TestType::fuzz_test: return fuzz_set; case TestType::sub_case: return test_set; } + throw std::runtime_error("Invalid test type"); } static std::set getRegisteredTests(unsigned type) { @@ -5230,9 +5652,9 @@ namespace detail { class XmlEncode { public: enum ForWhat { ForTextNodes, ForAttributes }; - XmlEncode(std::string const& str, ForWhat forWhat = ForTextNodes); + XmlEncode(const std::string& str, ForWhat forWhat = ForTextNodes); void encodeTo(std::ostream& os) const; - friend std::ostream& operator<<(std::ostream& os, XmlEncode const& xmlEncode); + friend std::ostream& operator<<(std::ostream& os, const XmlEncode& xmlEncode); private: std::string m_str; @@ -5248,10 +5670,10 @@ class XmlWriter { ScopedElement& operator=(ScopedElement&& other) noexcept; ~ScopedElement(); - ScopedElement& writeText(std::string const& text, bool indent = true); + ScopedElement& writeText(const std::string& text, bool indent = true, bool new_line = true); template - ScopedElement& writeAttribute(std::string const& name, T const& attribute) { + ScopedElement& writeAttribute(const std::string& name, const T& attribute) { m_writer->writeAttribute(name, attribute); return *this; } @@ -5263,27 +5685,27 @@ class XmlWriter { XmlWriter(std::ostream& os = std::cerr); ~XmlWriter(); - XmlWriter(XmlWriter const&) = delete; - XmlWriter& operator=(XmlWriter const&) = delete; + XmlWriter(const XmlWriter&) = delete; + XmlWriter& operator=(const XmlWriter&) = delete; - XmlWriter& startElement(std::string const& name); - ScopedElement scopedElement(std::string const& name); + XmlWriter& startElement(const std::string& name); + ScopedElement scopedElement(const std::string& 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); + XmlWriter& writeAttribute(const std::string& name, const std::string& attribute); + XmlWriter& writeAttribute(const std::string& name, const char* attribute); + XmlWriter& writeAttribute(const std::string& name, bool attribute); template - XmlWriter& writeAttribute(std::string const& name, T const& attribute) { + XmlWriter& writeAttribute(const std::string& name, const T& attribute) { std::stringstream rss; rss << attribute; return writeAttribute(name, rss.str()); } - XmlWriter& writeText(std::string const& text, bool indent = true); + XmlWriter& writeText(const std::string& text, bool indent = true, bool new_line = true); - void ensureTagClosed(); + void ensureTagClosed(bool new_line = true); void writeDeclaration(); private: @@ -5291,6 +5713,7 @@ class XmlWriter { bool m_tagIsOpen = false; bool m_needsNewline = false; + bool m_needsIndent = false; std::vector m_tags; std::string m_indent; std::ostream& m_os; @@ -5331,7 +5754,7 @@ static void hexEscapeChar(std::ostream& os, unsigned char c) { os.flags(f); } -XmlEncode::XmlEncode(std::string const& str, ForWhat forWhat) : m_str(str), m_forWhat(forWhat) {} +XmlEncode::XmlEncode(const std::string& 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 @@ -5424,7 +5847,7 @@ void XmlEncode::encodeTo(std::ostream& os) const { } } -std::ostream& operator<<(std::ostream& os, XmlEncode const& xmlEncode) { +std::ostream& operator<<(std::ostream& os, const XmlEncode& xmlEncode) { xmlEncode.encodeTo(os); return os; } @@ -5448,9 +5871,9 @@ 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); +XmlWriter::ScopedElement& XmlWriter::ScopedElement::writeText(const std::string& text, + bool indent, bool new_line) { + m_writer->writeText(text, indent, new_line); return *this; } @@ -5460,7 +5883,7 @@ XmlWriter::~XmlWriter() { while (!m_tags.empty()) endElement(); } -XmlWriter& XmlWriter::startElement(std::string const& name) { +XmlWriter& XmlWriter::startElement(const std::string& name) { ensureTagClosed(); newlineIfNecessary(); m_os << m_indent << '<' << name; @@ -5470,7 +5893,7 @@ XmlWriter& XmlWriter::startElement(std::string const& name) { return *this; } -XmlWriter::ScopedElement XmlWriter::scopedElement(std::string const& name) { +XmlWriter::ScopedElement XmlWriter::scopedElement(const std::string& name) { ScopedElement scoped(this); startElement(name); return scoped; @@ -5483,44 +5906,48 @@ XmlWriter& XmlWriter::endElement() { m_os << "/>"; m_tagIsOpen = false; } else { - m_os << m_indent << ""; + if (m_needsIndent) m_os << m_indent; + else m_needsIndent = true; + m_os << ""; } m_os << std::endl; m_tags.pop_back(); return *this; } -XmlWriter& XmlWriter::writeAttribute(std::string const& name, std::string const& attribute) { +XmlWriter& XmlWriter::writeAttribute(const std::string& name, const std::string& 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) { +XmlWriter& XmlWriter::writeAttribute(const std::string& 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) { +XmlWriter& XmlWriter::writeAttribute(const std::string& name, bool attribute) { m_os << ' ' << name << "=\"" << (attribute ? "true" : "false") << '"'; return *this; } -XmlWriter& XmlWriter::writeText(std::string const& text, bool indent) { +XmlWriter& XmlWriter::writeText(const std::string& text, bool indent, bool new_line) { if (!text.empty()) { bool tagWasOpen = m_tagIsOpen; - ensureTagClosed(); + ensureTagClosed(new_line); if (tagWasOpen && indent) m_os << m_indent; m_os << XmlEncode(text); - m_needsNewline = true; + m_needsNewline = new_line; + m_needsIndent = new_line; } return *this; } -void XmlWriter::ensureTagClosed() { +void XmlWriter::ensureTagClosed(bool new_line) { if (m_tagIsOpen) { - m_os << ">" << std::endl; + m_os << ">"; + if (new_line) m_os << std::endl; m_tagIsOpen = false; } } @@ -5543,74 +5970,89 @@ class XmlReporter : public IReporter { public: detail::XmlWriter xml; - struct TestCaseData { - struct TestCase { - std::string filename, name; - unsigned line; - double time; - TestContext context; - }; + struct TestCaseTemp { + const TestCase* tc; + }; - std::vector testcases; - double total_time; - } tc_data; - std::vector current; + std::vector current; virtual std::string getName() const override { return "xml"; } // There are a list of events - virtual void testStart() override { xml.writeDeclaration(); } + virtual void testStart() override { + xml.writeDeclaration(); + xml.startElement("ZeroErr") + .writeAttribute("binary", ut.binary) + .writeAttribute("version", ZEROERR_VERSION_STR); + xml.startElement("TestSuite"); + } virtual void testCaseStart(const TestCase& tc, std::stringbuf&) override { - current.push_back(&tc); + current.push_back({&tc}); + xml.startElement("TestCase") + .writeAttribute("name", tc.name) + .writeAttribute("filename", tc.file) + .writeAttribute("line", tc.line) + .writeAttribute("skipped", "false"); + if (ut.log_to_report) suspendLog(); } - virtual void testCaseEnd(const TestCase& tc, std::stringbuf&, const TestContext& ctx, + virtual void testCaseEnd(const TestCase& tc, std::stringbuf& sb, const TestContext& ctx, int) override { - tc_data.testcases.push_back({tc.file, tc.name, tc.line, 0.0, ctx}); current.pop_back(); + xml.scopedElement("Result") + .writeAttribute("time", 0) + .writeAttribute("passed", ctx.passed) + .writeAttribute("warnings", ctx.warning) + .writeAttribute("failed", ctx.failed) + .writeAttribute("skipped", ctx.skipped); + xml.scopedElement("ResultAsserts") + .writeAttribute("passed", ctx.passed_as) + .writeAttribute("warnings", ctx.warning_as) + .writeAttribute("failed", ctx.failed_as) + .writeAttribute("skipped", ctx.skipped_as); + xml.scopedElement("Output").writeText(sb.str()); + + if (ut.log_to_report) { + xml.startElement("Log"); + LogIterator begin = LogStream::getDefault().begin(); + LogIterator end = LogStream::getDefault().end(); + for (auto p = begin; p != end; ++p) { + xml.startElement("LogEntry") + .writeAttribute("function", p->info->function) + .writeAttribute("line", p->info->line) + .writeAttribute("message", p->info->message) + .writeAttribute("category", p->info->category) + .writeAttribute("severity", p->info->severity); + for (auto pair : p->getData()) { + xml.scopedElement(pair.first).writeText(pair.second, false, false); + } + xml.endElement(); + } + xml.endElement(); + resumeLog(); + } + xml.endElement(); } - virtual void subCaseStart(const TestCase& tc, std::stringbuf&) override { - current.push_back(&tc); + virtual void subCaseStart(const TestCase& tc, std::stringbuf& sb) override { + testCaseStart(tc, sb); } - virtual void subCaseEnd(const TestCase& tc, std::stringbuf&, const TestContext& ctx, + virtual void subCaseEnd(const TestCase& tc, std::stringbuf& sb, const TestContext& ctx, int) override { - tc_data.testcases.push_back({tc.file, tc.name, tc.line, 0.0, ctx}); - current.pop_back(); + testCaseEnd(tc, sb, ctx, 0); } virtual void testEnd(const TestContext& tc) override { - xml.startElement("ZeroErr") - .writeAttribute("binary", ut.binary) - .writeAttribute("version", ZEROERR_VERSION_STR); + xml.endElement(); + xml.startElement("OverallResults") .writeAttribute("errors", tc.failed_as) .writeAttribute("failures", tc.failed) .writeAttribute("tests", tc.passed + tc.failed + tc.warning); xml.endElement(); - xml.startElement("TestSuite"); - for (const auto& testCase : tc_data.testcases) { - xml.startElement("TestCase") - .writeAttribute("name", testCase.name) - .writeAttribute("filename", testCase.filename) - .writeAttribute("line", testCase.line) - .writeAttribute("skipped", "false"); - xml.scopedElement("Result") - .writeAttribute("time", testCase.time) - .writeAttribute("passed", testCase.context.passed) - .writeAttribute("warnings", testCase.context.warning) - .writeAttribute("failed", testCase.context.failed) - .writeAttribute("skipped", testCase.context.skipped); - xml.scopedElement("ResultAsserts") - .writeAttribute("passed", testCase.context.passed_as) - .writeAttribute("warnings", testCase.context.warning_as) - .writeAttribute("failed", testCase.context.failed_as) - .writeAttribute("skipped", testCase.context.skipped_as); - xml.endElement(); - } - xml.endElement(); + xml.endElement(); } @@ -5630,7 +6072,6 @@ IReporter* IReporter::create(const std::string& name, UnitTest& ut) { int main(int argc, const char** argv) { zeroerr::UnitTest().parseArgs(argc, argv).run(); std::_Exit(0); - return 0; } @@ -5708,9 +6149,9 @@ void RunFuzzTest(IFuzzTest& fuzz_test, int seed, int runs, int max_len, int time #include -#ifdef _WIN32 -#define ZEROERR_ETW 1 -#endif +// #ifdef _WIN32 +// #define ZEROERR_ETW 1 +// #endif namespace zeroerr { @@ -5944,10 +6385,10 @@ void Benchmark::report() { Table output; output.set_header(headers); for (auto& row : result) { - auto result = row.average(); + auto row_avg = row.average(); std::vector values; for (int j = 0; j < 7; ++j) - if (row.has.data[j]) values.push_back(result.data[j]); + if (row.has.data[j]) values.push_back(row_avg.data[j]); output.add_row(row.name, values); } std::cerr << output.str() << std::endl; @@ -6243,7 +6684,8 @@ struct WindowsPerformanceCounter { // https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ke/profobj/kprofile_source.htm // TotalIssues TotalCycles CacheMisses BranchMispredictions unsigned long perf_counter[4] = {0x02, 0x13, 0x0A, 0x0B}; - TraceSetInformation(mTraceHandle, TracePmcCounterListInfo, perf_counter, sizeof(perf_counter)); + TraceSetInformation(mTraceHandle, TracePmcCounterListInfo, perf_counter, + sizeof(perf_counter)); cleanup: if (mTraceHandle) { @@ -6329,6 +6771,8 @@ void PerformanceCounter::endMeasure() { void PerformanceCounter::updateResults(uint64_t numIters) { #ifdef ZEROERR_PERF _perf->updateResults(numIters); +#else + (void)numIters; #endif }