diff --git a/.gitignore b/.gitignore index 694c363..c3c093f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,53 +1,62 @@ -# Build directory -/build/ - -# CMake generated files -CMakeCache.txt -CMakeFiles/ -cmake_install.cmake -Makefile - -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app - -# IDE-specific files (uncomment if needed) -# .vscode/ -# .idea/ -# *.swp -# *.swo - -# OS generated files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db \ No newline at end of file +# Build directory +/build/ +/build_wsl/ + +# CMake generated files +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +Makefile + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# IDE-specific files (uncomment if needed) +# .vscode/ +# .idea/ +# *.swp +# *.swo + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Local documentation +CLAUDE.local.md +CLAUDE.md +settings.json +Testing/Temporary/LastTest.log +Testing/Temporary/CTestCostData.txt +.claude/settings.local.json diff --git a/CMakeLists.txt b/CMakeLists.txt index f304386..4e8cb55 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,89 +1,112 @@ -cmake_minimum_required(VERSION 3.12) -project(ctrack VERSION 1.0.2 LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Option to disable parallel processing -option(DISABLE_PAR "Disable parallel processing" OFF) - -# Option to disable building examples -option(DISABLE_EXAMPLES "Disable building examples" OFF) - -option(ENABLE_WARNINGS "Enable warnings" OFF) - -# Check for TBB -if(NOT MSVC AND NOT DISABLE_PAR) - find_package(TBB QUIET) - if(TBB_FOUND) - message(STATUS "TBB found. Enabling parallel execution.") - else() - message(STATUS "TBB not found. Disabling parallel execution.") - set(DISABLE_PAR ON) - endif() -elseif(DISABLE_PAR) - message(STATUS "DISABLE_PAR set. Disabling parallel execution.") -endif() - -# Create the ctrack library -add_library(ctrack INTERFACE) -target_include_directories(ctrack INTERFACE - $ - $ -) - -# Configure ctrack based on TBB availability -if(DISABLE_PAR) - target_compile_definitions(ctrack INTERFACE CTRACK_DISABLE_EXECUTION_POLICY) -elseif(NOT MSVC AND TBB_FOUND) - target_link_libraries(ctrack INTERFACE TBB::tbb) -endif() - -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -if(ENABLE_WARNINGS) - if (NOT MSVC) - include(cmake/add_warning.cmake) - include(cmake/warnings.cmake) - endif() -endif() - -# Add the examples subdirectory if not disabled -if(NOT DISABLE_EXAMPLES) - add_subdirectory(examples) -else() - message(STATUS "Building examples disabled.") -endif() - -# Installation -include(GNUInstallDirs) -include(CMakePackageConfigHelpers) - -install(TARGETS ctrack - EXPORT ctrackTargets - INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} -) - -install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) - -install(EXPORT ctrackTargets - FILE ctrackTargets.cmake - NAMESPACE ctrack:: - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ctrack -) - -configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in - "${CMAKE_CURRENT_BINARY_DIR}/ctrackConfig.cmake" - INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ctrack -) - -write_basic_package_version_file( - "${CMAKE_CURRENT_BINARY_DIR}/ctrackConfigVersion.cmake" - VERSION ${PROJECT_VERSION} - COMPATIBILITY SameMajorVersion -) - -install(FILES - "${CMAKE_CURRENT_BINARY_DIR}/ctrackConfig.cmake" - "${CMAKE_CURRENT_BINARY_DIR}/ctrackConfigVersion.cmake" - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ctrack +cmake_minimum_required(VERSION 3.12) +project(ctrack VERSION 1.0.2 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Option to disable parallel processing +option(DISABLE_PAR "Disable parallel processing" OFF) + +# Option to disable building examples +option(DISABLE_EXAMPLES "Disable building examples" OFF) + +# Option to build benchmark (default OFF) +option(BUILD_BENCHMARK "Build benchmark" OFF) + +# Option to build tests (default OFF) +option(BUILD_TESTS "Build tests" OFF) + +option(ENABLE_WARNINGS "Enable warnings" OFF) + +# Check for TBB +if(NOT MSVC AND NOT DISABLE_PAR) + find_package(TBB QUIET) + if(TBB_FOUND) + message(STATUS "TBB found. Enabling parallel execution.") + else() + message(STATUS "TBB not found. Disabling parallel execution.") + set(DISABLE_PAR ON) + endif() +elseif(DISABLE_PAR) + message(STATUS "DISABLE_PAR set. Disabling parallel execution.") +endif() + +# Create the ctrack library +add_library(ctrack INTERFACE) +target_include_directories(ctrack INTERFACE + $ + $ +) + +# Configure ctrack based on TBB availability +if(DISABLE_PAR) + target_compile_definitions(ctrack INTERFACE CTRACK_DISABLE_EXECUTION_POLICY) +elseif(NOT MSVC AND TBB_FOUND) + target_link_libraries(ctrack INTERFACE TBB::tbb) +endif() + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +if(ENABLE_WARNINGS) + if (NOT MSVC) + include(cmake/add_warning.cmake) + include(cmake/warnings.cmake) + endif() +endif() + +# Add the examples subdirectory if not disabled +if(NOT DISABLE_EXAMPLES) + add_subdirectory(examples) +else() + message(STATUS "Building examples disabled.") +endif() + +# Add the benchmark subdirectory if enabled +if(BUILD_BENCHMARK) + add_subdirectory(benchmark) + message(STATUS "Building benchmark enabled.") +else() + message(STATUS "Building benchmark disabled.") +endif() + +# Add the test subdirectory if enabled +if(BUILD_TESTS) + add_subdirectory(test) + enable_testing() + message(STATUS "Building tests enabled.") +else() + message(STATUS "Building tests disabled.") +endif() + +# Installation +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) + +install(TARGETS ctrack + EXPORT ctrackTargets + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) + +install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + +install(EXPORT ctrackTargets + FILE ctrackTargets.cmake + NAMESPACE ctrack:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ctrack +) + +configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/ctrackConfig.cmake" + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ctrack +) + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/ctrackConfigVersion.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion +) + +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/ctrackConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/ctrackConfigVersion.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ctrack ) \ No newline at end of file diff --git a/Config.cmake.in b/Config.cmake.in index e7ed8c1..4bc3356 100644 --- a/Config.cmake.in +++ b/Config.cmake.in @@ -1,18 +1,18 @@ -@PACKAGE_INIT@ - -include("${CMAKE_CURRENT_LIST_DIR}/ctrackTargets.cmake") - -if(NOT MSVC AND NOT CTRACK_DISABLE_EXECUTION_POLICY) - include(CMakeFindDependencyMacro) - find_dependency(TBB QUIET) - if(NOT TBB_FOUND) - message(STATUS "TBB not found. Disabling parallel execution for ctrack.") - set(CTRACK_DISABLE_EXECUTION_POLICY ON) - endif() -endif() - -if(CTRACK_DISABLE_EXECUTION_POLICY) - target_compile_definitions(ctrack::ctrack INTERFACE CTRACK_DISABLE_EXECUTION_POLICY) -endif() - +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/ctrackTargets.cmake") + +if(NOT MSVC AND NOT CTRACK_DISABLE_EXECUTION_POLICY) + include(CMakeFindDependencyMacro) + find_dependency(TBB QUIET) + if(NOT TBB_FOUND) + message(STATUS "TBB not found. Disabling parallel execution for ctrack.") + set(CTRACK_DISABLE_EXECUTION_POLICY ON) + endif() +endif() + +if(CTRACK_DISABLE_EXECUTION_POLICY) + target_compile_definitions(ctrack::ctrack INTERFACE CTRACK_DISABLE_EXECUTION_POLICY) +endif() + check_required_components(ctrack) \ No newline at end of file diff --git a/LICENSE b/LICENSE index e5d1ca9..f55c146 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2024 COMPAILE Solutions GmbH - Grischa Hauser - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +MIT License + +Copyright (c) 2024 COMPAILE Solutions GmbH - Grischa Hauser + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 08b5e89..9451c5b 100644 --- a/README.md +++ b/README.md @@ -1,348 +1,528 @@ -# CTRACK ![MIT](https://img.shields.io/badge/license-MIT-blue.svg) - -An open-source benchmark and tracking library for C++ projects, designed to provide deep insights into function performance with minimal overhead. - -CTRACK is a powerful tool that can be seamlessly integrated into both development and production environments. It allows developers to effortlessly monitor applications and identify bottlenecks, requiring minimal setup and maintenance. - -## Features - -- Single header file -- No dependencies (optional tbb for non msvc to use paralell result calculation) -- Easy to use (just 1 line per function you want to track) -- Minimal overhead (can record tens of millions events per second) -- Optimized for multi-threaded environments -- Requires C++17 -- Compatible with major compilers out of the box -- Multiple output formats (stdout(with color), string, and more soon JSON export, SQL) -- Suitable for both development and high-performance production systems -- Can be easily customized to filter out noise and only print events that matter. Recording levels can be adjusted based on the environment (production/development) to balance between insight and performance. -- Includes a beautiful table print functionality for clear and readable output. While easy-to-use functions are provided for accessing CTRACK events, more experienced users can access all data directly for advanced analysis. - - -## Goals - -1. Help developers identify areas for performance improvement -2. Monitor application performance in production - - -## Basic Usage - -CTRACK is easy to use and provides powerful performance insights. Here's how to get started: - -1. To track a function, simply add the `CTRACK;` macro at the beginning of the function body: - -```cpp -void myFunction() { - CTRACK; - // Your function code here -} -``` - -2. To print the results, you have two options: - - a. Print colored results to the console: - ```cpp - ctrack::result_print(); - ``` - - b. Get the results as a string (useful for logging or custom output): - ```cpp - std::string results = ctrack::result_as_string(); - ``` - -### Example - -```cpp -#include "ctrack.hpp" - -void expensiveOperation() { - CTRACK; - // Simulating some work - for (int i = 0; i < 1000000; ++i) { - // Do something - } -} - -int main() { - for (int i = 0; i < 100; ++i) { - expensiveOperation(); - } - - // Print results to console - ctrack::result_print(); - - return 0; -} -``` - -This basic usage will automatically track the performance of `expensiveOperation` and provide you with insights when you call `result_print()`. - -For more complex scenarios, configuration options, and advanced features, please refer to the [Advanced Usage](#advanced-usage) section below. -Additionally, be sure to check out the examples directory in the repository for more detailed usage examples and best practices. - - - -## Metrics & Output - -CTRACK provides comprehensive performance metrics through two main components: the Summary Table and the Detail Table. These tables offer different levels of insight into your application's performance. - -### Time Units - -All times in CTRACK are presented and automatically converted in easily understandable units: -- ns (nanoseconds) -- μs (microseconds) printed as mcs -- ms (milliseconds) -- s (seconds) - -### General Metrics - -- **min, mean, med, max**: The fastest (min), average (mean), median (med), and slowest (max) execution times for a specific CTRACK event. - -- **time a - time active**: Total time the event was active, useful for multithreaded environments. For example, if a 100ms function is called by 10 threads simultaneously, time active will show 100ms instead of 1000ms. - -- **time ae - time active exclusive**: Subtracts the time spent in child functions that are also tracked. Intelligently handles recursion and overlapping timeframes. - -- **time [x-y]**: Shows event times within specified percentile ranges (default [0-100] and [1-99]) to exclude outliers. - -- **sd - Standard Deviation**: Displays the variability in function execution times. - -- **cv - Coefficient of Variation**: Unitless version of standard deviation (sd / mean) for comparing variability across functions with different scales. - -- **time acc**: Simple sum of execution times for all calls to a tracked function. - -- **threads**: Number of different threads that called a specific function. - -### Summary Table - -![image](https://github.com/user-attachments/assets/0232a57c-8a3e-4a7b-a143-62bb8e2b9825) - - -#### Summary Header -- Start and End time of tracking -- Total time -- Time tracked (time spent in tracked functions) -- Time tracked percentage - -#### Summary Entries -- Filename, function, line -- Number of calls -- Time active exclusive for [0-100] and [center interval] (in percent and absolute) -- Time active for [0-100] in absolute - -The summary table is sorted by the active exclusive [center interval] metric. - -### Detail Table - -![image](https://github.com/user-attachments/assets/ae9cf8e9-52b8-42ed-95fa-c95a418da510) - -For each function: -- Filename, function, line -- Time accumulated -- Standard Deviation -- Coefficient of Variation (cv) -- Number of calls -- Number of calling threads - -Each entry shows 3 blocks for the fastest, center, and slowest events. - -### Output Format - -- String output: Summary table followed by Detail tables (slowest to fastest) -- Console output: Reversed order (Detail tables followed by Summary table) - -This comprehensive set of metrics allows for deep insight into your application's performance, helping you identify bottlenecks and optimize effectively. - -For more advanced usage and customization options, please refer to the [Advanced Usage](#advanced-usage) section below. - -## Installation - -CTRACK is designed to be easy to integrate into your C++ projects. There are two primary ways to use CTRACK: - -### 1. Header-Only Inclusion - -CTRACK is a header-only library, which means you can start using it by simply including the main header file in your project: - -```cpp -#include "ctrack.hpp" -``` - -This method is straightforward and doesn't require any additional setup or build process. - -Note: If you are using a compiler which needs TBB for C++ standard parallel algorithms, you need to link to -ltbb. You can always fall back to sequential result calculation by setting -CTRACK_DISABLE_EXECUTION_POLICY. The recording will be unchanged, but the printing/calculating of the stats will be a bit slower. - -### 2. CMake Package - -For projects using CMake, CTRACK can be installed and used as a CMake package. This method provides better integration with your build system and makes it easier to manage dependencies. - -To use CTRACK as a CMake package: - -1. Install CTRACK using CMake: - - ```bash - git clone https://github.com/your-repo/ctrack.git - cd ctrack - mkdir build && cd build - cmake .. - cmake --build . --target install - ``` - -2. In your project's `CMakeLists.txt`, add: - - ```cmake - find_package(ctrack REQUIRED) - target_link_libraries(your_target PRIVATE ctrack::ctrack) - ``` - -Note: If you are using a compiler which needs TBB for C++ standard parallel algorithms, you need to link to tbb. - ```target_link_libraries( your_target PRIVATE TBB::tbb ) ``` -You can always fall back to sequential result calculation by setting -CTRACK_DISABLE_EXECUTION_POLICY. The recording will be unchanged, but the printing/calculating of the stats will be a bit slower. - -For more detailed examples of how to use CTRACK with CMake, please refer to the `examples` directory in the CTRACK repository. - -Choose the installation method that best fits your project's needs and structure. Both methods provide full access to CTRACK's features and capabilities. - - -## Advanced Usage - -### Customizing Output Settings - -You can fine-tune CTRACK's output using the `ctrack_result_settings` struct: - -```cpp -struct ctrack_result_settings { - unsigned int non_center_percent = 1; - double min_percent_active_exclusive = 0.5; // between 0-100, default 0.5% - double percent_exclude_fastest_active_exclusive = 0.0; // between 0-100 -}; -``` - -- `non_center_percent`: Defines the range for the center interval (e.g., 1 means [1-99]) -- `min_percent_active_exclusive`: Excludes events active for less than the specified percentage -- `percent_exclude_fastest_active_exclusive`: Excludes the fastest n% of functions to reduce noise - -### Advanced CTRACK Calls - -CTRACK offers different tracking levels: - -- `CTRACK`: Standard tracking -- `CTRACK_DEV`: Development-specific tracking -- `CTRACK_PROD`: Production-specific tracking - -You can selectively disable tracking groups: - -- `CTRACK_DISABLE_DEV`: Disables all `CTRACK_DEV` calls - -To completely disable CTRACK at compile time, define `CTRACK_DISABLE`. - -### Custom Naming - -Use custom names for CTRACK calls instead of function names: - -```cpp -CTRACK_NAME("myname") -CTRACK_DEV_NAME("mydevname") -CTRACK_PROD_NAME("myprodname") -``` - -This is useful for large functions where you want multiple CTRACK entries with distinct names. - -### Code-Level Access - -The `result_print` and `result_as_string` functions are concise and located at the bottom of the CTRACK header. You can easily modify these or create custom functions to change the order, enable/disable colors, etc. - -The `calc_stats_and_clear` function produces the `ctrack_result` object. Instead of printing tables, you can access this data directly for custom analysis or integration with other systems. - -### Example: Customizing Output - -```cpp -ctrack_result_settings settings; -settings.non_center_percent = 2; -settings.min_percent_active_exclusive = 1.0; -settings.percent_exclude_fastest_active_exclusive = 5.0; - -std::string custom_result = ctrack::result_as_string(settings); -``` - -This advanced usage allows you to tailor CTRACK to your specific needs, from fine-tuning output to integrating with complex systems and workflows. - -## Performance Benchmarks - -The recording of events in this project is extremely fast. You can use the example projects to test it on your own system. - -- On an i9-12900KS: - - CTRACK can record 10,000,000 events in 132ms - This translates to over 75 million events per second - -The calculation of results is also efficient. However, the primary focus of ctrack is to have nearly zero overhead for tracking while allowing some overhead for calculating statistics at the end. -Would you like me to explain any part of this new section or suggest any modifications? - -## Why Another Benchmark Library? - -While there are several excellent benchmarking and profiling tools available, CTRACK fills a unique niche in the C++ performance analysis ecosystem. Here's why CTRACK stands out: - -1. **Production-Ready**: Unlike libraries such as Google Benchmark, which require specific benchmark calls, CTRACK can be seamlessly used in both development and production environments. - -2. **Legacy-Friendly**: CTRACK is designed to easily integrate with large, established projects that might be challenging to instrument with other tracking solutions. - -3. **Lightweight and Fast**: Traditional profiling tools like MSVC Performance Analyzer or Intel VTune can struggle with millions of events. CTRACK maintains high performance even under heavy load. - -4. **No Complex Setup**: Unlike full-featured profilers, CTRACK doesn't require an extensive setup process, making it ideal for quick deployments and CI/CD pipelines. - -5. **Platform Independent**: CTRACK works across different platforms without modification, unlike some platform-specific profiling tools. - -6. **Simplicity**: Many developers resort to manual timing using `std::chrono`. CTRACK provides a more robust solution with similar ease of use. - -7. **Scalability**: From small libraries to massive codebases, CTRACK adapts to your needs. - -8. **Flexible Configuration**: Easily enable, disable, or customize logging levels to suit different environments (development vs. production). - -9. **Instant Bottleneck Detection**: CTRACK's unique "time active" and "time active exclusive" metrics allow developers to instantly spot bottlenecks, even in complex multithreaded codebases. This feature sets CTRACK apart from other tools that struggle to provide clear insights in concurrent environments. - -CTRACK combines the ease of use of manual timing with the robustness of professional benchmarking tools, all in a package that's production-ready and highly adaptable. Its ability to quickly identify performance issues in multithreaded scenarios makes useful tool for modern C++ development. - - -## Inspired By - -CTRACK stands on the shoulders of giants in the C++ performance analysis ecosystem. We're grateful to the following projects and their maintainers for pioneering innovative approaches to performance measurement, particularly the timing-by-lifetime concepts that became foundational to CTRACK's design: - -- [Darknet](https://github.com/hank-ai/darknet) -- [dlib](https://dlib.net/dlib/timing.h.html) -- [Tracy Profiler](https://github.com/wolfpld/tracy) -- [Google Benchmark](https://github.com/google/benchmark) -- [CppBenchmark](https://github.com/chronoxor/CppBenchmark) -- [nanobench](https://github.com/martinus/nanobench) - - -## Contributing - -We welcome and encourage contributions from the community! Your input helps make CTRACK better for everyone. Here's how you can contribute: - -### Roadmap -- [ ] JSON Export Support -- [ ] SQL Export Support -- [ ] Handling Complex Circular - -### Pull Requests -We're always excited to receive pull requests that improve CTRACK. When submitting a PR, please ensure: - -1. Your code adheres to the project's coding standards. -2. Your contributions are MIT License compliant. - -### Bug Reports -Found a bug? We want to hear about it! Please open an issue on our GitHub repository with: - -1. A clear, descriptive title. -2. A detailed description of the issue, including steps to reproduce. -3. Your environment details (OS, compiler version, etc.). - -### Feature Requests -Have an idea for a new feature? Feel free to open an issue to discuss it. We're always looking for ways to make CTRACK more useful. - - -## License - -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - +# CTRACK ![MIT](https://img.shields.io/badge/license-MIT-blue.svg) + +An open-source benchmark and tracking library for C++ projects, designed to provide deep insights into function performance with minimal overhead. + +CTRACK is a powerful tool that can be seamlessly integrated into both development and production environments. It allows developers to effortlessly monitor applications and identify bottlenecks, requiring minimal setup and maintenance. + +## Features + +- Single header file +- No dependencies (optional tbb for non msvc to use paralell result calculation) +- Easy to use (just 1 line per function you want to track) +- Minimal overhead (can record tens of millions events per second) +- Optimized for multi-threaded environments +- Requires C++17 +- Compatible with major compilers out of the box +- Multiple output formats (stdout(with color), string, and more soon JSON export, SQL) +- Suitable for both development and high-performance production systems +- Can be easily customized to filter out noise and only print events that matter. Recording levels can be adjusted based on the environment (production/development) to balance between insight and performance. +- Includes a beautiful table print functionality for clear and readable output. While easy-to-use functions are provided for accessing CTRACK events, more experienced users can access all data directly for advanced analysis. + +## Goals + +1. Help developers identify areas for performance improvement +2. Monitor application performance in production + +## Basic Usage + +CTRACK is easy to use and provides powerful performance insights. Here's how to get started: + +1. To track a function, simply add the `CTRACK;` macro at the beginning of the function body: + +```cpp +void myFunction() { + CTRACK; + // Your function code here +} +``` + +2. To print the results, you have two options: + + a. Print colored results to the console: + + ```cpp + ctrack::result_print(); + ``` + + b. Get the results as a string (useful for logging or custom output): + + ```cpp + std::string results = ctrack::result_as_string(); + ``` + +### Important: Scope-Based Tracking + +CTRACK uses RAII (Resource Acquisition Is Initialization) to track function execution times. **Events are recorded when the CTRACK object goes out of scope**. This has an important implication: + +```cpp +int main() { + CTRACK; // This won't track main() completely! + doWork(); + ctrack::result_print(); // CTRACK is still in scope here + return 0; // CTRACK records here when main() exits +} +``` + +To properly track a section of code within a function, use explicit scoping: + +```cpp +int main() { + { + CTRACK; // Start tracking + doWork(); + } // CTRACK goes out of scope and records the event + + ctrack::result_print(); // Now we can see the results + return 0; +} +``` + +### Example + +```cpp +#include "ctrack.hpp" + +void expensiveOperation() { + CTRACK; + // Simulating some work + for (int i = 0; i < 1000000; ++i) { + // Do something + } +} + +int main() { + { + CTRACK; // Track this block + for (int i = 0; i < 100; ++i) { + expensiveOperation(); + } + } // CTRACK records here + + // Print results to console + ctrack::result_print(); + + return 0; +} +``` + +This basic usage will automatically track the performance of `expensiveOperation` and the main loop block, providing you with insights when you call `result_print()`. + +For more complex scenarios, configuration options, and advanced features, please refer to the [Advanced Usage](#advanced-usage) section below. +Additionally, be sure to check out the examples directory in the repository for more detailed usage examples and best practices. + +## Metrics & Output + +CTRACK provides comprehensive performance metrics through two main components: the Summary Table and the Detail Table. These tables offer different levels of insight into your application's performance. + +### Time Units + +All times in CTRACK are presented and automatically converted in easily understandable units: + +- ns (nanoseconds) +- μs (microseconds) printed as mcs +- ms (milliseconds) +- s (seconds) + +### General Metrics + +- **min, mean, med, max**: The fastest (min), average (mean), median (med), and slowest (max) execution times for a specific CTRACK event. + +- **time a - time active**: Total time the event was active, useful for multithreaded environments. For example, if a 100ms function is called by 10 threads simultaneously, time active will show 100ms instead of 1000ms. + +- **time ae - time active exclusive**: Subtracts the time spent in child functions that are also tracked. Intelligently handles recursion and overlapping timeframes. + +- **time [x-y]**: Shows event times within specified percentile ranges (default [0-100] and [1-99]) to exclude outliers. + +- **sd - Standard Deviation**: Displays the variability in function execution times. + +- **cv - Coefficient of Variation**: Unitless version of standard deviation (sd / mean) for comparing variability across functions with different scales. + +- **time acc**: Simple sum of execution times for all calls to a tracked function. + +- **threads**: Number of different threads that called a specific function. + +### Summary Table + +![image](https://github.com/user-attachments/assets/0232a57c-8a3e-4a7b-a143-62bb8e2b9825) + +#### Summary Header + +- Start and End time of tracking +- Total time +- Time tracked (time spent in tracked functions) +- Time tracked percentage + +#### Summary Entries + +- Filename, function, line +- Number of calls +- Time active exclusive for [0-100] and [center interval] (in percent and absolute) +- Time active for [0-100] in absolute + +The summary table is sorted by the active exclusive [center interval] metric. + +### Detail Table + +![image](https://github.com/user-attachments/assets/ae9cf8e9-52b8-42ed-95fa-c95a418da510) + +For each function: + +- Filename, function, line +- Time accumulated +- Standard Deviation +- Coefficient of Variation (cv) +- Number of calls +- Number of calling threads + +Each entry shows 3 blocks for the fastest, center, and slowest events. + +### Output Format + +- String output: Summary table followed by Detail tables (slowest to fastest) +- Console output: Reversed order (Detail tables followed by Summary table) + +This comprehensive set of metrics allows for deep insight into your application's performance, helping you identify bottlenecks and optimize effectively. + +For more advanced usage and customization options, please refer to the [Advanced Usage](#advanced-usage) section below. + +## Installation + +CTRACK is designed to be easy to integrate into your C++ projects. There are two primary ways to use CTRACK: + +### 1. Header-Only Inclusion + +CTRACK is a header-only library, which means you can start using it by simply including the main header file in your project: + +```cpp +#include "ctrack.hpp" +``` + +This method is straightforward and doesn't require any additional setup or build process. + +Note: If you are using a compiler which needs TBB for C++ standard parallel algorithms, you need to link to -ltbb. You can always fall back to sequential result calculation by setting +CTRACK_DISABLE_EXECUTION_POLICY. The recording will be unchanged, but the printing/calculating of the stats will be a bit slower. + +### 2. CMake Package + +For projects using CMake, CTRACK can be installed and used as a CMake package. This method provides better integration with your build system and makes it easier to manage dependencies. + +To use CTRACK as a CMake package: + +1. Install CTRACK using CMake: + + ```bash + git clone https://github.com/your-repo/ctrack.git + cd ctrack + mkdir build && cd build + cmake .. + cmake --build . --target install + ``` + +2. In your project's `CMakeLists.txt`, add: + + ```cmake + find_package(ctrack REQUIRED) + target_link_libraries(your_target PRIVATE ctrack::ctrack) + ``` + +Note: If you are using a compiler which needs TBB for C++ standard parallel algorithms, you need to link to tbb. +`target_link_libraries( your_target PRIVATE TBB::tbb ) ` +You can always fall back to sequential result calculation by setting +CTRACK_DISABLE_EXECUTION_POLICY. The recording will be unchanged, but the printing/calculating of the stats will be a bit slower. + +For more detailed examples of how to use CTRACK with CMake, please refer to the `examples` directory in the CTRACK repository. + +Choose the installation method that best fits your project's needs and structure. Both methods provide full access to CTRACK's features and capabilities. + +## Advanced Usage + +### Customizing Output Settings + +You can fine-tune CTRACK's output using the `ctrack_result_settings` struct: + +```cpp +struct ctrack_result_settings { + unsigned int non_center_percent = 1; + double min_percent_active_exclusive = 0.5; // between 0-100, default 0.5% + double percent_exclude_fastest_active_exclusive = 0.0; // between 0-100 +}; +``` + +- `non_center_percent`: Defines the range for the center interval (e.g., 1 means [1-99]) +- `min_percent_active_exclusive`: Excludes events active for less than the specified percentage +- `percent_exclude_fastest_active_exclusive`: Excludes the fastest n% of functions to reduce noise + +### Advanced CTRACK Calls + +CTRACK offers different tracking levels: + +- `CTRACK`: Standard tracking +- `CTRACK_DEV`: Development-specific tracking +- `CTRACK_PROD`: Production-specific tracking + +You can selectively disable tracking groups: + +- `CTRACK_DISABLE_DEV`: Disables all `CTRACK_DEV` calls + +To completely disable CTRACK at compile time, define `CTRACK_DISABLE`. + +### Custom Naming + +Use custom names for CTRACK calls instead of function names: + +```cpp +CTRACK_NAME("myname") +CTRACK_DEV_NAME("mydevname") +CTRACK_PROD_NAME("myprodname") +``` + +This is useful for large functions where you want multiple CTRACK entries with distinct names. + +### Code-Level Access + +The `result_print` and `result_as_string` functions are concise and located at the bottom of the CTRACK header. You can easily modify these or create custom functions to change the order, enable/disable colors, etc. + +**Direct Data Access (v1.1.0+)**: You can now access the profiling results directly through structured data tables using: + +```cpp +// Get all result tables (summary + details) +auto tables = ctrack::result_get_tables(); + +// Get only the summary table +auto summary = ctrack::result_get_summary_table(); + +// Get only the detail table +auto details = ctrack::result_get_detail_table(); + +// Access individual rows for custom processing +for (const auto& row : summary.rows) { + // Process filename, function, line, call_count, percentages, etc. +} +``` + +This enables easy data export to CSV, JSON, or any custom format without modifying the library. + +### Example: Customizing Output + +```cpp +ctrack_result_settings settings; +settings.non_center_percent = 2; +settings.min_percent_active_exclusive = 1.0; +settings.percent_exclude_fastest_active_exclusive = 5.0; + +std::string custom_result = ctrack::result_as_string(settings); +``` + +This advanced usage allows you to tailor CTRACK to your specific needs, from fine-tuning output to integrating with complex systems and workflows. + +## Performance Benchmarks + +The recording of events in this project is extremely fast. You can use the example projects to test it on your own system. + +- On an i9-12900KS: + + CTRACK can record 10,000,000 events in 132ms + This translates to over 75 million events per second + +The calculation of results is also efficient. However, the primary focus of ctrack is to have nearly zero overhead for tracking while allowing some overhead for calculating statistics at the end. +Would you like me to explain any part of this new section or suggest any modifications? + +## Why Another Benchmark Library? + +While there are several excellent benchmarking and profiling tools available, CTRACK fills a unique niche in the C++ performance analysis ecosystem. Here's why CTRACK stands out: + +1. **Production-Ready**: Unlike libraries such as Google Benchmark, which require specific benchmark calls, CTRACK can be seamlessly used in both development and production environments. + +2. **Legacy-Friendly**: CTRACK is designed to easily integrate with large, established projects that might be challenging to instrument with other tracking solutions. + +3. **Lightweight and Fast**: Traditional profiling tools like MSVC Performance Analyzer or Intel VTune can struggle with millions of events. CTRACK maintains high performance even under heavy load. + +4. **No Complex Setup**: Unlike full-featured profilers, CTRACK doesn't require an extensive setup process, making it ideal for quick deployments and CI/CD pipelines. + +5. **Platform Independent**: CTRACK works across different platforms without modification, unlike some platform-specific profiling tools. + +6. **Simplicity**: Many developers resort to manual timing using `std::chrono`. CTRACK provides a more robust solution with similar ease of use. + +7. **Scalability**: From small libraries to massive codebases, CTRACK adapts to your needs. + +8. **Flexible Configuration**: Easily enable, disable, or customize logging levels to suit different environments (development vs. production). + +9. **Instant Bottleneck Detection**: CTRACK's unique "time active" and "time active exclusive" metrics allow developers to instantly spot bottlenecks, even in complex multithreaded codebases. This feature sets CTRACK apart from other tools that struggle to provide clear insights in concurrent environments. + +CTRACK combines the ease of use of manual timing with the robustness of professional benchmarking tools, all in a package that's production-ready and highly adaptable. Its ability to quickly identify performance issues in multithreaded scenarios makes useful tool for modern C++ development. + +## Inspired By + +CTRACK stands on the shoulders of giants in the C++ performance analysis ecosystem. We're grateful to the following projects and their maintainers for pioneering innovative approaches to performance measurement, particularly the timing-by-lifetime concepts that became foundational to CTRACK's design: + +- [Darknet](https://github.com/hank-ai/darknet) +- [dlib](https://dlib.net/dlib/timing.h.html) +- [Tracy Profiler](https://github.com/wolfpld/tracy) +- [Google Benchmark](https://github.com/google/benchmark) +- [CppBenchmark](https://github.com/chronoxor/CppBenchmark) +- [nanobench](https://github.com/martinus/nanobench) + +## Changelog + +### Version 1.1.0 (2025-09-29) + +#### New Features +- **Comprehensive Benchmarking Suite**: Added complete benchmark framework with baseline comparison capabilities for tracking performance across releases +- **Extensive Unit Testing**: Added comprehensive test suite covering basic functionality, edge cases, multithreaded scenarios, nested tracking, results processing, and statistics calculation +- **Enhanced Performance Metrics**: + - Added summary fields for active exclusive time and percentage calculations + - Introduced structured result tables for improved performance reporting + - New template function for distinct field value extraction + - Direct data access API for advanced result analysis + +#### Performance Improvements +- Reduced memory usage by avoiding nested maps in event storage +- Enhanced distinct field value counting using unordered_set for better performance +- Optimized event capacity reservation strategy to balance memory usage and reallocation overhead +- Refactored event handling to use pointers for improved cache locality and performance + +#### Build System Enhancements +- Added `BUILD_BENCHMARK=ON` CMake option for building benchmarking suite +- Added `BUILD_TESTS=ON` CMake option for building unit tests +- Improved CMake configuration with enhanced warning handling and platform detection + +#### Other Improvements +- Improved code formatting and readability throughout ctrack.hpp +- Added accuracy error measurement in benchmarks +- Enhanced baseline data structure for benchmark comparison +- Better handling of multithreaded test timing precision + +### Version 1.0.0 +- Initial release with core functionality + +## Contributing + +We welcome and encourage contributions from the community! Your input helps make CTRACK better for everyone. Here's how you can contribute: + +### Pull Requests + +We're always excited to receive pull requests that improve CTRACK. When submitting a PR, please ensure: + +1. Your code adheres to the project's coding standards. +2. Your contributions are MIT License compliant. + +### Bug Reports + +Found a bug? We want to hear about it! Please open an issue on our GitHub repository with: + +1. A clear, descriptive title. +2. A detailed description of the issue, including steps to reproduce. +3. Your environment details (OS, compiler version, etc.). + +### Feature Requests + +Have an idea for a new feature? Feel free to open an issue to discuss it. We're always looking for ways to make CTRACK more useful. + +## Developer Guide + +### Performance Benchmarking + +CTRACK includes a comprehensive benchmark suite to measure and track library performance across different platforms and configurations. This benchmark is essential for: + +- Detecting performance regressions +- Validating optimization improvements +- Understanding CTRACK's overhead characteristics +- Ensuring consistent behavior across platforms + +#### Building the Benchmark + +The benchmark is not built by default. To enable it: + +```bash +mkdir build && cd build +cmake .. -DBUILD_BENCHMARK=ON +cmake --build . +``` + +#### Running the Benchmark + +The benchmark executable will be located at `build/benchmark/ctrack_benchmark`. + +Basic usage: + +```bash +# Run with default settings (50 million events) +./ctrack_benchmark + +# Run with custom event count and thread count +./ctrack_benchmark --events 10000000 --threads 8 + +# Enable verbose output +./ctrack_benchmark --verbose +``` + +#### Benchmark Metrics + +The benchmark measures four key areas: + +1. **Accuracy**: Validates that CTRACK correctly records function execution times by comparing against known timing patterns. + +2. **Overhead**: Measures the performance impact of CTRACK instrumentation by comparing execution times with and without tracking enabled. + +3. **Memory Usage**: Calculates the memory consumption per tracked event, helping understand scalability limits. + +4. **Calculation Time**: Measures how long it takes to process recorded events and generate results. + +#### Baseline Comparison + +The benchmark supports baseline recording and comparison, which is crucial for tracking performance changes over time: + +```bash +# Record a baseline +./ctrack_benchmark --record-baseline + +# Compare current performance against baseline +./ctrack_benchmark --compare-baseline + +# Use a custom baseline file +./ctrack_benchmark --baseline my_baseline.json --record-baseline +``` + +The baseline comparison will show: + +- 🟢 Green indicators for improvements +- 🔴 Red indicators for regressions +- Percentage changes for each metric + +#### Best Practices for Benchmarking + +1. **Consistent Environment**: Run benchmarks on the same hardware and with minimal background processes for reliable comparisons. + +2. **Multiple Runs**: Consider running the benchmark multiple times and averaging results to account for system variability. + +3. **Platform-Specific Baselines**: Maintain separate baselines for different platforms (Linux, Windows, macOS) as performance characteristics may vary. + +4. **CI Integration**: Consider integrating the benchmark into your CI pipeline to automatically detect performance regressions. + +5. **Release Validation**: Always run the benchmark before releases to ensure performance hasn't degraded. + +#### Interpreting Results + +- **Accuracy**: Should remain consistent (typically around 5% error due to timing precision limits) +- **Overhead**: Lower is better; typical values range from 50-150% depending on the workload +- **Memory/Event**: Should remain relatively constant; increases might indicate memory leaks +- **Calculation Time**: Should scale linearly with event count; non-linear scaling indicates algorithmic issues + +## Roadmap + +CTRACK is actively developed with exciting features planned for future releases: + +### Planned Features + +- **Flamegraph Export**: Generate flame graphs for visual performance analysis +- **JSON Export**: Export performance data in JSON format for integration with external tools +- **CI/CD Runners**: Native integration with CI/CD pipelines for automated performance regression testing +- **RDTSC Support**: Direct CPU cycle counting using RDTSC instruction for ultra-precise measurements +- **Light Version**: Minimal overhead variant for production environments with extreme performance requirements +- **Advanced Real-Time Reporting**: Live performance dashboards and real-time metric streaming capabilities + +We welcome community input on these features and suggestions for new ones. Please open an issue to discuss or contribute to any of these planned enhancements. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt new file mode 100644 index 0000000..1e014d6 --- /dev/null +++ b/benchmark/CMakeLists.txt @@ -0,0 +1,24 @@ +add_executable(ctrack_benchmark ctrack_benchmark.cpp) +target_link_libraries(ctrack_benchmark PRIVATE ctrack) + +# Enable threading support +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) +target_link_libraries(ctrack_benchmark PRIVATE Threads::Threads) + +# Add filesystem library if needed (for older compilers) +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9.0) + target_link_libraries(ctrack_benchmark PRIVATE stdc++fs) + endif() +elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9.0) + target_link_libraries(ctrack_benchmark PRIVATE c++fs) + endif() +endif() + +# Set output directory +set_target_properties(ctrack_benchmark + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/benchmark" +) \ No newline at end of file diff --git a/benchmark/ctrack_benchmark.cpp b/benchmark/ctrack_benchmark.cpp new file mode 100644 index 0000000..e6ff4be --- /dev/null +++ b/benchmark/ctrack_benchmark.cpp @@ -0,0 +1,811 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +// Configuration +struct BenchmarkConfig +{ + size_t total_events = 50'000'000; // Default 50 million events + size_t thread_count = std::thread::hardware_concurrency(); + bool record_baseline = false; + bool compare_baseline = false; + std::string baseline_file = "ctrack_baseline.json"; + bool verbose = false; +}; + +// Baseline data structure +struct BaselineData +{ + double accuracy_error_percent; + double accuracy_error_ms_per_event; + double overhead_percent; + double overhead_ms; + double overhead_ns_per_event; + double memory_bytes_per_event; + double calculation_time_ms; + double peak_calc_memory_mb; + size_t total_events; + size_t thread_count; + std::string timestamp; + std::string platform; +}; + +// Global config +BenchmarkConfig g_config; + +// Get current memory usage in bytes +size_t get_memory_usage() +{ +#ifdef _WIN32 + PROCESS_MEMORY_COUNTERS_EX pmc; + GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS *)&pmc, sizeof(pmc)); + return pmc.WorkingSetSize; +#else + struct rusage usage; + getrusage(RUSAGE_SELF, &usage); + return usage.ru_maxrss * 1024; // Convert KB to bytes on Linux +#endif +} + +// Precise busy wait function - waits for specified nanoseconds +void busy_wait_ns(int64_t nanoseconds) +{ + auto start = std::chrono::high_resolution_clock::now(); + auto target_duration = std::chrono::nanoseconds(nanoseconds); + + while (true) + { + auto now = std::chrono::high_resolution_clock::now(); + auto elapsed = now - start; + if (elapsed >= target_duration) + { + break; + } + } +} + +// Benchmark functions with predictable timing +void leaf_function(int depth) +{ + CTRACK_NAME("leaf_function"); + // Busy wait for 1 microsecond (1000 ns) + busy_wait_ns(1000); +} + +void level_3_function(int depth) +{ + CTRACK_NAME("level_3_function"); + // Busy wait for 500 ns + busy_wait_ns(500); + + // Call leaf function twice + leaf_function(depth + 1); + leaf_function(depth + 1); +} + +void level_2_function(int depth, int iterations) +{ + CTRACK_NAME("level_2_function"); + // Busy wait for 300 ns + busy_wait_ns(300); + + for (int i = 0; i < iterations; ++i) + { + level_3_function(depth + 1); + } +} + +void level_1_function(int iterations) +{ + CTRACK_NAME("level_1_function"); + // Busy wait for 200 ns + busy_wait_ns(200); + + level_2_function(1, iterations); +} + +// Version without CTRACK for overhead measurement +void leaf_function_no_track(int depth) +{ + busy_wait_ns(1000); +} + +void level_3_function_no_track(int depth) +{ + busy_wait_ns(500); + leaf_function_no_track(depth + 1); + leaf_function_no_track(depth + 1); +} + +void level_2_function_no_track(int depth, int iterations) +{ + busy_wait_ns(300); + for (int i = 0; i < iterations; ++i) + { + level_3_function_no_track(depth + 1); + } +} + +void level_1_function_no_track(int iterations) +{ + busy_wait_ns(200); + level_2_function_no_track(1, iterations); +} + +// Worker thread function +void benchmark_worker(size_t events_per_thread, std::atomic &start_flag) +{ + // Wait for start signal + while (!start_flag.load()) + { + std::this_thread::yield(); + } + + // Calculate iterations to reach target event count + // Each level_1 call generates: 1 + 1 + iterations * (1 + 2) events + // For iterations=10: 1 + 1 + 10 * 3 = 32 events per call + const int iterations = 10; + const int events_per_call = 2 + iterations * 3; + size_t calls_needed = events_per_thread / events_per_call; + + for (size_t i = 0; i < calls_needed; ++i) + { + level_1_function(iterations); + } +} + +// Worker thread function without tracking +void benchmark_worker_no_track(size_t events_per_thread, std::atomic &start_flag) +{ + while (!start_flag.load()) + { + std::this_thread::yield(); + } + + const int iterations = 10; + const int events_per_call = 2 + iterations * 3; + size_t calls_needed = events_per_thread / events_per_call; + + for (size_t i = 0; i < calls_needed; ++i) + { + level_1_function_no_track(iterations); + } +} + +// Parse timing from CTRACK results string for a specific function +double parse_function_timing(const std::string &results, const std::string &function_name) +{ + // Look for the Details section first + size_t details_pos = results.find("Details"); + if (details_pos == std::string::npos) + { + return -1.0; // Details section not found + } + + // Look for the function name after the Details section + size_t func_pos = results.find(function_name, details_pos); + if (func_pos == std::string::npos) + { + return -1.0; // Function not found in Details section + } + + // Find the line containing this function in the Details section + size_t line_start = results.rfind('\n', func_pos); + if (line_start == std::string::npos) + line_start = details_pos; + else + line_start++; // Skip the newline + + size_t line_end = results.find('\n', func_pos); + if (line_end == std::string::npos) + line_end = results.length(); + + std::string line = results.substr(line_start, line_end - line_start); + + // Look for the "time acc" column value (4th column after filename, function, line) + // Split by | and find the 4th field + std::vector fields; + std::istringstream iss(line); + std::string field; + + while (std::getline(iss, field, '|')) + { + // Trim whitespace + field.erase(0, field.find_first_not_of(" \t")); + field.erase(field.find_last_not_of(" \t") + 1); + if (!field.empty()) + { + fields.push_back(field); + } + } + + // The time acc should be in the 4th field (0-indexed: filename=0, function=1, line=2, time_acc=3) + if (fields.size() > 3) + { + std::string time_acc = fields[3]; + + // Parse value and unit from time_acc (e.g., "2.09 ms") + std::istringstream time_iss(time_acc); + double value; + std::string unit; + + if (time_iss >> value >> unit) + { + // Convert to nanoseconds based on unit + if (unit == "s") + return value * 1e9; + else if (unit == "ms") + return value * 1e6; + else if (unit == "mcs") + return value * 1e3; + else if (unit == "ns") + return value; + } + } + + return -1.0; // Could not parse +} + +// Measure accuracy by comparing known timings with CTRACK measurements +std::pair measure_accuracy() +{ + std::cout << "\n=== Measuring Accuracy ===" << std::endl; + + // Clear any previous tracking data by getting and discarding results + ctrack::result_as_string(); + + // Run a controlled test with known timings + const int test_iterations = 100; + for (int i = 0; i < test_iterations; ++i) + { + level_1_function(10); + } + + // Get results + auto results = ctrack::result_as_string(); + + // Expected timings per iteration (in nanoseconds): + // leaf_function: 1000ns (called 20 times per iteration) = 20,000ns total per iteration + // level_3_function: 500ns + 2*1000ns = 2500ns (called 10 times per iteration) = 25,000ns total per iteration + // level_2_function: 300ns + 10*2500ns = 25,300ns (called 1 time per iteration) = 25,300ns total per iteration + // level_1_function: 200ns + 25,300ns = 25,500ns (called 1 time per iteration) = 25,500ns total per iteration + + struct ExpectedTiming + { + std::string name; + double expected_total_ns; + int call_count; + }; + + std::vector expected_timings = { + {"leaf_function", 1000.0 * 20 * test_iterations, 20 * test_iterations}, + {"level_3_function", 2500.0 * 10 * test_iterations, 10 * test_iterations}, + {"level_2_function", 25300.0 * 1 * test_iterations, 1 * test_iterations}, + {"level_1_function", 25500.0 * 1 * test_iterations, 1 * test_iterations}}; + + double total_expected_time = 0.0; + double total_actual_time = 0.0; + double max_absolute_error = 0.0; + + if (g_config.verbose) + { + std::cout << "Function accuracy analysis:" << std::endl; + } + + for (const auto &timing : expected_timings) + { + double actual_ns = parse_function_timing(results, timing.name); + if (actual_ns > 0) + { + double expected_ns = timing.expected_total_ns; + double absolute_error = std::abs(actual_ns - expected_ns); + double percent_error = (absolute_error / expected_ns) * 100.0; + + total_expected_time += expected_ns; + total_actual_time += actual_ns; + max_absolute_error = (std::max)(max_absolute_error, absolute_error); + + if (g_config.verbose) + { + std::cout << " " << timing.name << ": expected " << expected_ns / 1e6 << " ms, got " + << actual_ns / 1e6 << " ms (error: " << percent_error << "%)" << std::endl; + } + } + else if (g_config.verbose) + { + std::cout << " " << timing.name << ": could not parse timing" << std::endl; + } + } + + double overall_error_percent = 0.0; + double overall_error_ms = 0.0; + + if (total_expected_time > 0) + { + double total_absolute_error = std::abs(total_actual_time - total_expected_time); + overall_error_percent = (total_absolute_error / total_expected_time) * 100.0; + + // Calculate total number of events across all functions + double total_events = 0; + for (const auto &timing : expected_timings) + { + total_events += timing.call_count; + } + + // Convert to milliseconds per event + overall_error_ms = (total_absolute_error / 1e6) / total_events; // Convert to milliseconds per event + } + + if (g_config.verbose) + { + std::cout << "Overall accuracy error: " << overall_error_percent << "% (" << overall_error_ms << " ms per event)" << std::endl; + } + + return {overall_error_percent, overall_error_ms}; +} + +// Measure overhead by comparing with and without CTRACK +std::tuple measure_overhead() +{ + std::cout << "\n=== Measuring Overhead ===" << std::endl; + + const size_t overhead_events = 1'000'000; // 1M events for overhead test + size_t events_per_thread = overhead_events / g_config.thread_count; + + // Measure without CTRACK + auto start_no_track = std::chrono::high_resolution_clock::now(); + { + std::vector threads; + std::atomic start_flag{false}; + + for (size_t i = 0; i < g_config.thread_count; ++i) + { + threads.emplace_back(benchmark_worker_no_track, events_per_thread, std::ref(start_flag)); + } + + start_flag = true; + + for (auto &t : threads) + { + t.join(); + } + } + auto end_no_track = std::chrono::high_resolution_clock::now(); + auto duration_no_track = std::chrono::duration_cast(end_no_track - start_no_track).count(); + + // Clear tracking data by getting and discarding results + ctrack::result_as_string(); + + // Measure with CTRACK + auto start_track = std::chrono::high_resolution_clock::now(); + { + std::vector threads; + std::atomic start_flag{false}; + + for (size_t i = 0; i < g_config.thread_count; ++i) + { + threads.emplace_back(benchmark_worker, events_per_thread, std::ref(start_flag)); + } + + start_flag = true; + + for (auto &t : threads) + { + t.join(); + } + } + auto end_track = std::chrono::high_resolution_clock::now(); + auto duration_track = std::chrono::duration_cast(end_track - start_track).count(); + + double overhead_percent = ((double)(duration_track - duration_no_track) / duration_no_track) * 100.0; + double overhead_ms = (duration_track - duration_no_track) / 1000.0; // Convert microseconds to milliseconds + double overhead_ns_per_event = ((duration_track - duration_no_track) * 1000.0) / overhead_events; // nanoseconds per event + + if (g_config.verbose) + { + std::cout << "Without CTRACK: " << duration_no_track << " µs" << std::endl; + std::cout << "With CTRACK: " << duration_track << " µs" << std::endl; + std::cout << "Overhead: " << overhead_percent << "% (" << overhead_ms << " ms total, " + << overhead_ns_per_event << " ns per event)" << std::endl; + } + + return {overhead_percent, overhead_ms, overhead_ns_per_event}; +} + +// Measure memory usage and calculation time +std::tuple measure_memory_and_calculation_time() +{ + std::cout << "\n=== Measuring Memory Usage and Calculation Time ===" << std::endl; + + // Clear any previous tracking data by getting and discarding results + ctrack::result_as_string(); + + // Measure initial memory + size_t initial_memory = get_memory_usage(); + + // Generate events + size_t events_per_thread = g_config.total_events / g_config.thread_count; + + if (g_config.verbose) + { + std::cout << "Generating " << g_config.total_events << " events across " + << g_config.thread_count << " threads..." << std::endl; + } + + auto gen_start = std::chrono::high_resolution_clock::now(); + { + std::vector threads; + std::atomic start_flag{false}; + + for (size_t i = 0; i < g_config.thread_count; ++i) + { + threads.emplace_back(benchmark_worker, events_per_thread, std::ref(start_flag)); + } + + start_flag = true; + + for (auto &t : threads) + { + t.join(); + } + } + auto gen_end = std::chrono::high_resolution_clock::now(); + + // Measure memory after event generation + size_t post_event_memory = get_memory_usage(); + size_t memory_used = post_event_memory - initial_memory; + double bytes_per_event = (double)memory_used / g_config.total_events; + + if (g_config.verbose) + { + auto gen_duration = std::chrono::duration_cast(gen_end - gen_start).count(); + std::cout << "Event generation took: " << gen_duration << " ms" << std::endl; + std::cout << "Memory used: " << memory_used / (1024.0 * 1024.0) << " MB" << std::endl; + std::cout << "Memory per event: " << bytes_per_event << " bytes" << std::endl; + } + + // Measure calculation time and peak memory usage + std::atomic monitoring{true}; + std::atomic peak_memory{post_event_memory}; + + // Start memory monitoring thread + std::thread monitor_thread([&monitoring, &peak_memory, initial_memory]() + { + while (monitoring.load()) { + size_t current_memory = get_memory_usage(); + size_t current_peak = peak_memory.load(); + while (current_memory > current_peak && + !peak_memory.compare_exchange_weak(current_peak, current_memory)) {} + std::this_thread::sleep_for(std::chrono::milliseconds(10)); // Poll every 10ms + } }); + + auto calc_start = std::chrono::high_resolution_clock::now(); + auto results = ctrack::result_as_string(); + auto calc_end = std::chrono::high_resolution_clock::now(); + + // Stop monitoring + monitoring = false; + monitor_thread.join(); + + auto calc_duration = std::chrono::duration_cast(calc_end - calc_start).count() / 1000.0; + double peak_calc_memory_mb = (peak_memory.load() - initial_memory) / (1024.0 * 1024.0); + + if (g_config.verbose) + { + std::cout << "Result calculation took: " << calc_duration << " ms" << std::endl; + std::cout << "Peak memory during calculation: " << peak_calc_memory_mb << " MB" << std::endl; + } + + return {bytes_per_event, calc_duration, peak_calc_memory_mb}; +} + +// Save baseline to file +void save_baseline(const BaselineData &data) +{ + std::ofstream file(g_config.baseline_file); + if (!file) + { + std::cerr << "Error: Could not open baseline file for writing: " << g_config.baseline_file << std::endl; + return; + } + + // Simple JSON format + file << "{\n"; + file << " \"accuracy_error_percent\": " << data.accuracy_error_percent << ",\n"; + file << " \"accuracy_error_ms_per_event\": " << data.accuracy_error_ms_per_event << ",\n"; + file << " \"overhead_percent\": " << data.overhead_percent << ",\n"; + file << " \"overhead_ms\": " << data.overhead_ms << ",\n"; + file << " \"overhead_ns_per_event\": " << data.overhead_ns_per_event << ",\n"; + file << " \"memory_bytes_per_event\": " << data.memory_bytes_per_event << ",\n"; + file << " \"calculation_time_ms\": " << data.calculation_time_ms << ",\n"; + file << " \"peak_calc_memory_mb\": " << data.peak_calc_memory_mb << ",\n"; + file << " \"total_events\": " << data.total_events << ",\n"; + file << " \"thread_count\": " << data.thread_count << ",\n"; + file << " \"timestamp\": \"" << data.timestamp << "\",\n"; + file << " \"platform\": \"" << data.platform << "\"\n"; + file << "}\n"; + + std::cout << "\nBaseline saved to: " << g_config.baseline_file << std::endl; +} + +// Load baseline from file +bool load_baseline(BaselineData &data) +{ + std::ifstream file(g_config.baseline_file); + if (!file) + { + return false; + } + + // Simple JSON parsing (production code would use a proper JSON library) + std::string line; + while (std::getline(file, line)) + { + if (line.find("\"accuracy_error_percent\":") != std::string::npos) + { + size_t pos = line.find(": ") + 2; + size_t end = line.find(",", pos); + data.accuracy_error_percent = std::stod(line.substr(pos, end - pos)); + } + else if (line.find("\"accuracy_error_ms_per_event\":") != std::string::npos) + { + size_t pos = line.find(": ") + 2; + size_t end = line.find(",", pos); + data.accuracy_error_ms_per_event = std::stod(line.substr(pos, end - pos)); + } + else if (line.find("\"overhead_percent\":") != std::string::npos) + { + size_t pos = line.find(": ") + 2; + size_t end = line.find(",", pos); + data.overhead_percent = std::stod(line.substr(pos, end - pos)); + } + else if (line.find("\"overhead_ms\":") != std::string::npos) + { + size_t pos = line.find(": ") + 2; + size_t end = line.find(",", pos); + data.overhead_ms = std::stod(line.substr(pos, end - pos)); + } + else if (line.find("\"overhead_ns_per_event\":") != std::string::npos) + { + size_t pos = line.find(": ") + 2; + size_t end = line.find(",", pos); + data.overhead_ns_per_event = std::stod(line.substr(pos, end - pos)); + } + else if (line.find("\"memory_bytes_per_event\":") != std::string::npos) + { + size_t pos = line.find(": ") + 2; + size_t end = line.find(",", pos); + data.memory_bytes_per_event = std::stod(line.substr(pos, end - pos)); + } + else if (line.find("\"calculation_time_ms\":") != std::string::npos) + { + size_t pos = line.find(": ") + 2; + size_t end = line.find(",", pos); + data.calculation_time_ms = std::stod(line.substr(pos, end - pos)); + } + else if (line.find("\"peak_calc_memory_mb\":") != std::string::npos) + { + size_t pos = line.find(": ") + 2; + size_t end = line.find(",", pos); + data.peak_calc_memory_mb = std::stod(line.substr(pos, end - pos)); + } + else if (line.find("\"total_events\":") != std::string::npos) + { + size_t pos = line.find(": ") + 2; + size_t end = line.find(",", pos); + data.total_events = std::stoull(line.substr(pos, end - pos)); + } + else if (line.find("\"thread_count\":") != std::string::npos) + { + size_t pos = line.find(": ") + 2; + size_t end = line.find(",", pos); + data.thread_count = std::stoull(line.substr(pos, end - pos)); + } + } + + return true; +} + +// Compare current results with baseline +void compare_with_baseline(const BaselineData ¤t) +{ + BaselineData baseline; + if (!load_baseline(baseline)) + { + std::cerr << "Error: Could not load baseline file: " << g_config.baseline_file << std::endl; + return; + } + + std::cout << "\n=== Baseline Comparison ===" << std::endl; + std::cout << std::fixed << std::setprecision(2); + auto print_comparison = [](const std::string &metric, double baseline_val, double current_val, bool lower_is_better = true) + { + double diff = current_val - baseline_val; + double percent_change = (diff / baseline_val) * 100.0; + + std::string direction = (diff > 0) ? "increased" : "decreased"; + std::string indicator = (lower_is_better ? (diff > 0 ? "worse" : "better") : (diff > 0 ? "better" : "worse")); + + std::cout << metric << ":\n"; + std::cout << " Baseline: " << baseline_val << "\n"; + std::cout << " Current: " << current_val << "\n"; + std::cout << " Change: " << indicator << " - " << std::abs(percent_change) << "% " << direction << "\n\n"; + }; + + print_comparison("Accuracy Error %", baseline.accuracy_error_percent, current.accuracy_error_percent); + print_comparison("Accuracy Error (ms/event)", baseline.accuracy_error_ms_per_event, current.accuracy_error_ms_per_event); + print_comparison("Overhead %", std::abs(baseline.overhead_percent), std::abs(current.overhead_percent)); + print_comparison("Overhead Time (ms)", std::abs(baseline.overhead_ms), std::abs(current.overhead_ms)); + print_comparison("Overhead per Event (ns)", baseline.overhead_ns_per_event, current.overhead_ns_per_event); + print_comparison("Memory/Event (bytes)", baseline.memory_bytes_per_event, current.memory_bytes_per_event); + print_comparison("Calculation Time (ms)", baseline.calculation_time_ms, current.calculation_time_ms); + print_comparison("Peak Calc Memory (MB)", baseline.peak_calc_memory_mb, current.peak_calc_memory_mb); +} + +// Get platform string +std::string get_platform() +{ +#ifdef _WIN32 + return "Windows"; +#elif __APPLE__ + return "macOS"; +#elif __linux__ + return "Linux"; +#else + return "Unknown"; +#endif +} + +// Get current timestamp +std::string get_timestamp() +{ + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + std::stringstream ss; +#ifdef _WIN32 + struct tm time_info; + localtime_s(&time_info, &time_t); + ss << std::put_time(&time_info, "%Y-%m-%d %H:%M:%S"); +#else + ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S"); +#endif + return ss.str(); +} + +// Print usage +void print_usage(const char *program_name) +{ + std::cout << "Usage: " << program_name << " [options]\n"; + std::cout << "Options:\n"; + std::cout << " --events Number of events to generate (default: 50000000)\n"; + std::cout << " --threads Number of threads to use (default: hardware concurrency)\n"; + std::cout << " --baseline Baseline file path (default: ctrack_baseline.json)\n"; + std::cout << " --record-baseline Record current results as baseline\n"; + std::cout << " --compare-baseline Compare results with baseline\n"; + std::cout << " --verbose Enable verbose output\n"; + std::cout << " --help Show this help message\n"; +} + +// Parse command line arguments +bool parse_args(int argc, char *argv[]) +{ + for (int i = 1; i < argc; ++i) + { + std::string arg = argv[i]; + + if (arg == "--help") + { + print_usage(argv[0]); + return false; + } + else if (arg == "--events" && i + 1 < argc) + { + g_config.total_events = std::stoull(argv[++i]); + } + else if (arg == "--threads" && i + 1 < argc) + { + g_config.thread_count = std::stoull(argv[++i]); + } + else if (arg == "--baseline" && i + 1 < argc) + { + g_config.baseline_file = argv[++i]; + } + else if (arg == "--record-baseline") + { + g_config.record_baseline = true; + } + else if (arg == "--compare-baseline") + { + g_config.compare_baseline = true; + } + else if (arg == "--verbose") + { + g_config.verbose = true; + } + else + { + std::cerr << "Unknown option: " << arg << std::endl; + print_usage(argv[0]); + return false; + } + } + + return true; +} + +int main(int argc, char *argv[]) +{ + if (!parse_args(argc, argv)) + { + return 1; + } + + std::cout << "CTRACK Comprehensive Benchmark\n"; + std::cout << "==============================\n"; + std::cout << "Total events: " << g_config.total_events << "\n"; + std::cout << "Thread count: " << g_config.thread_count << "\n"; + std::cout << "Events per thread: " << g_config.total_events / g_config.thread_count << "\n"; + + // Run benchmarks + auto [accuracy_error_percent, accuracy_error_ms_per_event] = measure_accuracy(); + auto [overhead_percent, overhead_ms, overhead_ns_per_event] = measure_overhead(); + auto [bytes_per_event, calc_time, peak_calc_memory] = measure_memory_and_calculation_time(); + + // Prepare results + BaselineData current_data; + current_data.accuracy_error_percent = accuracy_error_percent; + current_data.accuracy_error_ms_per_event = accuracy_error_ms_per_event; + current_data.overhead_percent = overhead_percent; + current_data.overhead_ms = overhead_ms; + current_data.overhead_ns_per_event = overhead_ns_per_event; + current_data.memory_bytes_per_event = bytes_per_event; + current_data.calculation_time_ms = calc_time; + current_data.peak_calc_memory_mb = peak_calc_memory; + current_data.total_events = g_config.total_events; + current_data.thread_count = g_config.thread_count; + current_data.timestamp = get_timestamp(); + current_data.platform = get_platform(); + + // Print summary + std::cout << "\n=== Benchmark Results ===" << std::endl; + std::cout << std::fixed << std::setprecision(2); + std::cout << "Accuracy error: " << accuracy_error_percent << "% (" << accuracy_error_ms_per_event << " ms per event)" << std::endl; + std::cout << "Overhead: " << overhead_percent << "% (" << overhead_ms << " ms total, " + << overhead_ns_per_event << " ns per event)" << std::endl; + std::cout << "Memory per event: " << bytes_per_event << " bytes" << std::endl; + std::cout << "Calculation time: " << calc_time << " ms" << std::endl; + std::cout << "Peak calculation memory: " << peak_calc_memory << " MB" << std::endl; + + // Handle baseline operations + if (g_config.record_baseline) + { + save_baseline(current_data); + } + + if (g_config.compare_baseline) + { + compare_with_baseline(current_data); + } + + return 0; +} \ No newline at end of file diff --git a/cmake/add_warning.cmake b/cmake/add_warning.cmake index c70adb4..407833a 100644 --- a/cmake/add_warning.cmake +++ b/cmake/add_warning.cmake @@ -1,42 +1,42 @@ -# Taken from https://github.com/ClickHouse/ClickHouse/blob/master/cmake/add_warnings.cmake - -include (CheckCXXCompilerFlag) - -# Try to add -Wflag if compiler supports it -macro (add_warning flag) - string (REPLACE "-" "_" underscored_flag ${flag}) - string (REPLACE "+" "x" underscored_flag ${underscored_flag}) - - check_cxx_compiler_flag("-W${flag}" SUPPORTS_CXXFLAG_${underscored_flag}) - - if (SUPPORTS_CXXFLAG_${underscored_flag}) - set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -W${flag}") - else () - message (STATUS "Flag -W${flag} is unsupported") - endif () - -endmacro () - -# Try to add -Wno flag if compiler supports it -macro (no_warning flag) - add_warning(no-${flag}) -endmacro () - - -# The same but only for specified target. -macro (target_add_warning target flag) - string (REPLACE "-" "_" underscored_flag ${flag}) - string (REPLACE "+" "x" underscored_flag ${underscored_flag}) - - check_cxx_compiler_flag("-W${flag}" SUPPORTS_CXXFLAG_${underscored_flag}) - - if (SUPPORTS_CXXFLAG_${underscored_flag}) - target_compile_options (${target} PRIVATE "-W${flag}") - else () - message (STATUS "Flag -W${flag} is unsupported") - endif () -endmacro () - -macro (target_no_warning target flag) - target_add_warning(${target} no-${flag}) -endmacro () +# Taken from https://github.com/ClickHouse/ClickHouse/blob/master/cmake/add_warnings.cmake + +include (CheckCXXCompilerFlag) + +# Try to add -Wflag if compiler supports it +macro (add_warning flag) + string (REPLACE "-" "_" underscored_flag ${flag}) + string (REPLACE "+" "x" underscored_flag ${underscored_flag}) + + check_cxx_compiler_flag("-W${flag}" SUPPORTS_CXXFLAG_${underscored_flag}) + + if (SUPPORTS_CXXFLAG_${underscored_flag}) + set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -W${flag}") + else () + message (STATUS "Flag -W${flag} is unsupported") + endif () + +endmacro () + +# Try to add -Wno flag if compiler supports it +macro (no_warning flag) + add_warning(no-${flag}) +endmacro () + + +# The same but only for specified target. +macro (target_add_warning target flag) + string (REPLACE "-" "_" underscored_flag ${flag}) + string (REPLACE "+" "x" underscored_flag ${underscored_flag}) + + check_cxx_compiler_flag("-W${flag}" SUPPORTS_CXXFLAG_${underscored_flag}) + + if (SUPPORTS_CXXFLAG_${underscored_flag}) + target_compile_options (${target} PRIVATE "-W${flag}") + else () + message (STATUS "Flag -W${flag} is unsupported") + endif () +endmacro () + +macro (target_no_warning target flag) + target_add_warning(${target} no-${flag}) +endmacro () diff --git a/cmake/warnings.cmake b/cmake/warnings.cmake index 11c2606..91f1283 100644 --- a/cmake/warnings.cmake +++ b/cmake/warnings.cmake @@ -1,46 +1,46 @@ -# Taken from https://github.com/ClickHouse/ClickHouse/blob/master/cmake/warnings.cmake, -# slightly modified to fit into ctrack style. - -set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra") - -# Control maximum size of stack frames. It can be important if the code is run in fibers with small stack size. -# Only in release build because debug has too large stack frames. -if ((NOT CMAKE_BUILD_TYPE_UC STREQUAL "DEBUG") AND (NOT SANITIZE) AND (NOT CMAKE_CXX_COMPILER_ID MATCHES "AppleClang")) - add_warning(frame-larger-than=65536) -endif () - -# Add some warnings that are not available even with -Wall -Wextra -Wpedantic. -# We want to get everything out of the compiler for code quality. -add_warning(everything) -add_warning(pedantic) -no_warning(zero-length-array) -no_warning(c++98-compat-pedantic) -no_warning(c++98-compat) -no_warning(c++20-compat) # Use constinit in C++20 without warnings -no_warning(sign-conversion) -no_warning(implicit-int-conversion) -no_warning(implicit-int-float-conversion) -no_warning(ctad-maybe-unsupported) # clang 9+, linux-only -no_warning(disabled-macro-expansion) -no_warning(documentation-unknown-command) -no_warning(double-promotion) -no_warning(exit-time-destructors) -no_warning(float-equal) -no_warning(global-constructors) -no_warning(missing-prototypes) -no_warning(missing-variable-declarations) -no_warning(padded) -no_warning(switch-enum) -no_warning(undefined-func-template) -no_warning(unused-template) -no_warning(vla) -no_warning(weak-template-vtables) -no_warning(weak-vtables) -no_warning(thread-safety-negative) # experimental flag, too many false positives -no_warning(enum-constexpr-conversion) # breaks magic-enum library in clang-16 -no_warning(unsafe-buffer-usage) # too aggressive -no_warning(switch-default) # conflicts with "defaults in a switch covering all enum values" - -# Styling decisions for ctrack -no_warning(shadow-field-in-constructor) +# Taken from https://github.com/ClickHouse/ClickHouse/blob/master/cmake/warnings.cmake, +# slightly modified to fit into ctrack style. + +set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra") + +# Control maximum size of stack frames. It can be important if the code is run in fibers with small stack size. +# Only in release build because debug has too large stack frames. +if ((NOT CMAKE_BUILD_TYPE_UC STREQUAL "DEBUG") AND (NOT SANITIZE) AND (NOT CMAKE_CXX_COMPILER_ID MATCHES "AppleClang")) + add_warning(frame-larger-than=65536) +endif () + +# Add some warnings that are not available even with -Wall -Wextra -Wpedantic. +# We want to get everything out of the compiler for code quality. +add_warning(everything) +add_warning(pedantic) +no_warning(zero-length-array) +no_warning(c++98-compat-pedantic) +no_warning(c++98-compat) +no_warning(c++20-compat) # Use constinit in C++20 without warnings +no_warning(sign-conversion) +no_warning(implicit-int-conversion) +no_warning(implicit-int-float-conversion) +no_warning(ctad-maybe-unsupported) # clang 9+, linux-only +no_warning(disabled-macro-expansion) +no_warning(documentation-unknown-command) +no_warning(double-promotion) +no_warning(exit-time-destructors) +no_warning(float-equal) +no_warning(global-constructors) +no_warning(missing-prototypes) +no_warning(missing-variable-declarations) +no_warning(padded) +no_warning(switch-enum) +no_warning(undefined-func-template) +no_warning(unused-template) +no_warning(vla) +no_warning(weak-template-vtables) +no_warning(weak-vtables) +no_warning(thread-safety-negative) # experimental flag, too many false positives +no_warning(enum-constexpr-conversion) # breaks magic-enum library in clang-16 +no_warning(unsafe-buffer-usage) # too aggressive +no_warning(switch-default) # conflicts with "defaults in a switch covering all enum values" + +# Styling decisions for ctrack +no_warning(shadow-field-in-constructor) no_warning(newline-eof) \ No newline at end of file diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index e29825f..2e82e61 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,14 +1,15 @@ - -# Create executables for each example -add_executable(basic_singlethreaded basic_singlethreaded.cpp) -add_executable(multithreaded_prime_counter multithreaded_prime_counter.cpp) -add_executable(ctrack_overhead_test ctrack_overhead_test.cpp) -add_executable(high_variance_pi_estimation high_variance_pi_estimation.cpp) -add_executable(complex_multithreaded_puzzle complex_multithreaded_puzzle.cpp) - -# Link the ctrack library to each example -target_link_libraries(basic_singlethreaded PRIVATE ctrack) -target_link_libraries(multithreaded_prime_counter PRIVATE ctrack) -target_link_libraries(ctrack_overhead_test PRIVATE ctrack) -target_link_libraries(high_variance_pi_estimation PRIVATE ctrack) -target_link_libraries(complex_multithreaded_puzzle PRIVATE ctrack) \ No newline at end of file + +# Create executables for each example +add_executable(basic_singlethreaded basic_singlethreaded.cpp) +add_executable(multithreaded_prime_counter multithreaded_prime_counter.cpp) +add_executable(ctrack_overhead_test ctrack_overhead_test.cpp) +add_executable(high_variance_pi_estimation high_variance_pi_estimation.cpp) +add_executable(complex_multithreaded_puzzle complex_multithreaded_puzzle.cpp) + + +# Link the ctrack library to each example +target_link_libraries(basic_singlethreaded PRIVATE ctrack) +target_link_libraries(multithreaded_prime_counter PRIVATE ctrack) +target_link_libraries(ctrack_overhead_test PRIVATE ctrack) +target_link_libraries(high_variance_pi_estimation PRIVATE ctrack) +target_link_libraries(complex_multithreaded_puzzle PRIVATE ctrack) diff --git a/examples/basic_singlethreaded.cpp b/examples/basic_singlethreaded.cpp index 9c7efb8..27fcd81 100644 --- a/examples/basic_singlethreaded.cpp +++ b/examples/basic_singlethreaded.cpp @@ -1,46 +1,46 @@ -#include -#include -#include "ctrack.hpp" // Assuming this is the header for your library - -// Function to calculate the sum of squares from 1 to n -double sum_of_squares(int n) { - CTRACK; // Start tracking this function - double sum = 0; - for (int i = 1; i <= n; ++i) { - sum += i * i; - } - return sum; -} - -// Function to calculate the factorial of n -unsigned long long factorial(int n) { - CTRACK; // Start tracking this function - if (n <= 1) return 1; - return n * factorial(n - 1); -} - -// Function to calculate the nth Fibonacci number -int fibonacci(int n) { - CTRACK; // Start tracking this function - if (n <= 1) return n; - return fibonacci(n - 1) + fibonacci(n - 2); -} - -int main() { - - - // Perform calculations - double sum = sum_of_squares(1000); - unsigned long long fact = factorial(20); - int fib = fibonacci(30); - - // Print results - std::cout << "Sum of squares: " << sum << std::endl; - std::cout << "Factorial: " << fact << std::endl; - std::cout << "Fibonacci: " << fib << std::endl; - - // Print benchmarking results - ctrack::result_print(); - //std::cout << ctrack::result_as_string() << std::endl; - return 0; +#include +#include +#include "ctrack.hpp" // Assuming this is the header for your library + +// Function to calculate the sum of squares from 1 to n +double sum_of_squares(int n) { + CTRACK; // Start tracking this function + double sum = 0; + for (int i = 1; i <= n; ++i) { + sum += i * i; + } + return sum; +} + +// Function to calculate the factorial of n +unsigned long long factorial(int n) { + CTRACK; // Start tracking this function + if (n <= 1) return 1; + return n * factorial(n - 1); +} + +// Function to calculate the nth Fibonacci number +int fibonacci(int n) { + CTRACK; // Start tracking this function + if (n <= 1) return n; + return fibonacci(n - 1) + fibonacci(n - 2); +} + +int main() { + + + // Perform calculations + double sum = sum_of_squares(1000); + unsigned long long fact = factorial(20); + int fib = fibonacci(30); + + // Print results + std::cout << "Sum of squares: " << sum << std::endl; + std::cout << "Factorial: " << fact << std::endl; + std::cout << "Fibonacci: " << fib << std::endl; + + // Print benchmarking results + ctrack::result_print(); + //std::cout << ctrack::result_as_string() << std::endl; + return 0; } \ No newline at end of file diff --git a/examples/complex_multithreaded_puzzle.cpp b/examples/complex_multithreaded_puzzle.cpp index 562b527..4c563be 100644 --- a/examples/complex_multithreaded_puzzle.cpp +++ b/examples/complex_multithreaded_puzzle.cpp @@ -1,87 +1,87 @@ -#include -#include -#include -#include -#include -#include -#include -#include "ctrack.hpp" - -std::mutex cout_mutex; -std::atomic global_counter(0); - -void sleepy_function(int ms) { - CTRACK; - std::this_thread::sleep_for(std::chrono::milliseconds(ms)); -} - -int recursive_work(int n) { - CTRACK; - if (n <= 1) return 1; - sleepy_function(1); - return recursive_work(n - 1) + recursive_work(n - 2); -} - -void nested_function_a(int depth) { - CTRACK; - if (depth > 0) { - sleepy_function(5); - nested_function_a(depth - 1); - } -} - -void nested_function_b(int depth) { - CTRACK; - if (depth > 0) { - sleepy_function(3); - nested_function_b(depth - 1); - } - if (depth == 3) { // Hidden slow path - sleepy_function(100); - } -} - -void complex_operation(int id) { - CTRACK; - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_int_distribution<> dis(1, 10); - - for (int i = 0; i < 5; ++i) { - int random_num = dis(gen); - if (random_num % 2 == 0) { - nested_function_a(random_num); - } else { - nested_function_b(random_num); - } - - if (random_num == 7) { // Rare slow path - recursive_work(20); - } - - global_counter.fetch_add(1, std::memory_order_relaxed); - } - - { - std::lock_guard lock(cout_mutex); - std::cout << "Thread " << id << " completed." << std::endl; - } -} - -int main() { - const int thread_count = 4; - std::vector threads; - - for (int i = 0; i < thread_count; ++i) { - threads.emplace_back(complex_operation, i); - } - - for (auto& thread : threads) { - thread.join(); - } - - std::cout << "Global counter: " << global_counter << std::endl; - ctrack::result_print(); - - return 0; +#include +#include +#include +#include +#include +#include +#include +#include "ctrack.hpp" + +std::mutex cout_mutex; +std::atomic global_counter(0); + +void sleepy_function(int ms) { + CTRACK; + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); +} + +int recursive_work(int n) { + CTRACK; + if (n <= 1) return 1; + sleepy_function(1); + return recursive_work(n - 1) + recursive_work(n - 2); +} + +void nested_function_a(int depth) { + CTRACK; + if (depth > 0) { + sleepy_function(5); + nested_function_a(depth - 1); + } +} + +void nested_function_b(int depth) { + CTRACK; + if (depth > 0) { + sleepy_function(3); + nested_function_b(depth - 1); + } + if (depth == 3) { // Hidden slow path + sleepy_function(100); + } +} + +void complex_operation(int id) { + CTRACK; + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> dis(1, 10); + + for (int i = 0; i < 5; ++i) { + int random_num = dis(gen); + if (random_num % 2 == 0) { + nested_function_a(random_num); + } else { + nested_function_b(random_num); + } + + if (random_num == 7) { // Rare slow path + recursive_work(20); + } + + global_counter.fetch_add(1, std::memory_order_relaxed); + } + + { + std::lock_guard lock(cout_mutex); + std::cout << "Thread " << id << " completed." << std::endl; + } +} + +int main() { + const int thread_count = 4; + std::vector threads; + + for (int i = 0; i < thread_count; ++i) { + threads.emplace_back(complex_operation, i); + } + + for (auto& thread : threads) { + thread.join(); + } + + std::cout << "Global counter: " << global_counter << std::endl; + ctrack::result_print(); + + return 0; } \ No newline at end of file diff --git a/examples/ctrack_overhead_test.cpp b/examples/ctrack_overhead_test.cpp index 284def1..6ee8da8 100644 --- a/examples/ctrack_overhead_test.cpp +++ b/examples/ctrack_overhead_test.cpp @@ -1,46 +1,46 @@ -#include -#include -#include -#include -#include "ctrack.hpp" - -void empty_function() { - CTRACK; - // Function is intentionally empty -} - -void run_empty_functions(int count) { - for (int i = 0; i < count; ++i) { - empty_function(); - } -} - -int main() { - const int total_calls = 10000000; // 10 million calls in total - const int thread_count = std::thread::hardware_concurrency(); - const int calls_per_thread = total_calls / thread_count; - - std::cout << "Running performance test with " << thread_count << " threads." << std::endl; - std::cout << "Total function calls: " << total_calls << std::endl; - - std::vector threads; - - auto start_time = std::chrono::high_resolution_clock::now(); - - for (int i = 0; i < thread_count; ++i) { - threads.emplace_back(run_empty_functions, calls_per_thread); - } - - for (auto& thread : threads) { - thread.join(); - } - - auto end_time = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(end_time - start_time); - - std::cout << "Total execution time: " << duration.count() << " milliseconds" << std::endl; - - ctrack::result_print(); - - return 0; +#include +#include +#include +#include +#include "ctrack.hpp" + +void empty_function() { + CTRACK; + // Function is intentionally empty +} + +void run_empty_functions(int count) { + for (int i = 0; i < count; ++i) { + empty_function(); + } +} + +int main() { + const int total_calls = 10000000; // 10 million calls in total + const int thread_count = std::thread::hardware_concurrency(); + const int calls_per_thread = total_calls / thread_count; + + std::cout << "Running performance test with " << thread_count << " threads." << std::endl; + std::cout << "Total function calls: " << total_calls << std::endl; + + std::vector threads; + + auto start_time = std::chrono::high_resolution_clock::now(); + + for (int i = 0; i < thread_count; ++i) { + threads.emplace_back(run_empty_functions, calls_per_thread); + } + + for (auto& thread : threads) { + thread.join(); + } + + auto end_time = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end_time - start_time); + + std::cout << "Total execution time: " << duration.count() << " milliseconds" << std::endl; + + ctrack::result_print(); + + return 0; } \ No newline at end of file diff --git a/examples/high_variance_pi_estimation.cpp b/examples/high_variance_pi_estimation.cpp index 13742d3..2512b75 100644 --- a/examples/high_variance_pi_estimation.cpp +++ b/examples/high_variance_pi_estimation.cpp @@ -1,51 +1,51 @@ -#include -#include -#include -#include -#include "ctrack.hpp" - -double estimate_pi(int points) { - CTRACK; - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_real_distribution<> dis(-1.0, 1.0); - - int inside_circle = 0; - for (int i = 0; i < points; ++i) { - double x = dis(gen); - double y = dis(gen); - if (x*x + y*y <= 1.0) { - inside_circle++; - } - } - - return 4.0 * inside_circle / points; -} - -void run_estimations(int iterations, int points_per_estimation) { - for (int i = 0; i < iterations; ++i) { - estimate_pi(points_per_estimation); - } -} - -int main() { - const int total_estimations = 1000; - const int points_per_estimation = 100000; - const int thread_count = std::thread::hardware_concurrency(); - const int estimations_per_thread = total_estimations / thread_count; - - std::vector threads; - - for (int i = 0; i < thread_count; ++i) { - threads.emplace_back(run_estimations, estimations_per_thread, points_per_estimation); - } - - for (auto& thread : threads) { - thread.join(); - } - - std::cout << "Completed " << total_estimations << " pi estimations" << std::endl; - ctrack::result_print(); - - return 0; +#include +#include +#include +#include +#include "ctrack.hpp" + +double estimate_pi(int points) { + CTRACK; + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution<> dis(-1.0, 1.0); + + int inside_circle = 0; + for (int i = 0; i < points; ++i) { + double x = dis(gen); + double y = dis(gen); + if (x*x + y*y <= 1.0) { + inside_circle++; + } + } + + return 4.0 * inside_circle / points; +} + +void run_estimations(int iterations, int points_per_estimation) { + for (int i = 0; i < iterations; ++i) { + estimate_pi(points_per_estimation); + } +} + +int main() { + const int total_estimations = 1000; + const int points_per_estimation = 100000; + const int thread_count = std::thread::hardware_concurrency(); + const int estimations_per_thread = total_estimations / thread_count; + + std::vector threads; + + for (int i = 0; i < thread_count; ++i) { + threads.emplace_back(run_estimations, estimations_per_thread, points_per_estimation); + } + + for (auto& thread : threads) { + thread.join(); + } + + std::cout << "Completed " << total_estimations << " pi estimations" << std::endl; + ctrack::result_print(); + + return 0; } \ No newline at end of file diff --git a/examples/multithreaded_prime_counter.cpp b/examples/multithreaded_prime_counter.cpp index 170d377..7488c83 100644 --- a/examples/multithreaded_prime_counter.cpp +++ b/examples/multithreaded_prime_counter.cpp @@ -1,50 +1,50 @@ -#include -#include -#include -#include -#include -#include "ctrack.hpp" - -std::atomic primeCount(0); - -bool isPrime(int n) { - CTRACK; - if (n <= 1) return false; - for (int i = 2; i <= std::sqrt(n); ++i) { - if (n % i == 0) return false; - } - return true; -} - -void countPrimesInRange(int start, int end) { - CTRACK; - for (int i = start; i <= end; ++i) { - if (isPrime(i)) { - primeCount++; - } - } -} - -int main() { - const int totalNumbers = 1000000; - const int threadCount = 8; - const int numbersPerThread = totalNumbers / threadCount; - - std::vector threads; - - for (int i = 0; i < threadCount; ++i) { - int start = i * numbersPerThread + 1; - int end = (i == threadCount - 1) ? totalNumbers : (i + 1) * numbersPerThread; - threads.emplace_back(countPrimesInRange, start, end); - } - - for (auto& thread : threads) { - thread.join(); - } - - std::cout << "Total primes found: " << primeCount << std::endl; - - ctrack::result_print(); - - return 0; +#include +#include +#include +#include +#include +#include "ctrack.hpp" + +std::atomic primeCount(0); + +bool isPrime(int n) { + CTRACK; + if (n <= 1) return false; + for (int i = 2; i <= std::sqrt(n); ++i) { + if (n % i == 0) return false; + } + return true; +} + +void countPrimesInRange(int start, int end) { + CTRACK; + for (int i = start; i <= end; ++i) { + if (isPrime(i)) { + primeCount++; + } + } +} + +int main() { + const int totalNumbers = 1000000; + const int threadCount = 8; + const int numbersPerThread = totalNumbers / threadCount; + + std::vector threads; + + for (int i = 0; i < threadCount; ++i) { + int start = i * numbersPerThread + 1; + int end = (i == threadCount - 1) ? totalNumbers : (i + 1) * numbersPerThread; + threads.emplace_back(countPrimesInRange, start, end); + } + + for (auto& thread : threads) { + thread.join(); + } + + std::cout << "Total primes found: " << primeCount << std::endl; + + ctrack::result_print(); + + return 0; } \ No newline at end of file diff --git a/include/ctrack.hpp b/include/ctrack.hpp index 9b57dcd..52d309c 100644 --- a/include/ctrack.hpp +++ b/include/ctrack.hpp @@ -1,988 +1,1296 @@ -// Copyright (c) 2024 COMPAILE Solutions GmbH - Grischa Hauser -// License:MIT License See LICENSE for the full license. -// https://github.com/Compaile/ctrack -#pragma once -#ifndef CTRACK_H -#define CTRACK_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifndef CTRACK_DISABLE_EXECUTION_POLICY -#include -#endif -#include -#include -#include -#include -#include -#include - -#define CTRACK_VERSION_MAJOR 1 -#define CTRACK_VERSION_MINOR 0 -#define CTRACK_VERSION_PATCH 2 - -// Helper macro to convert a numeric value to a string -#define STRINGIFY(x) #x -#define TOSTRING(x) STRINGIFY(x) - -// Create a string version -#define CTRACK_VERSION_STRING \ - TOSTRING(CTRACK_VERSION_MAJOR) "_" \ - TOSTRING(CTRACK_VERSION_MINOR) "_" \ - TOSTRING(CTRACK_VERSION_PATCH) - -// Use the version string as the namespace name -#define CTRACK_VERSION_NAMESPACE v##CTRACK_VERSION_MAJOR##_##CTRACK_VERSION_MINOR##_##CTRACK_VERSION_PATCH - -namespace ctrack { - - inline namespace CTRACK_VERSION_NAMESPACE { -#ifndef CTRACK_DISABLE_EXECUTION_POLICY - constexpr auto execution_policy = std::execution::par_unseq; -#define OPT_EXEC_POLICY execution_policy, -#else -#define OPT_EXEC_POLICY -#endif - - template - auto sum_field(const std::vector& vec, Field T::* field) { - using FieldType = std::decay_t().*field)>; - return std::transform_reduce( - OPT_EXEC_POLICY - vec.begin(), vec.end(), - FieldType{}, - std::plus<>(), - [field](const auto& item) { return item.*field; } - ); - } - - template - auto sum_squared_field(const std::vector& values, Field T::* field) { - using FieldType = std::decay_t().*field)>; - return std::transform_reduce( - OPT_EXEC_POLICY - values.begin(), values.end(), - FieldType{}, - std::plus<>(), - [field](const T& v) { - return (v.*field) * (v.*field); - } - ); - } - - template - double calculate_std_dev_field(std::vector& values, Field T::* field, const double mean) { - double res = std::transform_reduce( - OPT_EXEC_POLICY - values.begin(), values.end(), - 0.0, - std::plus<>(), - [mean, field](const T& v) { - return std::pow(static_cast(v.*field) - mean, 2); - } - ); - - return sqrt(res / values.size()); - } - - template - auto get_distinct_field_values(const std::vector& vec, Field T::* field) { - std::set().*field)>> distinct_values; - - std::transform(vec.begin(), vec.end(), - std::inserter(distinct_values, distinct_values.end()), - [field](const T& item) { return item.*field; }); - return distinct_values; - } - - template - size_t count_distinct_field_values(const std::vector& vec, Field T::* field) { - return get_distinct_field_values(vec, field).size(); - } - - template - void order_pointer_vector_by_field(std::vector& vec, MemberType StructType::* member, bool asc = true) { - std::sort(OPT_EXEC_POLICY vec.begin(), vec.end(), - [member, asc](const StructType* a, const StructType* b) { - if (asc) - return (a->*member) < (b->*member); - else - return (a->*member) > (b->*member); - }); - } - - template - size_t countAllEvents(const std::deque>& events) { - return std::transform_reduce( - OPT_EXEC_POLICY - events.begin(), events.end(), - size_t(0), - std::plus<>(), - [](const auto& vec) { - return vec.size(); - } - ); - } - - struct ColorScheme { - std::string border_color; - std::string header_color; - std::string top_header_color; - std::string row_color; - - ColorScheme(const std::string& border, - const std::string& header, - const std::string& top_header, - const std::string& row) - : border_color(border), - header_color(header), - top_header_color(top_header), - row_color(row) {} - }; - - static inline const ColorScheme default_colors{ - "\033[38;5;24m", // Darker Blue (Border) - "\033[1;38;5;135m", // Purple (Header) - "\033[1;38;5;92m", // Darker Purple (Top Header) - "\033[38;5;39m" // Light Blue (Row) - }; - - // Alternate color scheme (still nice to read on terminals) - static inline const ColorScheme alternate_colors{ - "\033[38;5;28m", // Dark Green (Border) - "\033[1;38;5;208m", // Orange (Header) - "\033[1;38;5;130m", // Dark Orange (Top Header) - "\033[38;5;71m" // Light Green (Row) - }; - - class BeautifulTable { - private: - std::vector> top_header; - std::vector header; - std::vector> rows; - std::vector columnWidths; - bool useColor; - ColorScheme colors; - static inline const std::string RESET_COLOR = "\033[0m"; - - void updateColumnWidths(const std::vector& row) { - for (size_t i = 0; i < row.size(); ++i) { - if (i >= columnWidths.size()) { - columnWidths.push_back(row[i].length()); - } - else { - columnWidths[i] = std::max< size_t>(columnWidths[i], row[i].length()); - } - } - } - - template - void printHorizontalLine(StreamType& stream) const { - if (useColor) stream << colors.border_color; - stream << "+"; - for (size_t width : columnWidths) { - stream << std::string(width + 2, '-') << "+"; - } - if (useColor) stream << RESET_COLOR; - stream << "\n"; - } - - template - void printRow(StreamType& stream, const std::vector& row, const std::string& color, bool center = false) const { - if (useColor) stream << colors.border_color; - stream << "|"; - if (useColor) stream << RESET_COLOR << color; - for (size_t i = 0; i < row.size(); ++i) { - if (center) { - size_t padding = columnWidths[i] - row[i].length(); - size_t leftPadding = padding / 2; - size_t rightPadding = padding - leftPadding; - stream << std::string(leftPadding + 1, ' ') << row[i] << std::string(rightPadding + 1, ' '); - } - else { - stream << " " << std::setw(static_cast(columnWidths[i])) << std::right << row[i] << " "; - } - if (useColor) stream << RESET_COLOR << colors.border_color; - stream << "|"; - if (useColor) stream << RESET_COLOR << color; - } - if (useColor) stream << RESET_COLOR; - stream << "\n"; - } - - template - void printRow(StreamType& stream, const std::vector>& row, const std::string& color) const { - if (useColor) stream << colors.border_color; - stream << "|"; - if (useColor) stream << RESET_COLOR << color; - int y = 0; - for (size_t i = 0; i < row.size(); ++i) { - size_t sum = row[i].second - 1; - for (int x = y; x < y + row[i].second; x++) { - sum += columnWidths[x] + 2; - } - y += row[i].second; - - size_t textWidth = row[i].first.length(); - size_t totalPadding = sum - textWidth; - size_t leftPadding = totalPadding / 2; - size_t rightPadding = totalPadding - leftPadding; - - // Print left padding - stream << std::string(leftPadding, ' '); - - // Print text - stream << row[i].first; - - // Print right padding - stream << std::string(rightPadding, ' '); - if (useColor) stream << RESET_COLOR << colors.border_color; - stream << "|"; - if (useColor) stream << RESET_COLOR << color; - } - if (useColor) stream << RESET_COLOR; - stream << "\n"; - } - - public: - BeautifulTable(const std::vector& headerColumns, bool enableColor = false, const ColorScheme& colors = default_colors, const std::vector>& top_header = {}) - : top_header(top_header), header(headerColumns), useColor(enableColor), colors(colors) { - updateColumnWidths(header); - } - - void addRow(const std::vector& row) { - if (row.size() != header.size()) { - throw std::invalid_argument("Row size must match header size"); - } - rows.push_back(row); - updateColumnWidths(row); - } - - template - void print(StreamType& stream) const { - if (top_header.size() > 0) { - printHorizontalLine(stream); - printRow(stream, top_header, colors.top_header_color); - } - printHorizontalLine(stream); - printRow(stream, header, colors.header_color, true); - printHorizontalLine(stream); - for (const auto& row : rows) { - printRow(stream, row, colors.row_color); - printHorizontalLine(stream); - } - } - - template - static inline std::string table_string(const T& value) { - std::ostringstream oss; - oss << value; - return oss.str(); - } - - static inline std::string table_time(uint_fast64_t nanoseconds) { - return table_time(static_cast(nanoseconds)); - } - - static inline std::string table_time(double nanoseconds) { - const char* units[] = { "ns", "mcs", "ms", "s" }; - int unit = 0; - double value = static_cast(nanoseconds); - while (value >= 1000 && unit < 3) { - value /= 1000; - unit++; - } - std::ostringstream oss; - oss << std::fixed << std::setprecision(2) << value << " " << units[unit]; - return oss.str(); - } - - static inline std::string table_percentage(uint_fast64_t value, uint_fast64_t total) { - if (total == 0) { - return "nan%"; - } - - // Calculate the percentage - double percentage = (static_cast(value) / total) * 100.0; - - // Format the percentage as a string with 2 decimal places - std::ostringstream ss; - ss << std::fixed << std::setprecision(2) << percentage << "%"; - - return ss.str(); - } - - static inline std::string table_timepoint(const std::chrono::high_resolution_clock::time_point& tp) { - auto system_tp = std::chrono::system_clock::now() + - std::chrono::duration_cast( - tp - std::chrono::high_resolution_clock::now()); - - auto tt = std::chrono::system_clock::to_time_t(system_tp); - std::tm tm{}; - -#if defined(_WIN32) - localtime_s(&tm, &tt); -#else - localtime_r(&tt, &tm); -#endif - - std::ostringstream oss; - oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S"); - return oss.str(); - } - - static inline std::string stable_shortenPath(const std::string& fullPath, size_t maxLength = 35) { - namespace fs = std::filesystem; - - fs::path path(fullPath); - std::string filename = path.filename().string(); - - if (filename.length() <= maxLength) { - return filename; - } - - // If filename is too long, truncate it and add ... - return filename.substr(0, maxLength - 3) + "..."; - } - - using bt = BeautifulTable; - }; - - struct Event { - std::chrono::high_resolution_clock::time_point start_time; - std::chrono::high_resolution_clock::time_point end_time; - int line; - int thread_id; - std::string_view filename; - std::string_view function; - unsigned int event_id; - Event(const std::chrono::high_resolution_clock::time_point& start_time, const std::chrono::high_resolution_clock::time_point& end_time, const std::string_view filename, const int line, const std::string_view function, const int thread_id, const unsigned int event_id) - : start_time(start_time), end_time(end_time), line(line), thread_id(thread_id), filename(filename), function(function), event_id(event_id) - {} - }; - - struct Simple_Event { - uint_fast64_t duration = 0; - std::chrono::high_resolution_clock::time_point start_time{}; - int_fast64_t unique_id = 0; - std::chrono::high_resolution_clock::time_point end_time{}; - Simple_Event(const std::chrono::high_resolution_clock::time_point& start_time, const std::chrono::high_resolution_clock::time_point& end_time, const uint_fast64_t duration, const int_fast64_t unique_id) :duration(duration), start_time(start_time), unique_id(unique_id), end_time(end_time) {} - Simple_Event() {} - }; - - inline bool cmp_simple_event_by_duration_asc(const Simple_Event& a, const Simple_Event& b) - { - return a.duration < b.duration; - } - inline bool cmp_simple_event_by_start_time_asc(const Simple_Event& a, const Simple_Event& b) - { - return a.start_time < b.start_time; - } - - inline uint_fast64_t get_unique_event_id(unsigned int thread_id, unsigned int event_id) { - uint_fast64_t uniqueId = static_cast(thread_id); - uniqueId = uniqueId << 32; - uniqueId += static_cast(event_id); - return uniqueId; - } - - inline std::vector create_simple_events(const std::vector& events) { - std::vector simple_events{}; - simple_events.resize(events.size()); - std::transform( - OPT_EXEC_POLICY - events.begin(), events.end(), - simple_events.begin(), - [](const Event& event) { - Simple_Event simple_event(event.start_time, event.end_time, std::chrono::duration_cast( - event.end_time - event.start_time).count(), get_unique_event_id(event.thread_id, event.event_id)); - return simple_event; - } - ); - return simple_events; - } - - inline std::vector create_simple_events(const std::vector& events) { - std::vector simple_events{}; - simple_events.resize(events.size()); - std::transform( - OPT_EXEC_POLICY - events.begin(), events.end(), - simple_events.begin(), - [](const Event* event) { - Simple_Event simple_event(event->start_time, event->end_time, std::chrono::duration_cast( - event->end_time - event->start_time).count(), get_unique_event_id(event->thread_id, event->event_id)); - return simple_event; - } - ); - return simple_events; - } - - - //requires already sorted - inline std::vector sorted_create_grouped_simple_events(const std::vector& events) { - std::vector result{}; - if (events.size() == 0) - return result; - result.push_back(events[0]); - unsigned int current_idx = 0; - - for (size_t i = 1; i < events.size(); i++) { - if (result[current_idx].end_time >= events[i].start_time) { - result[current_idx].end_time = std::max(result[current_idx].end_time, events[i].end_time); - } - else { - result.push_back(events[i]); - current_idx++; - } - } - - for (auto& entry : result) - { - entry.duration = std::chrono::duration_cast(entry.end_time - entry.start_time).count(); - } - - return result; - } - - inline std::vector load_child_events_simple(const std::vector& parent_events_simple, - const std::unordered_map < int_fast64_t, Event>& events_map, const std::unordered_map< int_fast64_t, std::vector< int_fast64_t>>& child_graph) { - std::vector child_events{}; - - //std::set< int_fast64_t> parent_ids = get_distinct_field_values(parent_events_simple, &Simple_Event::unique_id); - for (const auto& simple_parent_event : parent_events_simple) - { - auto it = child_graph.find(simple_parent_event.unique_id); - if (it != child_graph.end()) { - for (auto& child_id : it->second) { - auto& child_event = events_map.at(child_id); - auto& parent_event = events_map.at(simple_parent_event.unique_id); - if (child_event.filename == parent_event.filename && - child_event.function == parent_event.function && - child_event.line == parent_event.line) - continue; - - child_events.push_back(&child_event); - } - } - } - - return create_simple_events(child_events); - }; - - class EventGroup { - public: - - void calculateStats(unsigned int non_center_percent, const - std::unordered_map < int_fast64_t, Event>& events_map, const std::unordered_map< int_fast64_t, std::vector< int_fast64_t>>& child_graph) { - if (all_events.size() == 0) - return; - - auto all_events_simple = create_simple_events(all_events); - std::sort(OPT_EXEC_POLICY all_events_simple.begin(), all_events_simple.end(), cmp_simple_event_by_duration_asc); - all_cnt = static_cast (all_events_simple.size()); - const double factor = (1.0 / static_cast(all_cnt)); - - auto all_child_events_simple = load_child_events_simple(all_events_simple, events_map, child_graph); - - all_time_acc = sum_field(all_events_simple, &Simple_Event::duration); - - const double all_mean = all_time_acc * factor; - if (std::fpclassify(all_mean) == FP_ZERO) return; - - all_st = calculate_std_dev_field(all_events_simple, &Simple_Event::duration, all_mean);// std::sqrt(all_variance); - all_cv = all_st / all_mean; - - all_thread_cnt = static_cast(count_distinct_field_values(all_events, &Event::thread_id)); - unsigned int amount_non_center = all_cnt * non_center_percent / 100; - - fastest_range = non_center_percent; - slowest_range = 100 - non_center_percent; - - std::vector fastest_events_simple, slowest_events_simple, center_events_simple; - fastest_events_simple.reserve(amount_non_center); - slowest_events_simple.reserve(amount_non_center); - if(all_cnt > 2) - center_events_simple.reserve(all_cnt - 2 * amount_non_center); - - for (unsigned int i = 0; i < all_events_simple.size(); i++) { - if (i < amount_non_center) { - fastest_events_simple.push_back(all_events_simple[i]); - } - else if (i >= all_cnt - amount_non_center) { - slowest_events_simple.push_back(all_events_simple[i]); - } - else { - center_events_simple.push_back(all_events_simple[i]); - } - } - if (amount_non_center > 0) { - //fastest - fastest_min = fastest_events_simple[0].duration; - fastest_mean = sum_field(fastest_events_simple, &Simple_Event::duration) / static_cast(amount_non_center); - - //slowest - slowest_max = slowest_events_simple[slowest_events_simple.size() - 1].duration; - slowest_mean = sum_field(slowest_events_simple, &Simple_Event::duration) / static_cast(amount_non_center); - } - - //center - center_min = center_events_simple[0].duration; - center_max = center_events_simple[center_events_simple.size() - 1].duration; - center_mean = sum_field(center_events_simple, &Simple_Event::duration) / static_cast(center_events_simple.size()); - if (center_events_simple.size() % 2 == 1) - center_med = center_events_simple[center_events_simple.size() / 2].duration; - else - center_med = (center_events_simple[center_events_simple.size() / 2].duration + center_events_simple[center_events_simple.size() / 2 - 1].duration) / 2; - - auto center_child_events_simple = load_child_events_simple(center_events_simple, events_map, child_graph); - - std::sort(OPT_EXEC_POLICY center_events_simple.begin(), center_events_simple.end(), cmp_simple_event_by_start_time_asc); - center_grouped = sorted_create_grouped_simple_events(center_events_simple); - center_time_active = sum_field(center_grouped, &Simple_Event::duration); - - std::sort(OPT_EXEC_POLICY center_child_events_simple.begin(), center_child_events_simple.end(), cmp_simple_event_by_start_time_asc); - auto center_child_events_grouped = sorted_create_grouped_simple_events(center_child_events_simple); - center_time_active_exclusive = center_time_active - sum_field(center_child_events_grouped, &Simple_Event::duration); - - std::sort(OPT_EXEC_POLICY all_events_simple.begin(), all_events_simple.end(), cmp_simple_event_by_start_time_asc); - all_grouped = sorted_create_grouped_simple_events(all_events_simple); - all_time_active = sum_field(all_grouped, &Simple_Event::duration); - - std::sort(OPT_EXEC_POLICY all_child_events_simple.begin(), all_child_events_simple.end(), cmp_simple_event_by_start_time_asc); - auto all_child_events_grouped = sorted_create_grouped_simple_events(all_child_events_simple); - all_time_active_exclusive = all_time_active - sum_field(all_child_events_grouped, &Simple_Event::duration); - } - - //all_group - - double all_cv = 0.0; - double all_st = 0.0; - - unsigned int all_cnt = 0; - uint_fast64_t all_time_acc = 0; - uint_fast64_t all_time_active = 0; - uint_fast64_t all_time_active_exclusive = 0; - unsigned int all_thread_cnt = 0; - std::vector all_grouped = {}; - std::vector all_events = {}; - - //fastest_group - unsigned int fastest_range = 0; - uint_fast64_t fastest_min = 0; - double fastest_mean = 0.0; - - //slowest group - unsigned int slowest_range = 0; - uint_fast64_t slowest_max = 0; - double slowest_mean = 0.0; - - //center group - - uint_fast64_t center_min = 0; - uint_fast64_t center_max = 0; - uint_fast64_t center_med = 0; - double center_mean = 0; - uint_fast64_t center_time_active = 0; - uint_fast64_t center_time_active_exclusive = 0; - std::vector center_grouped = {}; - - std::string filename = {}; - std::string function_name = {}; - int line = 0; - private: - }; - - typedef std::vector t_events; - typedef std::map> sub_events; - - - - struct store - { - inline static std::atomic write_events_locked = false; - inline static std::mutex event_mutex; - inline static std::chrono::high_resolution_clock::time_point track_start_time = std::chrono::high_resolution_clock::now(); - inline static std::atomic store_clear_cnt = 0; - - inline static std::atomic thread_cnt = -1; - inline static std::deque a_events{}; - inline static std::deque a_sub_events{}; - - inline static std::deque a_current_event_id{}, a_current_event_cnt{}, a_string_id{}; - - inline static std::deque< int> a_thread_ids{}; - }; - - inline thread_local t_events* event_ptr = nullptr; - inline thread_local sub_events* sub_events_ptr = nullptr; - - inline thread_local unsigned int* current_event_id = nullptr; - inline thread_local unsigned int* current_event_cnt = nullptr; - inline thread_local unsigned int* string_id = nullptr; - - inline thread_local int* thread_id = nullptr; - - typedef std::map line_result; - typedef std::map function_result; - typedef std::map < std::string_view, function_result> filename_result; - - struct ctrack_result_settings { - unsigned int non_center_percent = 1; - double min_percent_active_exclusive = 0.0; //between 0-100 - double percent_exclude_fastest_active_exclusive = 0.0; //between 0-100 - }; - - class ctrack_result { - public: - - ctrack_result(const ctrack_result_settings& settings, const std::chrono::high_resolution_clock::time_point& track_start_time, const std::chrono::high_resolution_clock::time_point& track_end_time) : - settings(settings), track_start_time(track_start_time), track_end_time(track_end_time) - { - time_total = std::chrono::duration_cast( - track_end_time - track_start_time).count(); - center_intervall_str = "[" + std::to_string(settings.non_center_percent) + "-" + std::to_string(100 - settings.non_center_percent) + "]"; - } - - template - void get_summary_table(StreamType& stream, bool use_color = false) { - BeautifulTable info({ "Start","End","time total","time ctracked","time ctracked %", }, use_color, alternate_colors); - info.addRow({ BeautifulTable::table_timepoint(track_start_time), BeautifulTable::table_timepoint(track_end_time), - BeautifulTable::table_time(time_total), BeautifulTable::table_time(sum_time_active_exclusive), - BeautifulTable::table_percentage(sum_time_active_exclusive, time_total) - }); - - info.print(stream); - BeautifulTable table({ "filename", "function", "line","calls","ae" + center_intervall_str + "%","ae[0-100]%", - "time ae[0-100]" ,"time a[0-100]" }, use_color, alternate_colors); - for (auto& entry : sorted_events) { - table.addRow({ BeautifulTable::stable_shortenPath(entry->filename), entry->function_name,BeautifulTable::table_string(entry->line), - BeautifulTable::table_string(entry->all_cnt), - BeautifulTable::table_percentage(entry->center_time_active_exclusive, time_total), - BeautifulTable::table_percentage(entry->all_time_active_exclusive,time_total), - BeautifulTable::table_time(entry->all_time_active_exclusive), - BeautifulTable::table_time(entry->all_time_active) }); - } - - table.print(stream); - } - - template - void get_detail_table(StreamType& stream, bool use_color = false, bool reverse_vector = false) { - if (reverse_vector) { - std::reverse(sorted_events.begin(), sorted_events.end()); - } - for (int i = static_cast(sorted_events.size()) - 1; i >= 0; i--) { - auto& entry = sorted_events[i]; - - BeautifulTable info({ "filename", "function", "line","time acc","sd","cv","calls","threads" }, use_color, default_colors); - info.addRow({ BeautifulTable::stable_shortenPath(entry->filename), entry->function_name,BeautifulTable::table_string(entry->line), - BeautifulTable::table_time(entry->all_time_acc), - BeautifulTable::table_time(sorted_events[i]->all_st),BeautifulTable::table_string(sorted_events[i]->all_cv), - BeautifulTable::table_string(sorted_events[i]->all_cnt), BeautifulTable::table_string(sorted_events[i]->all_thread_cnt) }); - - BeautifulTable table({ "min", "mean", "min","mean","med","time a","time ae","max","mean","max" }, use_color, default_colors, - { {"fastest[0-" + std::to_string(settings.non_center_percent) + "]%",2},{"center" + center_intervall_str + "%",6}, - {"slowest[" + std::to_string(100 - settings.non_center_percent) + "-100]%",2} }); - - table.addRow({ BeautifulTable::table_time(entry->fastest_min),BeautifulTable::table_time(entry->fastest_mean), - BeautifulTable::table_time(entry->center_min),BeautifulTable::table_time(entry->center_mean), - BeautifulTable::table_time(entry->center_med),BeautifulTable::table_time(entry->center_time_active), - BeautifulTable::table_time(entry->center_time_active_exclusive), - BeautifulTable::table_time(entry->center_max), - BeautifulTable::table_time(entry->slowest_mean),BeautifulTable::table_time(entry->slowest_max) }); - - info.print(stream); - table.print(stream); - - stream << std::endl; - } - } - - void calculate_stats() { - std::vector grouped_events{}; - for (auto& [filename, filename_entry] : f_res) - { - ctracked_files++; - for (auto& [function, function_entry] : filename_entry) { - ctracked_functions++; - for (auto& [line, line_entry] : function_entry) { - ctracked_uses++; - line_entry.filename = filename; - line_entry.function_name = function; - line_entry.line = line; - line_entry.calculateStats(settings.non_center_percent, a_events, child_graph); - sorted_events.push_back(&line_entry); - grouped_events.insert(grouped_events.end(), line_entry.all_grouped.begin(), line_entry.all_grouped.end()); - } - } - } - - std::sort(OPT_EXEC_POLICY grouped_events.begin(), grouped_events.end(), cmp_simple_event_by_start_time_asc); - auto all_grouped = sorted_create_grouped_simple_events(grouped_events); - sum_time_active_exclusive = sum_field(all_grouped, &Simple_Event::duration); - - order_pointer_vector_by_field(sorted_events, &EventGroup::all_time_active_exclusive, false); - - int fastest_events = static_cast(sorted_events.size() * settings.percent_exclude_fastest_active_exclusive / 100); - //remove fastest keep in mind fastest elements are at the back - if (fastest_events > 0) - sorted_events.erase(sorted_events.end() - fastest_events, sorted_events.end()); - - uint_fast64_t min_time_active_exclusive = static_cast(time_total * settings.min_percent_active_exclusive / 100); - //remove fastest keep in mind fastest elements are at the back - if (min_time_active_exclusive > 0) - sorted_events.erase(std::remove_if(sorted_events.begin(), sorted_events.end(), [min_time_active_exclusive](EventGroup* e) {return e->all_time_active_exclusive < min_time_active_exclusive; }), sorted_events.end()); - - } - - void reserve_a_events(size_t size) { - a_events.reserve(size); - } - - inline void add_event(const std::string_view& filename, const std::string_view function, const int line, const Event& e) { - f_res[filename][function][line].all_events.push_back(e); - a_events.insert({ get_unique_event_id(e.thread_id, e.event_id), e }); - } - - void add_sub_events(const sub_events& s_events, const unsigned int thread_id_) { - - for (auto const& [key, val] : s_events) - { - int_fast64_t parent_id = get_unique_event_id(thread_id_, key); - for (const auto& child : val) { - child_graph[parent_id].push_back(get_unique_event_id(thread_id_, child)); - } - } - } - - std::unordered_map < int_fast64_t, Event> a_events{}; - filename_result f_res{}; - - std::unordered_map< int_fast64_t, std::vector< int_fast64_t>> child_graph{}; - ctrack_result_settings settings; - std::chrono::high_resolution_clock::time_point track_start_time, track_end_time; - uint_fast64_t time_total; - uint_fast64_t sum_time_active_exclusive = 0; - - uint_fast64_t ctracked_files = 0; - uint_fast64_t ctracked_functions = 0; - uint_fast64_t ctracked_uses = 0; - - std::vector sorted_events{}; - std::string center_intervall_str; - }; - - inline int fetch_event_t_id() { - if (thread_id == nullptr || *thread_id == -1) { - std::scoped_lock lock(store::event_mutex); - - if (thread_id == nullptr) { - store::a_thread_ids.emplace_back(++store::thread_cnt); - thread_id = &store::a_thread_ids[store::a_thread_ids.size() - 1]; - } - else { - *thread_id = ++store::thread_cnt; - } - - store::a_events.emplace_back(t_events{}); - store::a_sub_events.emplace_back(sub_events{}); - store::a_current_event_id.emplace_back(0); - store::a_current_event_cnt.emplace_back(0); - store::a_string_id.emplace_back(0); - - event_ptr = &store::a_events[*thread_id]; - sub_events_ptr = &store::a_sub_events[*thread_id]; - - current_event_id = &store::a_current_event_id[*thread_id]; - current_event_cnt = &store::a_current_event_cnt[*thread_id]; - string_id = &store::a_string_id[*thread_id]; - - event_ptr->reserve(100); - } - return *thread_id; - } - - class EventHandler { - public: - EventHandler(int line = __builtin_LINE(), const char* filename = __builtin_FILE(), const char* function = __builtin_FUNCTION(), std::chrono::high_resolution_clock::time_point start_time = std::chrono::high_resolution_clock::now()) : line(line) - - { - - previous_store_clear_cnt = store::store_clear_cnt; - this->filename = filename; - this->function = function; - while (store::write_events_locked) {} - - register_event(); - this->start_time = start_time; - } - ~EventHandler() { - auto end_time = std::chrono::high_resolution_clock::now(); - while (store::write_events_locked) {} - - if (store::store_clear_cnt != previous_store_clear_cnt) - { - register_event(); - } - - - if (event_ptr->capacity() - event_ptr->size() < 1) event_ptr->reserve(event_ptr->capacity() * 4); - - event_ptr->emplace_back(Event{ start_time,end_time,filename,line,function,t_id ,event_id }); - - *current_event_id = previous_event_id; - if (previous_event_id > 0) { - if ((*sub_events_ptr)[previous_event_id].capacity() - (*sub_events_ptr)[previous_event_id].size() < 1) (*sub_events_ptr)[previous_event_id].reserve((*sub_events_ptr)[previous_event_id].capacity() * 4); - (*sub_events_ptr)[previous_event_id].push_back(event_id); - } - } - private: - void register_event() { - t_id = fetch_event_t_id(); - previous_event_id = *current_event_id; - event_id = ++(*current_event_cnt); - *current_event_id = event_id; - } - std::chrono::high_resolution_clock::time_point start_time; - int line; - unsigned int previous_store_clear_cnt; - - std::string_view filename, function; - - int t_id; - unsigned int event_id; - unsigned int previous_event_id; - }; - - inline void clear_a_store() { - store::a_current_event_id.clear(); - store::a_current_event_id.shrink_to_fit(); - - store::a_current_event_cnt.clear(); - store::a_current_event_cnt.shrink_to_fit(); - - store::a_string_id.clear(); - store::a_string_id.shrink_to_fit(); - - store::a_events.clear(); - store::a_events.shrink_to_fit(); - - store::a_sub_events.clear(); - store::a_sub_events.shrink_to_fit(); - - store::thread_cnt = -1; - for (auto& entry : store::a_thread_ids) - { - entry = -1; - } - store::store_clear_cnt++; - store::track_start_time = std::chrono::high_resolution_clock::now(); - } - - inline ctrack_result calc_stats_and_clear(ctrack_result_settings settings = {}) { - auto end = std::chrono::high_resolution_clock::now(); - ctrack_result res{ settings, store::track_start_time, end }; - - //copy data - { - store::write_events_locked = true; - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - std::scoped_lock lock(store::event_mutex); - - auto all_events_cnt = countAllEvents(store::a_events); - res.reserve_a_events(all_events_cnt); - - for (int thread_id_ = 0; thread_id_ <= store::thread_cnt; thread_id_++) { - auto& t_events_entry = store::a_events[thread_id_]; - auto& t_sub_events = store::a_sub_events[thread_id_]; - res.add_sub_events(t_sub_events, thread_id_); - - for (const auto& c_event : t_events_entry) - { - res.add_event(c_event.filename, c_event.function, c_event.line, c_event); - } - t_events_entry.clear(); - t_events_entry.shrink_to_fit(); - } - clear_a_store(); - store::write_events_locked = false; - } - - res.calculate_stats(); - store::track_start_time = std::chrono::high_resolution_clock::now(); - - return res; - } - - inline void result_print(ctrack_result_settings settings = {}) { - auto res = calc_stats_and_clear(settings); - std::cout << "Details" << std::endl; - res.get_detail_table(std::cout, true); - std::cout << "Summary" << std::endl; - res.get_summary_table(std::cout, true); - } - - inline std::string result_as_string(ctrack_result_settings settings = {}) { - auto res = calc_stats_and_clear(settings); - std::stringstream ss; - ss << "Summary\n"; - res.get_summary_table(ss, false); - ss << "Details\n"; - res.get_detail_table(ss, false, true); - - return ss.str(); - } - } -} - -#ifndef CTRACK_DISABLE -#define CTRACK_CONCAT_IMPL(x, y) x ## y -#define CTRACK_CONCAT(x, y) CTRACK_CONCAT_IMPL(x, y) -#define CTRACK_UNIQUE_NAME(prefix) CTRACK_CONCAT(prefix, __COUNTER__) - - -#define CTRACK_IMPL ctrack::EventHandler CTRACK_UNIQUE_NAME(ctrack_instance_){__builtin_LINE(),__builtin_FILE(),__builtin_FUNCTION()} -#define CTRACK_IMPL_NAME(name) ctrack::EventHandler CTRACK_UNIQUE_NAME(ctrack_instance_){__builtin_LINE(),__builtin_FILE(),name} -#if defined(CTRACK_DISABLE_DEV) -#define CTRACK_PROD CTRACK_IMPL -#define CTRACK_PROD_NAME(name) CTRACK_IMPL_NAME(name) -#define CTRACK_DEV // Disabled -#define CTRACK_DEV_NAME(name) // Disabled -#elif defined(CTRACK_DISABLE_PROD) -#define CTRACK_PROD // Disabled -#define CTRACK_PROD_NAME(name) // Disabled -#define CTRACK_DEV CTRACK_IMPL -#define CTRACK_DEV_NAME(name) CTRACK_IMPL_NAME(name) -#else -#define CTRACK_PROD CTRACK_IMPL -#define CTRACK_PROD_NAME(name) CTRACK_IMPL_NAME(name) -#define CTRACK_DEV CTRACK_IMPL -#define CTRACK_DEV_NAME(name) CTRACK_IMPL_NAME(name) -#endif - -// Alias CTRACK to CTRACK_PROD -#define CTRACK CTRACK_PROD -#define CTRACK_NAME(name) CTRACK_PROD_NAME(name) - -#else // CTRACK_DISABLE -#define CTRACK_PROD -#define CTRACK_PROD_NAME(name) -#define CTRACK_DEV -#define CTRACK_DEV_NAME(name) -#define CTRACK -#define CTRACK_NAME(name) -#endif // CTRACK_DISABLE - +// Copyright (c) 2024 COMPAILE Solutions GmbH - Grischa Hauser +// License:MIT License See LICENSE for the full license. +// https://github.com/Compaile/ctrack +#pragma once +#ifndef CTRACK_H +#define CTRACK_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef CTRACK_DISABLE_EXECUTION_POLICY +#include +#endif +#include +#include +#include +#include +#include +#include + +#define CTRACK_VERSION_MAJOR 1 +#define CTRACK_VERSION_MINOR 1 +#define CTRACK_VERSION_PATCH 0 + +// Helper macro to convert a numeric value to a string +#define STRINGIFY(x) #x +#define TOSTRING(x) STRINGIFY(x) + +// Create a string version +#define CTRACK_VERSION_STRING \ + TOSTRING(CTRACK_VERSION_MAJOR) \ + "_" TOSTRING(CTRACK_VERSION_MINOR) "_" TOSTRING(CTRACK_VERSION_PATCH) + +// Use the version string as the namespace name +#define CTRACK_VERSION_NAMESPACE v##CTRACK_VERSION_MAJOR##_##CTRACK_VERSION_MINOR##_##CTRACK_VERSION_PATCH + +namespace ctrack +{ + + inline namespace CTRACK_VERSION_NAMESPACE + { +#ifndef CTRACK_DISABLE_EXECUTION_POLICY + constexpr auto execution_policy = std::execution::par_unseq; +#define OPT_EXEC_POLICY execution_policy, +#else +#define OPT_EXEC_POLICY +#endif + + template + auto sum_field(const std::vector &vec, Field T::*field) + { + using FieldType = std::decay_t().*field)>; + return std::transform_reduce( + OPT_EXEC_POLICY + vec.begin(), + vec.end(), + FieldType{}, + std::plus<>(), + [field](const auto &item) + { return item.*field; }); + } + + template + auto sum_squared_field(const std::vector &values, Field T::*field) + { + using FieldType = std::decay_t().*field)>; + return std::transform_reduce( + OPT_EXEC_POLICY + values.begin(), + values.end(), + FieldType{}, + std::plus<>(), + [field](const T &v) + { + return (v.*field) * (v.*field); + }); + } + + template + double calculate_std_dev_field(std::vector &values, Field T::*field, const double mean) + { + double res = std::transform_reduce( + OPT_EXEC_POLICY + values.begin(), + values.end(), + 0.0, + std::plus<>(), + [mean, field](const T &v) + { + return std::pow(static_cast(v.*field) - mean, 2); + }); + + return sqrt(res / values.size()); + } + + template + auto get_distinct_field_values(const std::vector &vec, Field T::*field) + { + std::set().*field)>> distinct_values; + + std::transform(vec.begin(), vec.end(), + std::inserter(distinct_values, distinct_values.end()), + [field](const T *item) + { return item->*field; }); + return distinct_values; + } + + template + auto get_distinct_field_values(const std::vector &vec, Field T::*field) + { + std::set().*field)>> distinct_values; + + std::transform(vec.begin(), vec.end(), + std::inserter(distinct_values, distinct_values.end()), + [field](const T &item) + { return item.*field; }); + return distinct_values; + } + + template + size_t count_distinct_field_values(const std::vector &vec, Field T::*field) + { + return get_distinct_field_values(vec, field).size(); + } + + template + void order_pointer_vector_by_field(std::vector &vec, MemberType StructType::*member, bool asc = true) + { + std::sort(OPT_EXEC_POLICY vec.begin(), vec.end(), + [member, asc](const StructType *a, const StructType *b) + { + if (asc) + return (a->*member) < (b->*member); + else + return (a->*member) > (b->*member); + }); + } + + template + size_t countAllEvents(const std::deque> &events) + { + return std::transform_reduce( + OPT_EXEC_POLICY + events.begin(), + events.end(), + size_t(0), + std::plus<>(), + [](const auto &vec) + { + return vec.size(); + }); + } + + struct ColorScheme + { + std::string border_color; + std::string header_color; + std::string top_header_color; + std::string row_color; + + ColorScheme(const std::string &border, + const std::string &header, + const std::string &top_header, + const std::string &row) + : border_color(border), + header_color(header), + top_header_color(top_header), + row_color(row) {} + }; + + static inline const ColorScheme default_colors{ + "\033[38;5;24m", // Darker Blue (Border) + "\033[1;38;5;135m", // Purple (Header) + "\033[1;38;5;92m", // Darker Purple (Top Header) + "\033[38;5;39m" // Light Blue (Row) + }; + + // Alternate color scheme (still nice to read on terminals) + static inline const ColorScheme alternate_colors{ + "\033[38;5;28m", // Dark Green (Border) + "\033[1;38;5;208m", // Orange (Header) + "\033[1;38;5;130m", // Dark Orange (Top Header) + "\033[38;5;71m" // Light Green (Row) + }; + + class BeautifulTable + { + private: + std::vector> top_header; + std::vector header; + std::vector> rows; + std::vector columnWidths; + bool useColor; + ColorScheme colors; + static inline const std::string RESET_COLOR = "\033[0m"; + + void updateColumnWidths(const std::vector &row) + { + for (size_t i = 0; i < row.size(); ++i) + { + if (i >= columnWidths.size()) + { + columnWidths.push_back(row[i].length()); + } + else + { + columnWidths[i] = std::max(columnWidths[i], row[i].length()); + } + } + } + + template + void printHorizontalLine(StreamType &stream) const + { + if (useColor) + stream << colors.border_color; + stream << "+"; + for (size_t width : columnWidths) + { + stream << std::string(width + 2, '-') << "+"; + } + if (useColor) + stream << RESET_COLOR; + stream << "\n"; + } + + template + void printRow(StreamType &stream, const std::vector &row, const std::string &color, bool center = false) const + { + if (useColor) + stream << colors.border_color; + stream << "|"; + if (useColor) + stream << RESET_COLOR << color; + for (size_t i = 0; i < row.size(); ++i) + { + if (center) + { + size_t padding = columnWidths[i] - row[i].length(); + size_t leftPadding = padding / 2; + size_t rightPadding = padding - leftPadding; + stream << std::string(leftPadding + 1, ' ') << row[i] << std::string(rightPadding + 1, ' '); + } + else + { + stream << " " << std::setw(static_cast(columnWidths[i])) << std::right << row[i] << " "; + } + if (useColor) + stream << RESET_COLOR << colors.border_color; + stream << "|"; + if (useColor) + stream << RESET_COLOR << color; + } + if (useColor) + stream << RESET_COLOR; + stream << "\n"; + } + + template + void printRow(StreamType &stream, const std::vector> &row, const std::string &color) const + { + if (useColor) + stream << colors.border_color; + stream << "|"; + if (useColor) + stream << RESET_COLOR << color; + int y = 0; + for (size_t i = 0; i < row.size(); ++i) + { + size_t sum = row[i].second - 1; + for (int x = y; x < y + row[i].second; x++) + { + sum += columnWidths[x] + 2; + } + y += row[i].second; + + size_t textWidth = row[i].first.length(); + size_t totalPadding = sum - textWidth; + size_t leftPadding = totalPadding / 2; + size_t rightPadding = totalPadding - leftPadding; + + // Print left padding + stream << std::string(leftPadding, ' '); + + // Print text + stream << row[i].first; + + // Print right padding + stream << std::string(rightPadding, ' '); + if (useColor) + stream << RESET_COLOR << colors.border_color; + stream << "|"; + if (useColor) + stream << RESET_COLOR << color; + } + if (useColor) + stream << RESET_COLOR; + stream << "\n"; + } + + public: + BeautifulTable(const std::vector &headerColumns, bool enableColor = false, const ColorScheme &colors = default_colors, const std::vector> &top_header = {}) + : top_header(top_header), header(headerColumns), useColor(enableColor), colors(colors) + { + updateColumnWidths(header); + } + + void addRow(const std::vector &row) + { + if (row.size() != header.size()) + { + throw std::invalid_argument("Row size must match header size"); + } + rows.push_back(row); + updateColumnWidths(row); + } + + template + void print(StreamType &stream) const + { + if (top_header.size() > 0) + { + printHorizontalLine(stream); + printRow(stream, top_header, colors.top_header_color); + } + printHorizontalLine(stream); + printRow(stream, header, colors.header_color, true); + printHorizontalLine(stream); + for (const auto &row : rows) + { + printRow(stream, row, colors.row_color); + printHorizontalLine(stream); + } + } + + template + static inline std::string table_string(const T &value) + { + std::ostringstream oss; + oss << value; + return oss.str(); + } + + static inline std::string table_time(uint_fast64_t nanoseconds) + { + return table_time(static_cast(nanoseconds)); + } + + static inline std::string table_time(double nanoseconds) + { + const char *units[] = {"ns", "mcs", "ms", "s"}; + int unit = 0; + double value = static_cast(nanoseconds); + while (value >= 1000 && unit < 3) + { + value /= 1000; + unit++; + } + std::ostringstream oss; + oss << std::fixed << std::setprecision(2) << value << " " << units[unit]; + return oss.str(); + } + + static inline std::string table_percentage(uint_fast64_t value, uint_fast64_t total) + { + if (total == 0) + { + return "nan%"; + } + + // Calculate the percentage + double percentage = (static_cast(value) / total) * 100.0; + + // Format the percentage as a string with 2 decimal places + std::ostringstream ss; + ss << std::fixed << std::setprecision(2) << percentage << "%"; + + return ss.str(); + } + + static inline std::string table_timepoint(const std::chrono::high_resolution_clock::time_point &tp) + { + auto system_tp = std::chrono::system_clock::now() + + std::chrono::duration_cast( + tp - std::chrono::high_resolution_clock::now()); + + auto tt = std::chrono::system_clock::to_time_t(system_tp); + std::tm tm{}; + +#if defined(_WIN32) + localtime_s(&tm, &tt); +#else + localtime_r(&tt, &tm); +#endif + + std::ostringstream oss; + oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S"); + return oss.str(); + } + + static inline std::string stable_shortenPath(const std::string &fullPath, size_t maxLength = 35) + { + namespace fs = std::filesystem; + + fs::path path(fullPath); + std::string filename = path.filename().string(); + + if (filename.length() <= maxLength) + { + return filename; + } + + // If filename is too long, truncate it and add ... + return filename.substr(0, maxLength - 3) + "..."; + } + + using bt = BeautifulTable; + }; + + struct Event + { + std::chrono::high_resolution_clock::time_point start_time; + std::chrono::high_resolution_clock::time_point end_time; + int line; + int thread_id; + std::string_view filename; + std::string_view function; + unsigned int event_id; + Event(const std::chrono::high_resolution_clock::time_point &start_time, const std::chrono::high_resolution_clock::time_point &end_time, const std::string_view filename, const int line, const std::string_view function, const int thread_id, const unsigned int event_id) + : start_time(start_time), end_time(end_time), line(line), thread_id(thread_id), filename(filename), function(function), event_id(event_id) + { + } + }; + + struct Simple_Event + { + uint_fast64_t duration = 0; + std::chrono::high_resolution_clock::time_point start_time{}; + int_fast64_t unique_id = 0; + std::chrono::high_resolution_clock::time_point end_time{}; + Simple_Event(const std::chrono::high_resolution_clock::time_point &start_time, const std::chrono::high_resolution_clock::time_point &end_time, const uint_fast64_t duration, const int_fast64_t unique_id) : duration(duration), start_time(start_time), unique_id(unique_id), end_time(end_time) {} + Simple_Event() {} + }; + + inline bool cmp_simple_event_by_duration_asc(const Simple_Event &a, const Simple_Event &b) + { + return a.duration < b.duration; + } + inline bool cmp_simple_event_by_start_time_asc(const Simple_Event &a, const Simple_Event &b) + { + return a.start_time < b.start_time; + } + + inline uint_fast64_t get_unique_event_id(unsigned int thread_id, unsigned int event_id) + { + uint_fast64_t uniqueId = static_cast(thread_id); + uniqueId = uniqueId << 32; + uniqueId += static_cast(event_id); + return uniqueId; + } + + inline std::vector create_simple_events(const std::vector &events) + { + std::vector simple_events{}; + simple_events.resize(events.size()); + std::transform( + OPT_EXEC_POLICY + events.begin(), + events.end(), + simple_events.begin(), + [](const Event &event) + { + Simple_Event simple_event(event.start_time, event.end_time, std::chrono::duration_cast(event.end_time - event.start_time).count(), get_unique_event_id(event.thread_id, event.event_id)); + return simple_event; + }); + return simple_events; + } + + inline std::vector create_simple_events(const std::vector &events) + { + std::vector simple_events{}; + simple_events.resize(events.size()); + std::transform( + OPT_EXEC_POLICY + events.begin(), + events.end(), + simple_events.begin(), + [](const Event *event) + { + Simple_Event simple_event(event->start_time, event->end_time, std::chrono::duration_cast(event->end_time - event->start_time).count(), get_unique_event_id(event->thread_id, event->event_id)); + return simple_event; + }); + return simple_events; + } + + // requires already sorted + inline std::vector sorted_create_grouped_simple_events(const std::vector &events) + { + std::vector result{}; + if (events.size() == 0) + return result; + result.push_back(events[0]); + unsigned int current_idx = 0; + + for (size_t i = 1; i < events.size(); i++) + { + if (result[current_idx].end_time >= events[i].start_time) + { + result[current_idx].end_time = std::max(result[current_idx].end_time, events[i].end_time); + } + else + { + result.push_back(events[i]); + current_idx++; + } + } + + for (auto &entry : result) + { + entry.duration = std::chrono::duration_cast(entry.end_time - entry.start_time).count(); + } + + return result; + } + + inline std::vector load_child_events_simple(const std::vector &parent_events_simple, + const std::unordered_map &events_map, const std::unordered_map> &child_graph) + { + std::vector child_events{}; + + // std::set< int_fast64_t> parent_ids = get_distinct_field_values(parent_events_simple, &Simple_Event::unique_id); + for (const auto &simple_parent_event : parent_events_simple) + { + auto it = child_graph.find(simple_parent_event.unique_id); + if (it != child_graph.end()) + { + for (auto &child_id : it->second) + { + auto &child_event = events_map.at(child_id); + auto &parent_event = events_map.at(simple_parent_event.unique_id); + if (child_event->filename == parent_event->filename && + child_event->function == parent_event->function && + child_event->line == parent_event->line) + continue; + + child_events.push_back(child_event); + } + } + } + + return create_simple_events(child_events); + }; + + class EventGroup + { + public: + void calculateStats(unsigned int non_center_percent, const std::unordered_map &events_map, const std::unordered_map> &child_graph) + { + if (all_events.size() == 0) + return; + + auto all_events_simple = create_simple_events(all_events); + std::sort(OPT_EXEC_POLICY all_events_simple.begin(), all_events_simple.end(), cmp_simple_event_by_duration_asc); + all_cnt = static_cast(all_events_simple.size()); + const double factor = (1.0 / static_cast(all_cnt)); + + auto all_child_events_simple = load_child_events_simple(all_events_simple, events_map, child_graph); + + all_time_acc = sum_field(all_events_simple, &Simple_Event::duration); + + const double all_mean = all_time_acc * factor; + if (std::fpclassify(all_mean) == FP_ZERO) + return; + + all_st = calculate_std_dev_field(all_events_simple, &Simple_Event::duration, all_mean); // std::sqrt(all_variance); + all_cv = all_st / all_mean; + + all_thread_cnt = static_cast(get_distinct_field_values(all_events, &Event::thread_id).size()); + unsigned int amount_non_center = all_cnt * non_center_percent / 100; + + fastest_range = non_center_percent; + slowest_range = 100 - non_center_percent; + + std::vector fastest_events_simple, slowest_events_simple, center_events_simple; + fastest_events_simple.reserve(amount_non_center); + slowest_events_simple.reserve(amount_non_center); + if (all_cnt > 2) + center_events_simple.reserve(all_cnt - 2 * amount_non_center); + + for (unsigned int i = 0; i < all_events_simple.size(); i++) + { + if (i < amount_non_center) + { + fastest_events_simple.push_back(all_events_simple[i]); + } + else if (i >= all_cnt - amount_non_center) + { + slowest_events_simple.push_back(all_events_simple[i]); + } + else + { + center_events_simple.push_back(all_events_simple[i]); + } + } + if (amount_non_center > 0) + { + // fastest + fastest_min = fastest_events_simple[0].duration; + fastest_mean = sum_field(fastest_events_simple, &Simple_Event::duration) / static_cast(amount_non_center); + + // slowest + slowest_max = slowest_events_simple[slowest_events_simple.size() - 1].duration; + slowest_mean = sum_field(slowest_events_simple, &Simple_Event::duration) / static_cast(amount_non_center); + } + + // center + center_min = center_events_simple[0].duration; + center_max = center_events_simple[center_events_simple.size() - 1].duration; + center_mean = sum_field(center_events_simple, &Simple_Event::duration) / static_cast(center_events_simple.size()); + if (center_events_simple.size() % 2 == 1) + center_med = center_events_simple[center_events_simple.size() / 2].duration; + else + center_med = (center_events_simple[center_events_simple.size() / 2].duration + center_events_simple[center_events_simple.size() / 2 - 1].duration) / 2; + + auto center_child_events_simple = load_child_events_simple(center_events_simple, events_map, child_graph); + + std::sort(OPT_EXEC_POLICY center_events_simple.begin(), center_events_simple.end(), cmp_simple_event_by_start_time_asc); + center_grouped = sorted_create_grouped_simple_events(center_events_simple); + center_time_active = sum_field(center_grouped, &Simple_Event::duration); + + std::sort(OPT_EXEC_POLICY center_child_events_simple.begin(), center_child_events_simple.end(), cmp_simple_event_by_start_time_asc); + auto center_child_events_grouped = sorted_create_grouped_simple_events(center_child_events_simple); + center_time_active_exclusive = center_time_active - sum_field(center_child_events_grouped, &Simple_Event::duration); + + std::sort(OPT_EXEC_POLICY all_events_simple.begin(), all_events_simple.end(), cmp_simple_event_by_start_time_asc); + all_grouped = sorted_create_grouped_simple_events(all_events_simple); + all_time_active = sum_field(all_grouped, &Simple_Event::duration); + + std::sort(OPT_EXEC_POLICY all_child_events_simple.begin(), all_child_events_simple.end(), cmp_simple_event_by_start_time_asc); + auto all_child_events_grouped = sorted_create_grouped_simple_events(all_child_events_simple); + all_time_active_exclusive = all_time_active - sum_field(all_child_events_grouped, &Simple_Event::duration); + } + + // all_group + + double all_cv = 0.0; + double all_st = 0.0; + + unsigned int all_cnt = 0; + uint_fast64_t all_time_acc = 0; + uint_fast64_t all_time_active = 0; + uint_fast64_t all_time_active_exclusive = 0; + unsigned int all_thread_cnt = 0; + std::vector all_grouped = {}; + std::vector all_events = {}; + + // fastest_group + unsigned int fastest_range = 0; + uint_fast64_t fastest_min = 0; + double fastest_mean = 0.0; + + // slowest group + unsigned int slowest_range = 0; + uint_fast64_t slowest_max = 0; + double slowest_mean = 0.0; + + // center group + + uint_fast64_t center_min = 0; + uint_fast64_t center_max = 0; + uint_fast64_t center_med = 0; + double center_mean = 0; + uint_fast64_t center_time_active = 0; + uint_fast64_t center_time_active_exclusive = 0; + std::vector center_grouped = {}; + + std::string filename = {}; + std::string function_name = {}; + int line = 0; + + private: + }; + + typedef std::vector t_events; + typedef std::map> sub_events; + + struct store + { + inline static std::atomic write_events_locked = false; + inline static std::mutex event_mutex; + inline static std::chrono::high_resolution_clock::time_point track_start_time = std::chrono::high_resolution_clock::now(); + inline static std::atomic store_clear_cnt = 0; + + inline static std::atomic thread_cnt = -1; + inline static std::deque a_events{}; + inline static std::deque a_sub_events{}; + + inline static std::deque a_current_event_id{}, a_current_event_cnt{}, a_string_id{}; + + inline static std::deque a_thread_ids{}; + }; + + inline thread_local t_events *event_ptr = nullptr; + inline thread_local sub_events *sub_events_ptr = nullptr; + + inline thread_local unsigned int *current_event_id = nullptr; + inline thread_local unsigned int *current_event_cnt = nullptr; + inline thread_local unsigned int *string_id = nullptr; + + inline thread_local int *thread_id = nullptr; + + typedef std::map line_result; + typedef std::map function_result; + typedef std::map filename_result; + + struct ctrack_result_settings + { + unsigned int non_center_percent = 1; + double min_percent_active_exclusive = 0.0; // between 0-100 + double percent_exclude_fastest_active_exclusive = 0.0; // between 0-100 + }; + + struct summary_row + { + std::string filename; + std::string function_name; + int line{}; + int calls{}; + double percent_ae_bracket{}; // ae[center]% by configuration + double percent_ae_all{}; // ae[0-100]% + std::chrono::nanoseconds time_ae_all{}; + std::chrono::nanoseconds time_a_all{}; + }; + + struct summary_table + { + std::vector rows; + }; + + struct detail_stats + { + // Info fields + std::string filename; + std::string function_name; + int line{}; + std::chrono::nanoseconds time_acc{}; // Simple sum of all execution times (can exceed wall clock in MT) + std::chrono::nanoseconds sd{}; // Standard deviation + double cv{}; // Coefficient of variation (sd/mean) + int calls{}; // Total number of calls + int threads{}; // Number of different threads that called this function + + // Summary-like fields (for unified access) + double percent_ae_bracket{}; // ae[center]% as percentage of total time + double percent_ae_all{}; // ae[0-100]% as percentage of total time + std::chrono::nanoseconds time_ae_all{}; // Active exclusive time (wall clock minus child functions) + std::chrono::nanoseconds time_a_all{}; // Active time (actual wall clock time, handles MT overlap) + + // Fastest/Center/Slowest stats + std::chrono::nanoseconds fastest_min{}; + std::chrono::nanoseconds fastest_mean{}; + std::chrono::nanoseconds center_min{}; + std::chrono::nanoseconds center_mean{}; + std::chrono::nanoseconds center_med{}; + std::chrono::nanoseconds center_time_a{}; // Active time for center range + std::chrono::nanoseconds center_time_ae{}; // Active exclusive time for center range + std::chrono::nanoseconds center_max{}; + std::chrono::nanoseconds slowest_mean{}; + std::chrono::nanoseconds slowest_max{}; + + // Percentile ranges for reference + unsigned int fastest_range{}; + unsigned int slowest_range{}; + }; + + struct detail_table + { + std::vector rows; + }; + + struct ctrack_result_tables + { + // Meta information + std::chrono::high_resolution_clock::time_point start_time; + std::chrono::high_resolution_clock::time_point end_time; + std::chrono::nanoseconds time_total{}; + std::chrono::nanoseconds time_ctracked{}; + + // Table data + summary_table summary; + detail_table details; + + // Settings used + ctrack_result_settings settings; + }; + + class ctrack_result + { + public: + ctrack_result(const ctrack_result_settings &settings, const std::chrono::high_resolution_clock::time_point &track_start_time, const std::chrono::high_resolution_clock::time_point &track_end_time) : settings(settings), track_start_time(track_start_time), track_end_time(track_end_time) + { + time_total = std::chrono::duration_cast( + track_end_time - track_start_time) + .count(); + center_intervall_str = "[" + std::to_string(settings.non_center_percent) + "-" + std::to_string(100 - settings.non_center_percent) + "]"; + } + + template + void get_summary_table(StreamType &stream, bool use_color = false) + { + BeautifulTable info({ + "Start", + "End", + "time total", + "time ctracked", + "time ctracked %", + }, + use_color, alternate_colors); + info.addRow({BeautifulTable::table_timepoint(tables.start_time), BeautifulTable::table_timepoint(tables.end_time), + BeautifulTable::table_time(static_cast(tables.time_total.count())), BeautifulTable::table_time(static_cast(tables.time_ctracked.count())), + BeautifulTable::table_percentage(static_cast(tables.time_ctracked.count()), static_cast(tables.time_total.count()))}); + + info.print(stream); + BeautifulTable table({"filename", "function", "line", "calls", "ae" + center_intervall_str + "%", "ae[0-100]%", + "time ae[0-100]", "time a[0-100]"}, + use_color, alternate_colors); + for (const auto &row : tables.summary.rows) + { + table.addRow({BeautifulTable::stable_shortenPath(row.filename), row.function_name, BeautifulTable::table_string(row.line), + BeautifulTable::table_string(row.calls), + BeautifulTable::table_percentage(static_cast(row.percent_ae_bracket * tables.time_total.count() / 100.0), static_cast(tables.time_total.count())), + BeautifulTable::table_percentage(static_cast(row.percent_ae_all * tables.time_total.count() / 100.0), static_cast(tables.time_total.count())), + BeautifulTable::table_time(static_cast(row.time_ae_all.count())), + BeautifulTable::table_time(static_cast(row.time_a_all.count()))}); + } + + table.print(stream); + } + + template + void get_detail_table(StreamType &stream, bool use_color = false, bool reverse_vector = false) + { + auto details_copy = tables.details.rows; + if (reverse_vector) + { + std::reverse(details_copy.begin(), details_copy.end()); + } + for (int i = static_cast(details_copy.size()) - 1; i >= 0; i--) + { + const auto &detail = details_copy[i]; + + BeautifulTable info({"filename", "function", "line", "time acc", "sd", "cv", "calls", "threads"}, use_color, default_colors); + info.addRow({BeautifulTable::stable_shortenPath(detail.filename), detail.function_name, BeautifulTable::table_string(detail.line), + BeautifulTable::table_time(static_cast(detail.time_acc.count())), + BeautifulTable::table_time(static_cast(detail.sd.count())), BeautifulTable::table_string(detail.cv), + BeautifulTable::table_string(detail.calls), BeautifulTable::table_string(detail.threads)}); + + BeautifulTable table({"min", "mean", "min", "mean", "med", "time a", "time ae", "max", "mean", "max"}, use_color, default_colors, + {{"fastest[0-" + std::to_string(detail.fastest_range) + "]%", 2}, {"center" + center_intervall_str + "%", 6}, {"slowest[" + std::to_string(detail.slowest_range) + "-100]%", 2}}); + + table.addRow({BeautifulTable::table_time(static_cast(detail.fastest_min.count())), BeautifulTable::table_time(static_cast(detail.fastest_mean.count())), + BeautifulTable::table_time(static_cast(detail.center_min.count())), BeautifulTable::table_time(static_cast(detail.center_mean.count())), + BeautifulTable::table_time(static_cast(detail.center_med.count())), BeautifulTable::table_time(static_cast(detail.center_time_a.count())), + BeautifulTable::table_time(static_cast(detail.center_time_ae.count())), + BeautifulTable::table_time(static_cast(detail.center_max.count())), + BeautifulTable::table_time(static_cast(detail.slowest_mean.count())), BeautifulTable::table_time(static_cast(detail.slowest_max.count()))}); + + info.print(stream); + table.print(stream); + + stream << std::endl; + } + } + + void calculate_stats() + { + std::vector grouped_events{}; + for (auto &[filename, filename_entry] : f_res) + { + ctracked_files++; + for (auto &[function, function_entry] : filename_entry) + { + ctracked_functions++; + for (auto &[line, line_entry] : function_entry) + { + ctracked_uses++; + line_entry.filename = filename; + line_entry.function_name = function; + line_entry.line = line; + line_entry.calculateStats(settings.non_center_percent, a_events, child_graph); + sorted_events.push_back(&line_entry); + grouped_events.insert(grouped_events.end(), line_entry.all_grouped.begin(), line_entry.all_grouped.end()); + } + } + } + + std::sort(OPT_EXEC_POLICY grouped_events.begin(), grouped_events.end(), cmp_simple_event_by_start_time_asc); + auto all_grouped = sorted_create_grouped_simple_events(grouped_events); + sum_time_active_exclusive = sum_field(all_grouped, &Simple_Event::duration); + + order_pointer_vector_by_field(sorted_events, &EventGroup::all_time_active_exclusive, false); + + int fastest_events = static_cast(sorted_events.size() * settings.percent_exclude_fastest_active_exclusive / 100); + // remove fastest keep in mind fastest elements are at the back + if (fastest_events > 0) + sorted_events.erase(sorted_events.end() - fastest_events, sorted_events.end()); + + uint_fast64_t min_time_active_exclusive = static_cast(time_total * settings.min_percent_active_exclusive / 100); + // remove fastest keep in mind fastest elements are at the back + if (min_time_active_exclusive > 0) + sorted_events.erase(std::remove_if(sorted_events.begin(), sorted_events.end(), [min_time_active_exclusive](EventGroup *e) + { return e->all_time_active_exclusive < min_time_active_exclusive; }), + sorted_events.end()); + + // Build the structured result tables + build_result_tables(); + } + + void move_events_from_store(std::deque &events) + { + m_events_storage = std::move(events); + } + + void populate_maps() + { + size_t total_events = 0; + for (const auto &event_vec : m_events_storage) + { + total_events += event_vec.size(); + } + a_events.reserve(total_events); + + for (const auto &event_vec : m_events_storage) + { + for (const auto &event : event_vec) + { + f_res[event.filename][event.function][event.line].all_events.push_back(&event); + a_events.insert({get_unique_event_id(event.thread_id, event.event_id), &event}); + } + } + } + + void add_sub_events(const sub_events &s_events, const unsigned int thread_id_) + { + + for (auto const &[key, val] : s_events) + { + int_fast64_t parent_id = get_unique_event_id(thread_id_, key); + for (const auto &child : val) + { + child_graph[parent_id].push_back(get_unique_event_id(thread_id_, child)); + } + } + } + + std::unordered_map a_events{}; + filename_result f_res{}; + + std::unordered_map> child_graph{}; + ctrack_result_settings settings; + std::chrono::high_resolution_clock::time_point track_start_time, track_end_time; + uint_fast64_t time_total; + uint_fast64_t sum_time_active_exclusive = 0; + + uint_fast64_t ctracked_files = 0; + uint_fast64_t ctracked_functions = 0; + uint_fast64_t ctracked_uses = 0; + + std::vector sorted_events{}; + std::string center_intervall_str; + ctrack_result_tables tables{}; + + private: + std::deque m_events_storage; + + void build_result_tables() + { + // Populate meta information + tables.start_time = track_start_time; + tables.end_time = track_end_time; + tables.time_total = std::chrono::nanoseconds(time_total); + tables.time_ctracked = std::chrono::nanoseconds(sum_time_active_exclusive); + tables.settings = settings; + + // Clear existing data + tables.summary.rows.clear(); + tables.details.rows.clear(); + + // Reserve space for efficiency + tables.summary.rows.reserve(sorted_events.size()); + tables.details.rows.reserve(sorted_events.size()); + + // Build summary and detail rows from sorted_events + for (const auto &entry : sorted_events) + { + // Build summary row + summary_row sum_row; + sum_row.filename = std::string(entry->filename); + sum_row.function_name = std::string(entry->function_name); + sum_row.line = entry->line; + sum_row.calls = entry->all_cnt; + sum_row.percent_ae_bracket = (time_total > 0) ? (static_cast(entry->center_time_active_exclusive) / time_total * 100.0) : 0.0; + sum_row.percent_ae_all = (time_total > 0) ? (static_cast(entry->all_time_active_exclusive) / time_total * 100.0) : 0.0; + sum_row.time_ae_all = std::chrono::nanoseconds(entry->all_time_active_exclusive); + sum_row.time_a_all = std::chrono::nanoseconds(entry->all_time_active); + tables.summary.rows.push_back(sum_row); + + // Build detail row + detail_stats detail_row; + detail_row.filename = std::string(entry->filename); + detail_row.function_name = std::string(entry->function_name); + detail_row.line = entry->line; + detail_row.time_acc = std::chrono::nanoseconds(entry->all_time_acc); + detail_row.sd = std::chrono::nanoseconds(static_cast(entry->all_st)); + detail_row.cv = entry->all_cv; + detail_row.calls = entry->all_cnt; + detail_row.threads = entry->all_thread_cnt; + + // Summary-like fields (same calculations as summary row) + detail_row.percent_ae_bracket = (time_total > 0) ? (static_cast(entry->center_time_active_exclusive) / time_total * 100.0) : 0.0; + detail_row.percent_ae_all = (time_total > 0) ? (static_cast(entry->all_time_active_exclusive) / time_total * 100.0) : 0.0; + detail_row.time_ae_all = std::chrono::nanoseconds(entry->all_time_active_exclusive); + detail_row.time_a_all = std::chrono::nanoseconds(entry->all_time_active); + + // Fastest/Center/Slowest stats + detail_row.fastest_min = std::chrono::nanoseconds(entry->fastest_min); + detail_row.fastest_mean = std::chrono::nanoseconds(static_cast(entry->fastest_mean)); + detail_row.center_min = std::chrono::nanoseconds(entry->center_min); + detail_row.center_mean = std::chrono::nanoseconds(static_cast(entry->center_mean)); + detail_row.center_med = std::chrono::nanoseconds(entry->center_med); + detail_row.center_time_a = std::chrono::nanoseconds(entry->center_time_active); + detail_row.center_time_ae = std::chrono::nanoseconds(entry->center_time_active_exclusive); + detail_row.center_max = std::chrono::nanoseconds(entry->center_max); + detail_row.slowest_mean = std::chrono::nanoseconds(static_cast(entry->slowest_mean)); + detail_row.slowest_max = std::chrono::nanoseconds(entry->slowest_max); + + detail_row.fastest_range = entry->fastest_range; + detail_row.slowest_range = entry->slowest_range; + + tables.details.rows.push_back(detail_row); + } + } + + public: + const ctrack_result_tables &get_tables() const { return tables; } + }; + + inline int fetch_event_t_id() + { + if (thread_id == nullptr || *thread_id == -1) + { + std::scoped_lock lock(store::event_mutex); + + if (thread_id == nullptr) + { + store::a_thread_ids.emplace_back(++store::thread_cnt); + thread_id = &store::a_thread_ids[store::a_thread_ids.size() - 1]; + } + else + { + *thread_id = ++store::thread_cnt; + } + + store::a_events.emplace_back(t_events{}); + store::a_sub_events.emplace_back(sub_events{}); + store::a_current_event_id.emplace_back(0); + store::a_current_event_cnt.emplace_back(0); + store::a_string_id.emplace_back(0); + + event_ptr = &store::a_events[*thread_id]; + sub_events_ptr = &store::a_sub_events[*thread_id]; + + current_event_id = &store::a_current_event_id[*thread_id]; + current_event_cnt = &store::a_current_event_cnt[*thread_id]; + string_id = &store::a_string_id[*thread_id]; + + event_ptr->reserve(100); + } + return *thread_id; + } + + class EventHandler + { + public: + EventHandler(int line = __builtin_LINE(), const char *filename = __builtin_FILE(), const char *function = __builtin_FUNCTION(), std::chrono::high_resolution_clock::time_point start_time = std::chrono::high_resolution_clock::now()) : line(line) + + { + + previous_store_clear_cnt = store::store_clear_cnt; + this->filename = filename; + this->function = function; + while (store::write_events_locked) + { + } + + register_event(); + this->start_time = start_time; + } + ~EventHandler() + { + auto end_time = std::chrono::high_resolution_clock::now(); + while (store::write_events_locked) + { + } + + if (store::store_clear_cnt != previous_store_clear_cnt) + { + register_event(); + } + + if (event_ptr->capacity() - event_ptr->size() < 1) + event_ptr->reserve(event_ptr->capacity() * 4); + + event_ptr->emplace_back(Event{start_time, end_time, filename, line, function, t_id, event_id}); + + *current_event_id = previous_event_id; + if (previous_event_id > 0) + { + if ((*sub_events_ptr)[previous_event_id].capacity() - (*sub_events_ptr)[previous_event_id].size() < 1) + (*sub_events_ptr)[previous_event_id].reserve((*sub_events_ptr)[previous_event_id].capacity() * 4); + (*sub_events_ptr)[previous_event_id].push_back(event_id); + } + } + + private: + void register_event() + { + t_id = fetch_event_t_id(); + previous_event_id = *current_event_id; + event_id = ++(*current_event_cnt); + *current_event_id = event_id; + } + std::chrono::high_resolution_clock::time_point start_time; + int line; + unsigned int previous_store_clear_cnt; + + std::string_view filename, function; + + int t_id; + unsigned int event_id; + unsigned int previous_event_id; + }; + + inline void clear_a_store() + { + store::a_current_event_id.clear(); + store::a_current_event_id.shrink_to_fit(); + + store::a_current_event_cnt.clear(); + store::a_current_event_cnt.shrink_to_fit(); + + store::a_string_id.clear(); + store::a_string_id.shrink_to_fit(); + + store::a_events.clear(); + store::a_events.shrink_to_fit(); + + store::a_sub_events.clear(); + store::a_sub_events.shrink_to_fit(); + + store::thread_cnt = -1; + for (auto &entry : store::a_thread_ids) + { + entry = -1; + } + + event_ptr = nullptr; + sub_events_ptr = nullptr; + current_event_id = nullptr; + current_event_cnt = nullptr; + string_id = nullptr; + thread_id = nullptr; + + store::store_clear_cnt++; + store::track_start_time = std::chrono::high_resolution_clock::now(); + } + + inline ctrack_result calc_stats_and_clear(ctrack_result_settings settings = {}) + { + auto end = std::chrono::high_resolution_clock::now(); + ctrack_result res{settings, store::track_start_time, end}; + + // copy data + { + store::write_events_locked = true; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::scoped_lock lock(store::event_mutex); + + res.move_events_from_store(store::a_events); + res.populate_maps(); + + for (int thread_id_ = 0; thread_id_ <= store::thread_cnt; thread_id_++) + { + auto &t_sub_events = store::a_sub_events[thread_id_]; + res.add_sub_events(t_sub_events, thread_id_); + } + clear_a_store(); + store::write_events_locked = false; + } + + res.calculate_stats(); + store::track_start_time = std::chrono::high_resolution_clock::now(); + + return res; + } + + inline void result_print(ctrack_result_settings settings = {}) + { + auto res = calc_stats_and_clear(settings); + std::cout << "Details" << std::endl; + res.get_detail_table(std::cout, true); + std::cout << "Summary" << std::endl; + res.get_summary_table(std::cout, true); + } + + inline std::string result_as_string(ctrack_result_settings settings = {}) + { + auto res = calc_stats_and_clear(settings); + std::stringstream ss; + ss << "Summary\n"; + res.get_summary_table(ss, false); + ss << "Details\n"; + res.get_detail_table(ss, false, true); + + return ss.str(); + } + + inline ctrack_result_tables result_get_tables(ctrack_result_settings settings = {}) + { + auto res = calc_stats_and_clear(settings); + return res.get_tables(); + } + + inline summary_table result_get_summary_table(ctrack_result_settings settings = {}) + { + auto res = calc_stats_and_clear(settings); + return res.get_tables().summary; + } + + inline detail_table result_get_detail_table(ctrack_result_settings settings = {}) + { + auto res = calc_stats_and_clear(settings); + return res.get_tables().details; + } + } +} + +#ifndef CTRACK_DISABLE +#define CTRACK_CONCAT_IMPL(x, y) x##y +#define CTRACK_CONCAT(x, y) CTRACK_CONCAT_IMPL(x, y) +#define CTRACK_UNIQUE_NAME(prefix) CTRACK_CONCAT(prefix, __COUNTER__) + +#define CTRACK_IMPL \ + ctrack::EventHandler CTRACK_UNIQUE_NAME(ctrack_instance_) { __builtin_LINE(), __builtin_FILE(), __builtin_FUNCTION() } +#define CTRACK_IMPL_NAME(name) \ + ctrack::EventHandler CTRACK_UNIQUE_NAME(ctrack_instance_) { __builtin_LINE(), __builtin_FILE(), name } +#if defined(CTRACK_DISABLE_DEV) +#define CTRACK_PROD CTRACK_IMPL +#define CTRACK_PROD_NAME(name) CTRACK_IMPL_NAME(name) +#define CTRACK_DEV // Disabled +#define CTRACK_DEV_NAME(name) // Disabled +#elif defined(CTRACK_DISABLE_PROD) +#define CTRACK_PROD // Disabled +#define CTRACK_PROD_NAME(name) // Disabled +#define CTRACK_DEV CTRACK_IMPL +#define CTRACK_DEV_NAME(name) CTRACK_IMPL_NAME(name) +#else +#define CTRACK_PROD CTRACK_IMPL +#define CTRACK_PROD_NAME(name) CTRACK_IMPL_NAME(name) +#define CTRACK_DEV CTRACK_IMPL +#define CTRACK_DEV_NAME(name) CTRACK_IMPL_NAME(name) +#endif + +// Alias CTRACK to CTRACK_PROD +#define CTRACK CTRACK_PROD +#define CTRACK_NAME(name) CTRACK_PROD_NAME(name) + +#else // CTRACK_DISABLE +#define CTRACK_PROD +#define CTRACK_PROD_NAME(name) +#define CTRACK_DEV +#define CTRACK_DEV_NAME(name) +#define CTRACK +#define CTRACK_NAME(name) +#endif // CTRACK_DISABLE + #endif \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..be693c2 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,57 @@ +# Test configuration for ctrack +cmake_minimum_required(VERSION 3.12) + +# Enable testing +enable_testing() + +# Include directories +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) +include_directories(${CMAKE_SOURCE_DIR}/include) + +# Common test compilation flags +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Define test executables +set(TEST_SOURCES + test_basic.cpp + test_multithreaded.cpp + test_nested.cpp + test_statistics.cpp + test_results.cpp + test_edge_cases.cpp +) + +# Create test executables and register with CTest +foreach(test_source ${TEST_SOURCES}) + # Get test name from filename + string(REPLACE ".cpp" "" test_name ${test_source}) + + # Create executable + add_executable(${test_name} ${test_source}) + + # Link with ctrack + target_link_libraries(${test_name} ctrack) + + # Add pthread support for multithreading tests + if(NOT WIN32) + target_link_libraries(${test_name} pthread) + endif() + + # Register with CTest + add_test(NAME ${test_name} COMMAND ${test_name}) + + # Set test properties for better output + set_tests_properties(${test_name} PROPERTIES + TIMEOUT 60 + FAIL_REGULAR_EXPRESSION "FAILED" + ) +endforeach() + +# Add a custom target to run all tests +add_custom_target(run_tests + COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure + DEPENDS ${TEST_SOURCES} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT "Running all ctrack tests" +) \ No newline at end of file diff --git a/test/doctest.h b/test/doctest.h new file mode 100644 index 0000000..52b4a4a --- /dev/null +++ b/test/doctest.h @@ -0,0 +1,7134 @@ +// ====================================================================== lgtm [cpp/missing-header-guard] +// == DO NOT MODIFY THIS FILE BY HAND - IT IS AUTO GENERATED BY CMAKE! == +// ====================================================================== +// +// doctest.h - the lightest feature-rich C++ single-header testing framework for unit tests and TDD +// +// Copyright (c) 2016-2023 Viktor Kirilov +// +// Distributed under the MIT Software License +// See accompanying file LICENSE.txt or copy at +// https://opensource.org/licenses/MIT +// +// The documentation can be found at the library's page: +// https://github.com/doctest/doctest/blob/master/doc/markdown/readme.md +// +// ================================================================================================= +// ================================================================================================= +// ================================================================================================= +// +// The library is heavily influenced by Catch - https://github.com/catchorg/Catch2 +// which uses the Boost Software License - Version 1.0 +// see here - https://github.com/catchorg/Catch2/blob/master/LICENSE.txt +// +// The concept of subcases (sections in Catch) and expression decomposition are from there. +// Some parts of the code are taken directly: +// - stringification - the detection of "ostream& operator<<(ostream&, const T&)" and StringMaker<> +// - the Approx() helper class for floating point comparison +// - colors in the console +// - breaking into a debugger +// - signal / SEH handling +// - timer +// - XmlWriter class - thanks to Phil Nash for allowing the direct reuse (AKA copy/paste) +// +// The expression decomposing templates are taken from lest - https://github.com/martinmoene/lest +// which uses the Boost Software License - Version 1.0 +// see here - https://github.com/martinmoene/lest/blob/master/LICENSE.txt +// +// ================================================================================================= +// ================================================================================================= +// ================================================================================================= + +#ifndef DOCTEST_LIBRARY_INCLUDED +#define DOCTEST_LIBRARY_INCLUDED + +// ================================================================================================= +// == VERSION ====================================================================================== +// ================================================================================================= + +#define DOCTEST_VERSION_MAJOR 2 +#define DOCTEST_VERSION_MINOR 4 +#define DOCTEST_VERSION_PATCH 12 + +// util we need here +#define DOCTEST_TOSTR_IMPL(x) #x +#define DOCTEST_TOSTR(x) DOCTEST_TOSTR_IMPL(x) + +#define DOCTEST_VERSION_STR \ + DOCTEST_TOSTR(DOCTEST_VERSION_MAJOR) "." \ + DOCTEST_TOSTR(DOCTEST_VERSION_MINOR) "." \ + DOCTEST_TOSTR(DOCTEST_VERSION_PATCH) + +#define DOCTEST_VERSION \ + (DOCTEST_VERSION_MAJOR * 10000 + DOCTEST_VERSION_MINOR * 100 + DOCTEST_VERSION_PATCH) + +// ================================================================================================= +// == COMPILER VERSION ============================================================================= +// ================================================================================================= + +// ideas for the version stuff are taken from here: https://github.com/cxxstuff/cxx_detect + +#ifdef _MSC_VER +#define DOCTEST_CPLUSPLUS _MSVC_LANG +#else +#define DOCTEST_CPLUSPLUS __cplusplus +#endif + +#define DOCTEST_COMPILER(MAJOR, MINOR, PATCH) ((MAJOR)*10000000 + (MINOR)*100000 + (PATCH)) + +// GCC/Clang and GCC/MSVC are mutually exclusive, but Clang/MSVC are not because of clang-cl... +#if defined(_MSC_VER) && defined(_MSC_FULL_VER) +#if _MSC_VER == _MSC_FULL_VER / 10000 +#define DOCTEST_MSVC DOCTEST_COMPILER(_MSC_VER / 100, _MSC_VER % 100, _MSC_FULL_VER % 10000) +#else // MSVC +#define DOCTEST_MSVC \ + DOCTEST_COMPILER(_MSC_VER / 100, (_MSC_FULL_VER / 100000) % 100, _MSC_FULL_VER % 100000) +#endif // MSVC +#endif // MSVC +#if defined(__clang__) && defined(__clang_minor__) && defined(__clang_patchlevel__) +#define DOCTEST_CLANG DOCTEST_COMPILER(__clang_major__, __clang_minor__, __clang_patchlevel__) +#elif defined(__GNUC__) && defined(__GNUC_MINOR__) && defined(__GNUC_PATCHLEVEL__) && \ + !defined(__INTEL_COMPILER) +#define DOCTEST_GCC DOCTEST_COMPILER(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#endif // GCC +#if defined(__INTEL_COMPILER) +#define DOCTEST_ICC DOCTEST_COMPILER(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, 0) +#endif // ICC + +#ifndef DOCTEST_MSVC +#define DOCTEST_MSVC 0 +#endif // DOCTEST_MSVC +#ifndef DOCTEST_CLANG +#define DOCTEST_CLANG 0 +#endif // DOCTEST_CLANG +#ifndef DOCTEST_GCC +#define DOCTEST_GCC 0 +#endif // DOCTEST_GCC +#ifndef DOCTEST_ICC +#define DOCTEST_ICC 0 +#endif // DOCTEST_ICC + +// ================================================================================================= +// == COMPILER WARNINGS HELPERS ==================================================================== +// ================================================================================================= + +#if DOCTEST_CLANG && !DOCTEST_ICC +#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x) +#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH _Pragma("clang diagnostic push") +#define DOCTEST_CLANG_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(clang diagnostic ignored w) +#define DOCTEST_CLANG_SUPPRESS_WARNING_POP _Pragma("clang diagnostic pop") +#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w) \ + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH DOCTEST_CLANG_SUPPRESS_WARNING(w) +#else // DOCTEST_CLANG +#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +#define DOCTEST_CLANG_SUPPRESS_WARNING(w) +#define DOCTEST_CLANG_SUPPRESS_WARNING_POP +#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_CLANG + +#if DOCTEST_GCC +#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x) +#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH _Pragma("GCC diagnostic push") +#define DOCTEST_GCC_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(GCC diagnostic ignored w) +#define DOCTEST_GCC_SUPPRESS_WARNING_POP _Pragma("GCC diagnostic pop") +#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w) \ + DOCTEST_GCC_SUPPRESS_WARNING_PUSH DOCTEST_GCC_SUPPRESS_WARNING(w) +#else // DOCTEST_GCC +#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH +#define DOCTEST_GCC_SUPPRESS_WARNING(w) +#define DOCTEST_GCC_SUPPRESS_WARNING_POP +#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_GCC + +#if DOCTEST_MSVC +#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH __pragma(warning(push)) +#define DOCTEST_MSVC_SUPPRESS_WARNING(w) __pragma(warning(disable : w)) +#define DOCTEST_MSVC_SUPPRESS_WARNING_POP __pragma(warning(pop)) +#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w) \ + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH DOCTEST_MSVC_SUPPRESS_WARNING(w) +#else // DOCTEST_MSVC +#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +#define DOCTEST_MSVC_SUPPRESS_WARNING(w) +#define DOCTEST_MSVC_SUPPRESS_WARNING_POP +#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_MSVC + +// ================================================================================================= +// == COMPILER WARNINGS ============================================================================ +// ================================================================================================= + +// both the header and the implementation suppress all of these, +// so it only makes sense to aggregate them like so +#define DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH \ + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunknown-pragmas") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wweak-vtables") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wpadded") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-prototypes") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat-pedantic") \ + \ + DOCTEST_GCC_SUPPRESS_WARNING_PUSH \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wunknown-pragmas") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wpragmas") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Weffc++") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-overflow") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-aliasing") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-declarations") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wuseless-cast") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wnoexcept") \ + \ + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH \ + /* these 4 also disabled globally via cmake: */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4514) /* unreferenced inline function has been removed */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4571) /* SEH related */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4710) /* function not inlined */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4711) /* function selected for inline expansion*/ \ + /* common ones */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4616) /* invalid compiler warning */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4619) /* invalid compiler warning */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4996) /* The compiler encountered a deprecated declaration */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4706) /* assignment within conditional expression */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4512) /* 'class' : assignment operator could not be generated */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4127) /* conditional expression is constant */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4820) /* padding */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4625) /* copy constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4626) /* assignment operator was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5027) /* move assignment operator implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5026) /* move constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4640) /* construction of local static object not thread-safe */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5045) /* Spectre mitigation for memory load */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5264) /* 'variable-name': 'const' variable is not used */ \ + /* static analysis */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26439) /* Function may not throw. Declare it 'noexcept' */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26495) /* Always initialize a member variable */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26451) /* Arithmetic overflow ... */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26444) /* Avoid unnamed objects with custom ctor and dtor... */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26812) /* Prefer 'enum class' over 'enum' */ + +#define DOCTEST_SUPPRESS_COMMON_WARNINGS_POP \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP \ + DOCTEST_GCC_SUPPRESS_WARNING_POP \ + DOCTEST_MSVC_SUPPRESS_WARNING_POP + +DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH + +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wnon-virtual-dtor") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wdeprecated") + +DOCTEST_GCC_SUPPRESS_WARNING_PUSH +DOCTEST_GCC_SUPPRESS_WARNING("-Wctor-dtor-privacy") +DOCTEST_GCC_SUPPRESS_WARNING("-Wnon-virtual-dtor") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-promo") + +DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +DOCTEST_MSVC_SUPPRESS_WARNING(4623) // default constructor was implicitly defined as deleted + +#define DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN \ + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH \ + DOCTEST_MSVC_SUPPRESS_WARNING(4548) /* before comma no effect; expected side - effect */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4265) /* virtual functions, but destructor is not virtual */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4986) /* exception specification does not match previous */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4350) /* 'member1' called instead of 'member2' */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4668) /* not defined as a preprocessor macro */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4365) /* signed/unsigned mismatch */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4774) /* format string not a string literal */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4820) /* padding */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4625) /* copy constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4626) /* assignment operator was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5027) /* move assignment operator implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5026) /* move constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4623) /* default constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5039) /* pointer to pot. throwing function passed to extern C */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5045) /* Spectre mitigation for memory load */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5105) /* macro producing 'defined' has undefined behavior */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4738) /* storing float result in memory, loss of performance */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5262) /* implicit fall-through */ + +#define DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END DOCTEST_MSVC_SUPPRESS_WARNING_POP + +// ================================================================================================= +// == FEATURE DETECTION ============================================================================ +// ================================================================================================= + +// general compiler feature support table: https://en.cppreference.com/w/cpp/compiler_support +// MSVC C++11 feature support table: https://msdn.microsoft.com/en-us/library/hh567368.aspx +// GCC C++11 feature support table: https://gcc.gnu.org/projects/cxx-status.html +// MSVC version table: +// https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#Internal_version_numbering +// MSVC++ 14.3 (17) _MSC_VER == 1930 (Visual Studio 2022) +// MSVC++ 14.2 (16) _MSC_VER == 1920 (Visual Studio 2019) +// MSVC++ 14.1 (15) _MSC_VER == 1910 (Visual Studio 2017) +// MSVC++ 14.0 _MSC_VER == 1900 (Visual Studio 2015) +// MSVC++ 12.0 _MSC_VER == 1800 (Visual Studio 2013) +// MSVC++ 11.0 _MSC_VER == 1700 (Visual Studio 2012) +// MSVC++ 10.0 _MSC_VER == 1600 (Visual Studio 2010) +// MSVC++ 9.0 _MSC_VER == 1500 (Visual Studio 2008) +// MSVC++ 8.0 _MSC_VER == 1400 (Visual Studio 2005) + +// Universal Windows Platform support +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) +#define DOCTEST_CONFIG_NO_WINDOWS_SEH +#endif // WINAPI_FAMILY +#if DOCTEST_MSVC && !defined(DOCTEST_CONFIG_WINDOWS_SEH) +#define DOCTEST_CONFIG_WINDOWS_SEH +#endif // MSVC +#if defined(DOCTEST_CONFIG_NO_WINDOWS_SEH) && defined(DOCTEST_CONFIG_WINDOWS_SEH) +#undef DOCTEST_CONFIG_WINDOWS_SEH +#endif // DOCTEST_CONFIG_NO_WINDOWS_SEH + +#if !defined(_WIN32) && !defined(__QNX__) && !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && \ + !defined(__EMSCRIPTEN__) && !defined(__wasi__) +#define DOCTEST_CONFIG_POSIX_SIGNALS +#endif // _WIN32 +#if defined(DOCTEST_CONFIG_NO_POSIX_SIGNALS) && defined(DOCTEST_CONFIG_POSIX_SIGNALS) +#undef DOCTEST_CONFIG_POSIX_SIGNALS +#endif // DOCTEST_CONFIG_NO_POSIX_SIGNALS + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +#if !defined(__cpp_exceptions) && !defined(__EXCEPTIONS) && !defined(_CPPUNWIND) \ + || defined(__wasi__) +#define DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // no exceptions +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +#define DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#if defined(DOCTEST_CONFIG_NO_EXCEPTIONS) && !defined(DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS) +#define DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS && !DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS + +#ifdef __wasi__ +#define DOCTEST_CONFIG_NO_MULTITHREADING +#endif + +#if defined(DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN) && !defined(DOCTEST_CONFIG_IMPLEMENT) +#define DOCTEST_CONFIG_IMPLEMENT +#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + +#if defined(_WIN32) || defined(__CYGWIN__) +#if DOCTEST_MSVC +#define DOCTEST_SYMBOL_EXPORT __declspec(dllexport) +#define DOCTEST_SYMBOL_IMPORT __declspec(dllimport) +#else // MSVC +#define DOCTEST_SYMBOL_EXPORT __attribute__((dllexport)) +#define DOCTEST_SYMBOL_IMPORT __attribute__((dllimport)) +#endif // MSVC +#else // _WIN32 +#define DOCTEST_SYMBOL_EXPORT __attribute__((visibility("default"))) +#define DOCTEST_SYMBOL_IMPORT +#endif // _WIN32 + +#ifdef DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL +#ifdef DOCTEST_CONFIG_IMPLEMENT +#define DOCTEST_INTERFACE DOCTEST_SYMBOL_EXPORT +#else // DOCTEST_CONFIG_IMPLEMENT +#define DOCTEST_INTERFACE DOCTEST_SYMBOL_IMPORT +#endif // DOCTEST_CONFIG_IMPLEMENT +#else // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL +#define DOCTEST_INTERFACE +#endif // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL + +// needed for extern template instantiations +// see https://github.com/fmtlib/fmt/issues/2228 +#if DOCTEST_MSVC +#define DOCTEST_INTERFACE_DECL +#define DOCTEST_INTERFACE_DEF DOCTEST_INTERFACE +#else // DOCTEST_MSVC +#define DOCTEST_INTERFACE_DECL DOCTEST_INTERFACE +#define DOCTEST_INTERFACE_DEF +#endif // DOCTEST_MSVC + +#define DOCTEST_EMPTY + +#if DOCTEST_MSVC +#define DOCTEST_NOINLINE __declspec(noinline) +#define DOCTEST_UNUSED +#define DOCTEST_ALIGNMENT(x) +#elif DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 5, 0) +#define DOCTEST_NOINLINE +#define DOCTEST_UNUSED +#define DOCTEST_ALIGNMENT(x) +#else +#define DOCTEST_NOINLINE __attribute__((noinline)) +#define DOCTEST_UNUSED __attribute__((unused)) +#define DOCTEST_ALIGNMENT(x) __attribute__((aligned(x))) +#endif + +#ifdef DOCTEST_CONFIG_NO_CONTRADICTING_INLINE +#define DOCTEST_INLINE_NOINLINE inline +#else +#define DOCTEST_INLINE_NOINLINE inline DOCTEST_NOINLINE +#endif + +#ifndef DOCTEST_NORETURN +#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_NORETURN +#else // DOCTEST_MSVC +#define DOCTEST_NORETURN [[noreturn]] +#endif // DOCTEST_MSVC +#endif // DOCTEST_NORETURN + +#ifndef DOCTEST_NOEXCEPT +#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_NOEXCEPT +#else // DOCTEST_MSVC +#define DOCTEST_NOEXCEPT noexcept +#endif // DOCTEST_MSVC +#endif // DOCTEST_NOEXCEPT + +#ifndef DOCTEST_CONSTEXPR +#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_CONSTEXPR const +#define DOCTEST_CONSTEXPR_FUNC inline +#else // DOCTEST_MSVC +#define DOCTEST_CONSTEXPR constexpr +#define DOCTEST_CONSTEXPR_FUNC constexpr +#endif // DOCTEST_MSVC +#endif // DOCTEST_CONSTEXPR + +#ifndef DOCTEST_NO_SANITIZE_INTEGER +#if DOCTEST_CLANG >= DOCTEST_COMPILER(3, 7, 0) +#define DOCTEST_NO_SANITIZE_INTEGER __attribute__((no_sanitize("integer"))) +#else +#define DOCTEST_NO_SANITIZE_INTEGER +#endif +#endif // DOCTEST_NO_SANITIZE_INTEGER + +// ================================================================================================= +// == FEATURE DETECTION END ======================================================================== +// ================================================================================================= + +#define DOCTEST_DECLARE_INTERFACE(name) \ + virtual ~name(); \ + name() = default; \ + name(const name&) = delete; \ + name(name&&) = delete; \ + name& operator=(const name&) = delete; \ + name& operator=(name&&) = delete; + +#define DOCTEST_DEFINE_INTERFACE(name) \ + name::~name() = default; + +// internal macros for string concatenation and anonymous variable name generation +#define DOCTEST_CAT_IMPL(s1, s2) s1##s2 +#define DOCTEST_CAT(s1, s2) DOCTEST_CAT_IMPL(s1, s2) +#ifdef __COUNTER__ // not standard and may be missing for some compilers +#define DOCTEST_ANONYMOUS(x) DOCTEST_CAT(x, __COUNTER__) +#else // __COUNTER__ +#define DOCTEST_ANONYMOUS(x) DOCTEST_CAT(x, __LINE__) +#endif // __COUNTER__ + +#ifndef DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE +#define DOCTEST_REF_WRAP(x) x& +#else // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE +#define DOCTEST_REF_WRAP(x) x +#endif // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE + +// not using __APPLE__ because... this is how Catch does it +#ifdef __MAC_OS_X_VERSION_MIN_REQUIRED +#define DOCTEST_PLATFORM_MAC +#elif defined(__IPHONE_OS_VERSION_MIN_REQUIRED) +#define DOCTEST_PLATFORM_IPHONE +#elif defined(_WIN32) +#define DOCTEST_PLATFORM_WINDOWS +#elif defined(__wasi__) +#define DOCTEST_PLATFORM_WASI +#else // DOCTEST_PLATFORM +#define DOCTEST_PLATFORM_LINUX +#endif // DOCTEST_PLATFORM + +namespace doctest { namespace detail { + static DOCTEST_CONSTEXPR int consume(const int*, int) noexcept { return 0; } +}} + +#define DOCTEST_GLOBAL_NO_WARNINGS(var, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wglobal-constructors") \ + static const int var = doctest::detail::consume(&var, __VA_ARGS__); \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#ifndef DOCTEST_BREAK_INTO_DEBUGGER +// should probably take a look at https://github.com/scottt/debugbreak +#ifdef DOCTEST_PLATFORM_LINUX +#if defined(__GNUC__) && (defined(__i386) || defined(__x86_64)) +// Break at the location of the failing check if possible +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("int $3\n" : :) // NOLINT(hicpp-no-assembler) +#else +#include +#define DOCTEST_BREAK_INTO_DEBUGGER() raise(SIGTRAP) +#endif +#elif defined(DOCTEST_PLATFORM_MAC) +#if defined(__x86_64) || defined(__x86_64__) || defined(__amd64__) || defined(__i386) +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("int $3\n" : :) // NOLINT(hicpp-no-assembler) +#elif defined(__ppc__) || defined(__ppc64__) +// https://www.cocoawithlove.com/2008/03/break-into-debugger.html +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("li r0, 20\nsc\nnop\nli r0, 37\nli r4, 2\nsc\nnop\n": : : "memory","r0","r3","r4") // NOLINT(hicpp-no-assembler) +#else +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("brk #0"); // NOLINT(hicpp-no-assembler) +#endif +#elif DOCTEST_MSVC +#define DOCTEST_BREAK_INTO_DEBUGGER() __debugbreak() +#elif defined(__MINGW32__) +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wredundant-decls") +extern "C" __declspec(dllimport) void __stdcall DebugBreak(); +DOCTEST_GCC_SUPPRESS_WARNING_POP +#define DOCTEST_BREAK_INTO_DEBUGGER() ::DebugBreak() +#else // linux +#define DOCTEST_BREAK_INTO_DEBUGGER() (static_cast(0)) +#endif // linux +#endif // DOCTEST_BREAK_INTO_DEBUGGER + +// this is kept here for backwards compatibility since the config option was changed +#ifdef DOCTEST_CONFIG_USE_IOSFWD +#ifndef DOCTEST_CONFIG_USE_STD_HEADERS +#define DOCTEST_CONFIG_USE_STD_HEADERS +#endif +#endif // DOCTEST_CONFIG_USE_IOSFWD + +// for clang - always include ciso646 (which drags some std stuff) because +// we want to check if we are using libc++ with the _LIBCPP_VERSION macro in +// which case we don't want to forward declare stuff from std - for reference: +// https://github.com/doctest/doctest/issues/126 +// https://github.com/doctest/doctest/issues/356 +#if DOCTEST_CLANG +#include +#endif // clang + +#ifdef _LIBCPP_VERSION +#ifndef DOCTEST_CONFIG_USE_STD_HEADERS +#define DOCTEST_CONFIG_USE_STD_HEADERS +#endif +#endif // _LIBCPP_VERSION + +#ifdef DOCTEST_CONFIG_USE_STD_HEADERS +#ifndef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#define DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN +#include +#include +#include +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END +#else // DOCTEST_CONFIG_USE_STD_HEADERS + +// Forward declaring 'X' in namespace std is not permitted by the C++ Standard. +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4643) + +namespace std { // NOLINT(cert-dcl58-cpp) +typedef decltype(nullptr) nullptr_t; // NOLINT(modernize-use-using) +typedef decltype(sizeof(void*)) size_t; // NOLINT(modernize-use-using) +template +struct char_traits; +template <> +struct char_traits; +template +class basic_ostream; // NOLINT(fuchsia-virtual-inheritance) +typedef basic_ostream> ostream; // NOLINT(modernize-use-using) +template +// NOLINTNEXTLINE +basic_ostream& operator<<(basic_ostream&, const char*); +template +class basic_istream; +typedef basic_istream> istream; // NOLINT(modernize-use-using) +template +class tuple; +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 +template +class allocator; +template +class basic_string; +using string = basic_string, allocator>; +#endif // VS 2019 +} // namespace std + +DOCTEST_MSVC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_USE_STD_HEADERS + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#include +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + +namespace doctest { + +using std::size_t; + +DOCTEST_INTERFACE extern bool is_running_in_test; + +#ifndef DOCTEST_CONFIG_STRING_SIZE_TYPE +#define DOCTEST_CONFIG_STRING_SIZE_TYPE unsigned +#endif + +// A 24 byte string class (can be as small as 17 for x64 and 13 for x86) that can hold strings with length +// of up to 23 chars on the stack before going on the heap - the last byte of the buffer is used for: +// - "is small" bit - the highest bit - if "0" then it is small - otherwise its "1" (128) +// - if small - capacity left before going on the heap - using the lowest 5 bits +// - if small - 2 bits are left unused - the second and third highest ones +// - if small - acts as a null terminator if strlen() is 23 (24 including the null terminator) +// and the "is small" bit remains "0" ("as well as the capacity left") so its OK +// Idea taken from this lecture about the string implementation of facebook/folly - fbstring +// https://www.youtube.com/watch?v=kPR8h4-qZdk +// TODO: +// - optimizations - like not deleting memory unnecessarily in operator= and etc. +// - resize/reserve/clear +// - replace +// - back/front +// - iterator stuff +// - find & friends +// - push_back/pop_back +// - assign/insert/erase +// - relational operators as free functions - taking const char* as one of the params +class DOCTEST_INTERFACE String +{ +public: + using size_type = DOCTEST_CONFIG_STRING_SIZE_TYPE; + +private: + static DOCTEST_CONSTEXPR size_type len = 24; //!OCLINT avoid private static members + static DOCTEST_CONSTEXPR size_type last = len - 1; //!OCLINT avoid private static members + + struct view // len should be more than sizeof(view) - because of the final byte for flags + { + char* ptr; + size_type size; + size_type capacity; + }; + + union + { + char buf[len]; // NOLINT(*-avoid-c-arrays) + view data; + }; + + char* allocate(size_type sz); + + bool isOnStack() const noexcept { return (buf[last] & 128) == 0; } + void setOnHeap() noexcept; + void setLast(size_type in = last) noexcept; + void setSize(size_type sz) noexcept; + + void copy(const String& other); + +public: + static DOCTEST_CONSTEXPR size_type npos = static_cast(-1); + + String() noexcept; + ~String(); + + // cppcheck-suppress noExplicitConstructor + String(const char* in); + String(const char* in, size_type in_size); + + String(std::istream& in, size_type in_size); + + String(const String& other); + String& operator=(const String& other); + + String& operator+=(const String& other); + + String(String&& other) noexcept; + String& operator=(String&& other) noexcept; + + char operator[](size_type i) const; + char& operator[](size_type i); + + // the only functions I'm willing to leave in the interface - available for inlining + const char* c_str() const { return const_cast(this)->c_str(); } // NOLINT + char* c_str() { + if (isOnStack()) { + return reinterpret_cast(buf); + } + return data.ptr; + } + + size_type size() const; + size_type capacity() const; + + String substr(size_type pos, size_type cnt = npos) &&; + String substr(size_type pos, size_type cnt = npos) const &; + + size_type find(char ch, size_type pos = 0) const; + size_type rfind(char ch, size_type pos = npos) const; + + int compare(const char* other, bool no_case = false) const; + int compare(const String& other, bool no_case = false) const; + +friend DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& s, const String& in); +}; + +DOCTEST_INTERFACE String operator+(const String& lhs, const String& rhs); + +DOCTEST_INTERFACE bool operator==(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator!=(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator<(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator>(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator<=(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator>=(const String& lhs, const String& rhs); + +class DOCTEST_INTERFACE Contains { +public: + explicit Contains(const String& string); + + bool checkWith(const String& other) const; + + String string; +}; + +DOCTEST_INTERFACE String toString(const Contains& in); + +DOCTEST_INTERFACE bool operator==(const String& lhs, const Contains& rhs); +DOCTEST_INTERFACE bool operator==(const Contains& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator!=(const String& lhs, const Contains& rhs); +DOCTEST_INTERFACE bool operator!=(const Contains& lhs, const String& rhs); + +namespace Color { + enum Enum + { + None = 0, + White, + Red, + Green, + Blue, + Cyan, + Yellow, + Grey, + + Bright = 0x10, + + BrightRed = Bright | Red, + BrightGreen = Bright | Green, + LightGrey = Bright | Grey, + BrightWhite = Bright | White + }; + + DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& s, Color::Enum code); +} // namespace Color + +namespace assertType { + enum Enum + { + // macro traits + + is_warn = 1, + is_check = 2 * is_warn, + is_require = 2 * is_check, + + is_normal = 2 * is_require, + is_throws = 2 * is_normal, + is_throws_as = 2 * is_throws, + is_throws_with = 2 * is_throws_as, + is_nothrow = 2 * is_throws_with, + + is_false = 2 * is_nothrow, + is_unary = 2 * is_false, // not checked anywhere - used just to distinguish the types + + is_eq = 2 * is_unary, + is_ne = 2 * is_eq, + + is_lt = 2 * is_ne, + is_gt = 2 * is_lt, + + is_ge = 2 * is_gt, + is_le = 2 * is_ge, + + // macro types + + DT_WARN = is_normal | is_warn, + DT_CHECK = is_normal | is_check, + DT_REQUIRE = is_normal | is_require, + + DT_WARN_FALSE = is_normal | is_false | is_warn, + DT_CHECK_FALSE = is_normal | is_false | is_check, + DT_REQUIRE_FALSE = is_normal | is_false | is_require, + + DT_WARN_THROWS = is_throws | is_warn, + DT_CHECK_THROWS = is_throws | is_check, + DT_REQUIRE_THROWS = is_throws | is_require, + + DT_WARN_THROWS_AS = is_throws_as | is_warn, + DT_CHECK_THROWS_AS = is_throws_as | is_check, + DT_REQUIRE_THROWS_AS = is_throws_as | is_require, + + DT_WARN_THROWS_WITH = is_throws_with | is_warn, + DT_CHECK_THROWS_WITH = is_throws_with | is_check, + DT_REQUIRE_THROWS_WITH = is_throws_with | is_require, + + DT_WARN_THROWS_WITH_AS = is_throws_with | is_throws_as | is_warn, + DT_CHECK_THROWS_WITH_AS = is_throws_with | is_throws_as | is_check, + DT_REQUIRE_THROWS_WITH_AS = is_throws_with | is_throws_as | is_require, + + DT_WARN_NOTHROW = is_nothrow | is_warn, + DT_CHECK_NOTHROW = is_nothrow | is_check, + DT_REQUIRE_NOTHROW = is_nothrow | is_require, + + DT_WARN_EQ = is_normal | is_eq | is_warn, + DT_CHECK_EQ = is_normal | is_eq | is_check, + DT_REQUIRE_EQ = is_normal | is_eq | is_require, + + DT_WARN_NE = is_normal | is_ne | is_warn, + DT_CHECK_NE = is_normal | is_ne | is_check, + DT_REQUIRE_NE = is_normal | is_ne | is_require, + + DT_WARN_GT = is_normal | is_gt | is_warn, + DT_CHECK_GT = is_normal | is_gt | is_check, + DT_REQUIRE_GT = is_normal | is_gt | is_require, + + DT_WARN_LT = is_normal | is_lt | is_warn, + DT_CHECK_LT = is_normal | is_lt | is_check, + DT_REQUIRE_LT = is_normal | is_lt | is_require, + + DT_WARN_GE = is_normal | is_ge | is_warn, + DT_CHECK_GE = is_normal | is_ge | is_check, + DT_REQUIRE_GE = is_normal | is_ge | is_require, + + DT_WARN_LE = is_normal | is_le | is_warn, + DT_CHECK_LE = is_normal | is_le | is_check, + DT_REQUIRE_LE = is_normal | is_le | is_require, + + DT_WARN_UNARY = is_normal | is_unary | is_warn, + DT_CHECK_UNARY = is_normal | is_unary | is_check, + DT_REQUIRE_UNARY = is_normal | is_unary | is_require, + + DT_WARN_UNARY_FALSE = is_normal | is_false | is_unary | is_warn, + DT_CHECK_UNARY_FALSE = is_normal | is_false | is_unary | is_check, + DT_REQUIRE_UNARY_FALSE = is_normal | is_false | is_unary | is_require, + }; +} // namespace assertType + +DOCTEST_INTERFACE const char* assertString(assertType::Enum at); +DOCTEST_INTERFACE const char* failureString(assertType::Enum at); +DOCTEST_INTERFACE const char* skipPathFromFilename(const char* file); + +struct DOCTEST_INTERFACE TestCaseData +{ + String m_file; // the file in which the test was registered (using String - see #350) + unsigned m_line; // the line where the test was registered + const char* m_name; // name of the test case + const char* m_test_suite; // the test suite in which the test was added + const char* m_description; + bool m_skip; + bool m_no_breaks; + bool m_no_output; + bool m_may_fail; + bool m_should_fail; + int m_expected_failures; + double m_timeout; +}; + +struct DOCTEST_INTERFACE AssertData +{ + // common - for all asserts + const TestCaseData* m_test_case; + assertType::Enum m_at; + const char* m_file; + int m_line; + const char* m_expr; + bool m_failed; + + // exception-related - for all asserts + bool m_threw; + String m_exception; + + // for normal asserts + String m_decomp; + + // for specific exception-related asserts + bool m_threw_as; + const char* m_exception_type; + + class DOCTEST_INTERFACE StringContains { + private: + Contains content; + bool isContains; + + public: + StringContains(const String& str) : content(str), isContains(false) { } + StringContains(Contains cntn) : content(static_cast(cntn)), isContains(true) { } + + bool check(const String& str) { return isContains ? (content == str) : (content.string == str); } + + operator const String&() const { return content.string; } + + const char* c_str() const { return content.string.c_str(); } + } m_exception_string; + + AssertData(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type, const StringContains& exception_string); +}; + +struct DOCTEST_INTERFACE MessageData +{ + String m_string; + const char* m_file; + int m_line; + assertType::Enum m_severity; +}; + +struct DOCTEST_INTERFACE SubcaseSignature +{ + String m_name; + const char* m_file; + int m_line; + + bool operator==(const SubcaseSignature& other) const; + bool operator<(const SubcaseSignature& other) const; +}; + +struct DOCTEST_INTERFACE IContextScope +{ + DOCTEST_DECLARE_INTERFACE(IContextScope) + virtual void stringify(std::ostream*) const = 0; +}; + +namespace detail { + struct DOCTEST_INTERFACE TestCase; +} // namespace detail + +struct ContextOptions //!OCLINT too many fields +{ + std::ostream* cout = nullptr; // stdout stream + String binary_name; // the test binary name + + const detail::TestCase* currentTest = nullptr; + + // == parameters from the command line + String out; // output filename + String order_by; // how tests should be ordered + unsigned rand_seed; // the seed for rand ordering + + unsigned first; // the first (matching) test to be executed + unsigned last; // the last (matching) test to be executed + + int abort_after; // stop tests after this many failed assertions + int subcase_filter_levels; // apply the subcase filters for the first N levels + + bool success; // include successful assertions in output + bool case_sensitive; // if filtering should be case sensitive + bool exit; // if the program should be exited after the tests are ran/whatever + bool duration; // print the time duration of each test case + bool minimal; // minimal console output (only test failures) + bool quiet; // no console output + bool no_throw; // to skip exceptions-related assertion macros + bool no_exitcode; // if the framework should return 0 as the exitcode + bool no_run; // to not run the tests at all (can be done with an "*" exclude) + bool no_intro; // to not print the intro of the framework + bool no_version; // to not print the version of the framework + bool no_colors; // if output to the console should be colorized + bool force_colors; // forces the use of colors even when a tty cannot be detected + bool no_breaks; // to not break into the debugger + bool no_skip; // don't skip test cases which are marked to be skipped + bool gnu_file_line; // if line numbers should be surrounded with :x: and not (x): + bool no_path_in_filenames; // if the path to files should be removed from the output + String strip_file_prefixes;// remove the longest matching one of these prefixes from any file paths in the output + bool no_line_numbers; // if source code line numbers should be omitted from the output + bool no_debug_output; // no output in the debug console when a debugger is attached + bool no_skipped_summary; // don't print "skipped" in the summary !!! UNDOCUMENTED !!! + bool no_time_in_output; // omit any time/timestamps from output !!! UNDOCUMENTED !!! + + bool help; // to print the help + bool version; // to print the version + bool count; // if only the count of matching tests is to be retrieved + bool list_test_cases; // to list all tests matching the filters + bool list_test_suites; // to list all suites matching the filters + bool list_reporters; // lists all registered reporters +}; + +namespace detail { + namespace types { +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + using namespace std; +#else + template + struct enable_if { }; + + template + struct enable_if { using type = T; }; + + struct true_type { static DOCTEST_CONSTEXPR bool value = true; }; + struct false_type { static DOCTEST_CONSTEXPR bool value = false; }; + + template struct remove_reference { using type = T; }; + template struct remove_reference { using type = T; }; + template struct remove_reference { using type = T; }; + + template struct is_rvalue_reference : false_type { }; + template struct is_rvalue_reference : true_type { }; + + template struct remove_const { using type = T; }; + template struct remove_const { using type = T; }; + + // Compiler intrinsics + template struct is_enum { static DOCTEST_CONSTEXPR bool value = __is_enum(T); }; + template struct underlying_type { using type = __underlying_type(T); }; + + template struct is_pointer : false_type { }; + template struct is_pointer : true_type { }; + + template struct is_array : false_type { }; + // NOLINTNEXTLINE(*-avoid-c-arrays) + template struct is_array : true_type { }; +#endif + } + + // + template + T&& declval(); + + template + DOCTEST_CONSTEXPR_FUNC T&& forward(typename types::remove_reference::type& t) DOCTEST_NOEXCEPT { + return static_cast(t); + } + + template + DOCTEST_CONSTEXPR_FUNC T&& forward(typename types::remove_reference::type&& t) DOCTEST_NOEXCEPT { + return static_cast(t); + } + + template + struct deferred_false : types::false_type { }; + +// MSVS 2015 :( +#if !DOCTEST_CLANG && defined(_MSC_VER) && _MSC_VER <= 1900 + template + struct has_global_insertion_operator : types::false_type { }; + + template + struct has_global_insertion_operator(), declval()), void())> : types::true_type { }; + + template + struct has_insertion_operator { static DOCTEST_CONSTEXPR bool value = has_global_insertion_operator::value; }; + + template + struct insert_hack; + + template + struct insert_hack { + static void insert(std::ostream& os, const T& t) { ::operator<<(os, t); } + }; + + template + struct insert_hack { + static void insert(std::ostream& os, const T& t) { operator<<(os, t); } + }; + + template + using insert_hack_t = insert_hack::value>; +#else + template + struct has_insertion_operator : types::false_type { }; +#endif + + template + struct has_insertion_operator(), declval()), void())> : types::true_type { }; + + template + struct should_stringify_as_underlying_type { + static DOCTEST_CONSTEXPR bool value = detail::types::is_enum::value && !doctest::detail::has_insertion_operator::value; + }; + + DOCTEST_INTERFACE std::ostream* tlssPush(); + DOCTEST_INTERFACE String tlssPop(); + + template + struct StringMakerBase { + template + static String convert(const DOCTEST_REF_WRAP(T)) { +#ifdef DOCTEST_CONFIG_REQUIRE_STRINGIFICATION_FOR_ALL_USED_TYPES + static_assert(deferred_false::value, "No stringification detected for type T. See string conversion manual"); +#endif + return "{?}"; + } + }; + + template + struct filldata; + + template + void filloss(std::ostream* stream, const T& in) { + filldata::fill(stream, in); + } + + template + void filloss(std::ostream* stream, const T (&in)[N]) { // NOLINT(*-avoid-c-arrays) + // T[N], T(&)[N], T(&&)[N] have same behaviour. + // Hence remove reference. + filloss::type>(stream, in); + } + + template + String toStream(const T& in) { + std::ostream* stream = tlssPush(); + filloss(stream, in); + return tlssPop(); + } + + template <> + struct StringMakerBase { + template + static String convert(const DOCTEST_REF_WRAP(T) in) { + return toStream(in); + } + }; +} // namespace detail + +template +struct StringMaker : public detail::StringMakerBase< + detail::has_insertion_operator::value || detail::types::is_pointer::value || detail::types::is_array::value> +{}; + +#ifndef DOCTEST_STRINGIFY +#ifdef DOCTEST_CONFIG_DOUBLE_STRINGIFY +#define DOCTEST_STRINGIFY(...) toString(toString(__VA_ARGS__)) +#else +#define DOCTEST_STRINGIFY(...) toString(__VA_ARGS__) +#endif +#endif + +template +String toString() { +#if DOCTEST_CLANG == 0 && DOCTEST_GCC == 0 && DOCTEST_ICC == 0 + String ret = __FUNCSIG__; // class doctest::String __cdecl doctest::toString(void) + String::size_type beginPos = ret.find('<'); + return ret.substr(beginPos + 1, ret.size() - beginPos - static_cast(sizeof(">(void)"))); +#else + String ret = __PRETTY_FUNCTION__; // doctest::String toString() [with T = TYPE] + String::size_type begin = ret.find('=') + 2; + return ret.substr(begin, ret.size() - begin - 1); +#endif +} + +template ::value, bool>::type = true> +String toString(const DOCTEST_REF_WRAP(T) value) { + return StringMaker::convert(value); +} + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +DOCTEST_INTERFACE String toString(const char* in); +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 +DOCTEST_INTERFACE String toString(const std::string& in); +#endif // VS 2019 + +DOCTEST_INTERFACE String toString(String in); + +DOCTEST_INTERFACE String toString(std::nullptr_t); + +DOCTEST_INTERFACE String toString(bool in); + +DOCTEST_INTERFACE String toString(float in); +DOCTEST_INTERFACE String toString(double in); +DOCTEST_INTERFACE String toString(double long in); + +DOCTEST_INTERFACE String toString(char in); +DOCTEST_INTERFACE String toString(char signed in); +DOCTEST_INTERFACE String toString(char unsigned in); +DOCTEST_INTERFACE String toString(short in); +DOCTEST_INTERFACE String toString(short unsigned in); +DOCTEST_INTERFACE String toString(signed in); +DOCTEST_INTERFACE String toString(unsigned in); +DOCTEST_INTERFACE String toString(long in); +DOCTEST_INTERFACE String toString(long unsigned in); +DOCTEST_INTERFACE String toString(long long in); +DOCTEST_INTERFACE String toString(long long unsigned in); + +template ::value, bool>::type = true> +String toString(const DOCTEST_REF_WRAP(T) value) { + using UT = typename detail::types::underlying_type::type; + return (DOCTEST_STRINGIFY(static_cast(value))); +} + +namespace detail { + template + struct filldata + { + static void fill(std::ostream* stream, const T& in) { +#if defined(_MSC_VER) && _MSC_VER <= 1900 + insert_hack_t::insert(*stream, in); +#else + operator<<(*stream, in); +#endif + } + }; + +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4866) +// NOLINTBEGIN(*-avoid-c-arrays) + template + struct filldata { + static void fill(std::ostream* stream, const T(&in)[N]) { + *stream << "["; + for (size_t i = 0; i < N; i++) { + if (i != 0) { *stream << ", "; } + *stream << (DOCTEST_STRINGIFY(in[i])); + } + *stream << "]"; + } + }; +// NOLINTEND(*-avoid-c-arrays) +DOCTEST_MSVC_SUPPRESS_WARNING_POP + + // Specialized since we don't want the terminating null byte! +// NOLINTBEGIN(*-avoid-c-arrays) + template + struct filldata { + static void fill(std::ostream* stream, const char (&in)[N]) { + *stream << String(in, in[N - 1] ? N : N - 1); + } // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + }; +// NOLINTEND(*-avoid-c-arrays) + + template <> + struct filldata { + static void fill(std::ostream* stream, const void* in); + }; + + template + struct filldata { +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4180) + static void fill(std::ostream* stream, const T* in) { +DOCTEST_MSVC_SUPPRESS_WARNING_POP +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wmicrosoft-cast") + filldata::fill(stream, +#if DOCTEST_GCC == 0 || DOCTEST_GCC >= DOCTEST_COMPILER(4, 9, 0) + reinterpret_cast(in) +#else + *reinterpret_cast(&in) +#endif + ); +DOCTEST_CLANG_SUPPRESS_WARNING_POP + } + }; +} + +struct DOCTEST_INTERFACE Approx +{ + Approx(double value); + + Approx operator()(double value) const; + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + explicit Approx(const T& value, + typename detail::types::enable_if::value>::type* = + static_cast(nullptr)) { + *this = static_cast(value); + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + Approx& epsilon(double newEpsilon); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + typename std::enable_if::value, Approx&>::type epsilon( + const T& newEpsilon) { + m_epsilon = static_cast(newEpsilon); + return *this; + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + Approx& scale(double newScale); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + typename std::enable_if::value, Approx&>::type scale( + const T& newScale) { + m_scale = static_cast(newScale); + return *this; + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + // clang-format off + DOCTEST_INTERFACE friend bool operator==(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator==(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator!=(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator!=(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator<=(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator<=(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator>=(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator>=(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator< (double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator< (const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator> (double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator> (const Approx & lhs, double rhs); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#define DOCTEST_APPROX_PREFIX \ + template friend typename std::enable_if::value, bool>::type + + DOCTEST_APPROX_PREFIX operator==(const T& lhs, const Approx& rhs) { return operator==(static_cast(lhs), rhs); } + DOCTEST_APPROX_PREFIX operator==(const Approx& lhs, const T& rhs) { return operator==(rhs, lhs); } + DOCTEST_APPROX_PREFIX operator!=(const T& lhs, const Approx& rhs) { return !operator==(lhs, rhs); } + DOCTEST_APPROX_PREFIX operator!=(const Approx& lhs, const T& rhs) { return !operator==(rhs, lhs); } + DOCTEST_APPROX_PREFIX operator<=(const T& lhs, const Approx& rhs) { return static_cast(lhs) < rhs.m_value || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator<=(const Approx& lhs, const T& rhs) { return lhs.m_value < static_cast(rhs) || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator>=(const T& lhs, const Approx& rhs) { return static_cast(lhs) > rhs.m_value || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator>=(const Approx& lhs, const T& rhs) { return lhs.m_value > static_cast(rhs) || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator< (const T& lhs, const Approx& rhs) { return static_cast(lhs) < rhs.m_value && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator< (const Approx& lhs, const T& rhs) { return lhs.m_value < static_cast(rhs) && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator> (const T& lhs, const Approx& rhs) { return static_cast(lhs) > rhs.m_value && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator> (const Approx& lhs, const T& rhs) { return lhs.m_value > static_cast(rhs) && lhs != rhs; } +#undef DOCTEST_APPROX_PREFIX +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + // clang-format on + + double m_epsilon; + double m_scale; + double m_value; +}; + +DOCTEST_INTERFACE String toString(const Approx& in); + +DOCTEST_INTERFACE const ContextOptions* getContextOptions(); + +template +struct DOCTEST_INTERFACE_DECL IsNaN +{ + F value; bool flipped; + IsNaN(F f, bool flip = false) : value(f), flipped(flip) { } + IsNaN operator!() const { return { value, !flipped }; } + operator bool() const; +}; +#ifndef __MINGW32__ +extern template struct DOCTEST_INTERFACE_DECL IsNaN; +extern template struct DOCTEST_INTERFACE_DECL IsNaN; +extern template struct DOCTEST_INTERFACE_DECL IsNaN; +#endif +DOCTEST_INTERFACE String toString(IsNaN in); +DOCTEST_INTERFACE String toString(IsNaN in); +DOCTEST_INTERFACE String toString(IsNaN in); + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace detail { + // clang-format off +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + template struct decay_array { using type = T; }; + template struct decay_array { using type = T*; }; + template struct decay_array { using type = T*; }; + + template struct not_char_pointer { static DOCTEST_CONSTEXPR int value = 1; }; + template<> struct not_char_pointer { static DOCTEST_CONSTEXPR int value = 0; }; + template<> struct not_char_pointer { static DOCTEST_CONSTEXPR int value = 0; }; + + template struct can_use_op : public not_char_pointer::type> {}; +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + // clang-format on + + struct DOCTEST_INTERFACE TestFailureException + { + }; + + DOCTEST_INTERFACE bool checkIfShouldThrow(assertType::Enum at); + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + DOCTEST_NORETURN +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + DOCTEST_INTERFACE void throwException(); + + struct DOCTEST_INTERFACE Subcase + { + SubcaseSignature m_signature; + bool m_entered = false; + + Subcase(const String& name, const char* file, int line); + Subcase(const Subcase&) = delete; + Subcase(Subcase&&) = delete; + Subcase& operator=(const Subcase&) = delete; + Subcase& operator=(Subcase&&) = delete; + ~Subcase(); + + operator bool() const; + + private: + bool checkFilters(); + }; + + template + String stringifyBinaryExpr(const DOCTEST_REF_WRAP(L) lhs, const char* op, + const DOCTEST_REF_WRAP(R) rhs) { + return (DOCTEST_STRINGIFY(lhs)) + op + (DOCTEST_STRINGIFY(rhs)); + } + +#if DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 6, 0) +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-comparison") +#endif + +// This will check if there is any way it could find a operator like member or friend and uses it. +// If not it doesn't find the operator or if the operator at global scope is defined after +// this template, the template won't be instantiated due to SFINAE. Once the template is not +// instantiated it can look for global operator using normal conversions. +#ifdef __NVCC__ +#define SFINAE_OP(ret,op) ret +#else +#define SFINAE_OP(ret,op) decltype((void)(doctest::detail::declval() op doctest::detail::declval()),ret{}) +#endif + +#define DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(op, op_str, op_macro) \ + template \ + DOCTEST_NOINLINE SFINAE_OP(Result,op) operator op(R&& rhs) { \ + bool res = op_macro(doctest::detail::forward(lhs), doctest::detail::forward(rhs)); \ + if(m_at & assertType::is_false) \ + res = !res; \ + if(!res || doctest::getContextOptions()->success) \ + return Result(res, stringifyBinaryExpr(lhs, op_str, rhs)); \ + return Result(res); \ + } + + // more checks could be added - like in Catch: + // https://github.com/catchorg/Catch2/pull/1480/files + // https://github.com/catchorg/Catch2/pull/1481/files +#define DOCTEST_FORBIT_EXPRESSION(rt, op) \ + template \ + rt& operator op(const R&) { \ + static_assert(deferred_false::value, \ + "Expression Too Complex Please Rewrite As Binary Comparison!"); \ + return *this; \ + } + + struct DOCTEST_INTERFACE Result // NOLINT(*-member-init) + { + bool m_passed; + String m_decomp; + + Result() = default; // TODO: Why do we need this? (To remove NOLINT) + Result(bool passed, const String& decomposition = String()); + + // forbidding some expressions based on this table: https://en.cppreference.com/w/cpp/language/operator_precedence + DOCTEST_FORBIT_EXPRESSION(Result, &) + DOCTEST_FORBIT_EXPRESSION(Result, ^) + DOCTEST_FORBIT_EXPRESSION(Result, |) + DOCTEST_FORBIT_EXPRESSION(Result, &&) + DOCTEST_FORBIT_EXPRESSION(Result, ||) + DOCTEST_FORBIT_EXPRESSION(Result, ==) + DOCTEST_FORBIT_EXPRESSION(Result, !=) + DOCTEST_FORBIT_EXPRESSION(Result, <) + DOCTEST_FORBIT_EXPRESSION(Result, >) + DOCTEST_FORBIT_EXPRESSION(Result, <=) + DOCTEST_FORBIT_EXPRESSION(Result, >=) + DOCTEST_FORBIT_EXPRESSION(Result, =) + DOCTEST_FORBIT_EXPRESSION(Result, +=) + DOCTEST_FORBIT_EXPRESSION(Result, -=) + DOCTEST_FORBIT_EXPRESSION(Result, *=) + DOCTEST_FORBIT_EXPRESSION(Result, /=) + DOCTEST_FORBIT_EXPRESSION(Result, %=) + DOCTEST_FORBIT_EXPRESSION(Result, <<=) + DOCTEST_FORBIT_EXPRESSION(Result, >>=) + DOCTEST_FORBIT_EXPRESSION(Result, &=) + DOCTEST_FORBIT_EXPRESSION(Result, ^=) + DOCTEST_FORBIT_EXPRESSION(Result, |=) + }; + +#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH + DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion") + DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-compare") + //DOCTEST_CLANG_SUPPRESS_WARNING("-Wdouble-promotion") + //DOCTEST_CLANG_SUPPRESS_WARNING("-Wconversion") + //DOCTEST_CLANG_SUPPRESS_WARNING("-Wfloat-equal") + + DOCTEST_GCC_SUPPRESS_WARNING_PUSH + DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion") + DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-compare") + //DOCTEST_GCC_SUPPRESS_WARNING("-Wdouble-promotion") + //DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion") + //DOCTEST_GCC_SUPPRESS_WARNING("-Wfloat-equal") + + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH + // https://stackoverflow.com/questions/39479163 what's the difference between 4018 and 4389 + DOCTEST_MSVC_SUPPRESS_WARNING(4388) // signed/unsigned mismatch + DOCTEST_MSVC_SUPPRESS_WARNING(4389) // 'operator' : signed/unsigned mismatch + DOCTEST_MSVC_SUPPRESS_WARNING(4018) // 'expression' : signed/unsigned mismatch + //DOCTEST_MSVC_SUPPRESS_WARNING(4805) // 'operation' : unsafe mix of type 'type' and type 'type' in operation + +#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + + // clang-format off +#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_COMPARISON_RETURN_TYPE bool +#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_COMPARISON_RETURN_TYPE typename types::enable_if::value || can_use_op::value, bool>::type + inline bool eq(const char* lhs, const char* rhs) { return String(lhs) == String(rhs); } + inline bool ne(const char* lhs, const char* rhs) { return String(lhs) != String(rhs); } + inline bool lt(const char* lhs, const char* rhs) { return String(lhs) < String(rhs); } + inline bool gt(const char* lhs, const char* rhs) { return String(lhs) > String(rhs); } + inline bool le(const char* lhs, const char* rhs) { return String(lhs) <= String(rhs); } + inline bool ge(const char* lhs, const char* rhs) { return String(lhs) >= String(rhs); } +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + // clang-format on + +#define DOCTEST_RELATIONAL_OP(name, op) \ + template \ + DOCTEST_COMPARISON_RETURN_TYPE name(const DOCTEST_REF_WRAP(L) lhs, \ + const DOCTEST_REF_WRAP(R) rhs) { \ + return lhs op rhs; \ + } + + DOCTEST_RELATIONAL_OP(eq, ==) + DOCTEST_RELATIONAL_OP(ne, !=) + DOCTEST_RELATIONAL_OP(lt, <) + DOCTEST_RELATIONAL_OP(gt, >) + DOCTEST_RELATIONAL_OP(le, <=) + DOCTEST_RELATIONAL_OP(ge, >=) + +#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_CMP_EQ(l, r) l == r +#define DOCTEST_CMP_NE(l, r) l != r +#define DOCTEST_CMP_GT(l, r) l > r +#define DOCTEST_CMP_LT(l, r) l < r +#define DOCTEST_CMP_GE(l, r) l >= r +#define DOCTEST_CMP_LE(l, r) l <= r +#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_CMP_EQ(l, r) eq(l, r) +#define DOCTEST_CMP_NE(l, r) ne(l, r) +#define DOCTEST_CMP_GT(l, r) gt(l, r) +#define DOCTEST_CMP_LT(l, r) lt(l, r) +#define DOCTEST_CMP_GE(l, r) ge(l, r) +#define DOCTEST_CMP_LE(l, r) le(l, r) +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + + template + // cppcheck-suppress copyCtorAndEqOperator + struct Expression_lhs + { + L lhs; + assertType::Enum m_at; + + explicit Expression_lhs(L&& in, assertType::Enum at) + : lhs(static_cast(in)) + , m_at(at) {} + + DOCTEST_NOINLINE operator Result() { +// this is needed only for MSVC 2015 +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4800) // 'int': forcing value to bool + bool res = static_cast(lhs); +DOCTEST_MSVC_SUPPRESS_WARNING_POP + if(m_at & assertType::is_false) { //!OCLINT bitwise operator in conditional + res = !res; + } + + if(!res || getContextOptions()->success) { + return { res, (DOCTEST_STRINGIFY(lhs)) }; + } + return { res }; + } + + /* This is required for user-defined conversions from Expression_lhs to L */ + operator L() const { return lhs; } + + // clang-format off + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(==, " == ", DOCTEST_CMP_EQ) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(!=, " != ", DOCTEST_CMP_NE) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>, " > ", DOCTEST_CMP_GT) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<, " < ", DOCTEST_CMP_LT) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>=, " >= ", DOCTEST_CMP_GE) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<=, " <= ", DOCTEST_CMP_LE) //!OCLINT bitwise operator in conditional + // clang-format on + + // forbidding some expressions based on this table: https://en.cppreference.com/w/cpp/language/operator_precedence + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &&) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ||) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, =) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, +=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, -=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, *=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, /=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, %=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |=) + // these 2 are unfortunate because they should be allowed - they have higher precedence over the comparisons, but the + // ExpressionDecomposer class uses the left shift operator to capture the left operand of the binary expression... + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>) + }; + +#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + + DOCTEST_CLANG_SUPPRESS_WARNING_POP + DOCTEST_MSVC_SUPPRESS_WARNING_POP + DOCTEST_GCC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + +#if DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 6, 0) +DOCTEST_CLANG_SUPPRESS_WARNING_POP +#endif + + struct DOCTEST_INTERFACE ExpressionDecomposer + { + assertType::Enum m_at; + + ExpressionDecomposer(assertType::Enum at); + + // The right operator for capturing expressions is "<=" instead of "<<" (based on the operator precedence table) + // but then there will be warnings from GCC about "-Wparentheses" and since "_Pragma()" is problematic this will stay for now... + // https://github.com/catchorg/Catch2/issues/870 + // https://github.com/catchorg/Catch2/issues/565 + template + Expression_lhs operator<<(const L&& operand) { //bitfields bind to universal ref but not const rvalue ref + return Expression_lhs(static_cast(operand), m_at); + } + + template ::value,void >::type* = nullptr> + Expression_lhs operator<<(const L &operand) { + return Expression_lhs(operand, m_at); + } + }; + + struct DOCTEST_INTERFACE TestSuite + { + const char* m_test_suite = nullptr; + const char* m_description = nullptr; + bool m_skip = false; + bool m_no_breaks = false; + bool m_no_output = false; + bool m_may_fail = false; + bool m_should_fail = false; + int m_expected_failures = 0; + double m_timeout = 0; + + TestSuite& operator*(const char* in); + + template + TestSuite& operator*(const T& in) { + in.fill(*this); + return *this; + } + }; + + using funcType = void (*)(); + + struct DOCTEST_INTERFACE TestCase : public TestCaseData + { + funcType m_test; // a function pointer to the test case + + String m_type; // for templated test cases - gets appended to the real name + int m_template_id; // an ID used to distinguish between the different versions of a templated test case + String m_full_name; // contains the name (only for templated test cases!) + the template type + + TestCase(funcType test, const char* file, unsigned line, const TestSuite& test_suite, + const String& type = String(), int template_id = -1); + + TestCase(const TestCase& other); + TestCase(TestCase&&) = delete; + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function + TestCase& operator=(const TestCase& other); + DOCTEST_MSVC_SUPPRESS_WARNING_POP + + TestCase& operator=(TestCase&&) = delete; + + TestCase& operator*(const char* in); + + template + TestCase& operator*(const T& in) { + in.fill(*this); + return *this; + } + + bool operator<(const TestCase& other) const; + + ~TestCase() = default; + }; + + // forward declarations of functions used by the macros + DOCTEST_INTERFACE int regTest(const TestCase& tc); + DOCTEST_INTERFACE int setTestSuite(const TestSuite& ts); + DOCTEST_INTERFACE bool isDebuggerActive(); + + template + int instantiationHelper(const T&) { return 0; } + + namespace binaryAssertComparison { + enum Enum + { + eq = 0, + ne, + gt, + lt, + ge, + le + }; + } // namespace binaryAssertComparison + + // clang-format off + template struct RelationalComparator { bool operator()(const DOCTEST_REF_WRAP(L), const DOCTEST_REF_WRAP(R) ) const { return false; } }; + +#define DOCTEST_BINARY_RELATIONAL_OP(n, op) \ + template struct RelationalComparator { bool operator()(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) const { return op(lhs, rhs); } }; + // clang-format on + + DOCTEST_BINARY_RELATIONAL_OP(0, doctest::detail::eq) + DOCTEST_BINARY_RELATIONAL_OP(1, doctest::detail::ne) + DOCTEST_BINARY_RELATIONAL_OP(2, doctest::detail::gt) + DOCTEST_BINARY_RELATIONAL_OP(3, doctest::detail::lt) + DOCTEST_BINARY_RELATIONAL_OP(4, doctest::detail::ge) + DOCTEST_BINARY_RELATIONAL_OP(5, doctest::detail::le) + + struct DOCTEST_INTERFACE ResultBuilder : public AssertData + { + ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type = "", const String& exception_string = ""); + + ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type, const Contains& exception_string); + + void setResult(const Result& res); + + template + DOCTEST_NOINLINE bool binary_assert(const DOCTEST_REF_WRAP(L) lhs, + const DOCTEST_REF_WRAP(R) rhs) { + m_failed = !RelationalComparator()(lhs, rhs); + if (m_failed || getContextOptions()->success) { + m_decomp = stringifyBinaryExpr(lhs, ", ", rhs); + } + return !m_failed; + } + + template + DOCTEST_NOINLINE bool unary_assert(const DOCTEST_REF_WRAP(L) val) { + m_failed = !val; + + if (m_at & assertType::is_false) { //!OCLINT bitwise operator in conditional + m_failed = !m_failed; + } + + if (m_failed || getContextOptions()->success) { + m_decomp = (DOCTEST_STRINGIFY(val)); + } + + return !m_failed; + } + + void translateException(); + + bool log(); + void react() const; + }; + + namespace assertAction { + enum Enum + { + nothing = 0, + dbgbreak = 1, + shouldthrow = 2 + }; + } // namespace assertAction + + DOCTEST_INTERFACE void failed_out_of_a_testing_context(const AssertData& ad); + + DOCTEST_INTERFACE bool decomp_assert(assertType::Enum at, const char* file, int line, + const char* expr, const Result& result); + +#define DOCTEST_ASSERT_OUT_OF_TESTS(decomp) \ + do { \ + if(!is_running_in_test) { \ + if(failed) { \ + ResultBuilder rb(at, file, line, expr); \ + rb.m_failed = failed; \ + rb.m_decomp = decomp; \ + failed_out_of_a_testing_context(rb); \ + if(isDebuggerActive() && !getContextOptions()->no_breaks) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + if(checkIfShouldThrow(at)) \ + throwException(); \ + } \ + return !failed; \ + } \ + } while(false) + +#define DOCTEST_ASSERT_IN_TESTS(decomp) \ + ResultBuilder rb(at, file, line, expr); \ + rb.m_failed = failed; \ + if(rb.m_failed || getContextOptions()->success) \ + rb.m_decomp = decomp; \ + if(rb.log()) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + if(rb.m_failed && checkIfShouldThrow(at)) \ + throwException() + + template + DOCTEST_NOINLINE bool binary_assert(assertType::Enum at, const char* file, int line, + const char* expr, const DOCTEST_REF_WRAP(L) lhs, + const DOCTEST_REF_WRAP(R) rhs) { + bool failed = !RelationalComparator()(lhs, rhs); + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS(stringifyBinaryExpr(lhs, ", ", rhs)); + DOCTEST_ASSERT_IN_TESTS(stringifyBinaryExpr(lhs, ", ", rhs)); + return !failed; + } + + template + DOCTEST_NOINLINE bool unary_assert(assertType::Enum at, const char* file, int line, + const char* expr, const DOCTEST_REF_WRAP(L) val) { + bool failed = !val; + + if(at & assertType::is_false) //!OCLINT bitwise operator in conditional + failed = !failed; + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS((DOCTEST_STRINGIFY(val))); + DOCTEST_ASSERT_IN_TESTS((DOCTEST_STRINGIFY(val))); + return !failed; + } + + struct DOCTEST_INTERFACE IExceptionTranslator + { + DOCTEST_DECLARE_INTERFACE(IExceptionTranslator) + virtual bool translate(String&) const = 0; + }; + + template + class ExceptionTranslator : public IExceptionTranslator //!OCLINT destructor of virtual class + { + public: + explicit ExceptionTranslator(String (*translateFunction)(T)) + : m_translateFunction(translateFunction) {} + + bool translate(String& res) const override { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + try { + throw; // lgtm [cpp/rethrow-no-exception] + // cppcheck-suppress catchExceptionByValue + } catch(const T& ex) { + res = m_translateFunction(ex); //!OCLINT parameter reassignment + return true; + } catch(...) {} //!OCLINT - empty catch statement +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + static_cast(res); // to silence -Wunused-parameter + return false; + } + + private: + String (*m_translateFunction)(T); + }; + + DOCTEST_INTERFACE void registerExceptionTranslatorImpl(const IExceptionTranslator* et); + + // ContextScope base class used to allow implementing methods of ContextScope + // that don't depend on the template parameter in doctest.cpp. + struct DOCTEST_INTERFACE ContextScopeBase : public IContextScope { + ContextScopeBase(const ContextScopeBase&) = delete; + + ContextScopeBase& operator=(const ContextScopeBase&) = delete; + ContextScopeBase& operator=(ContextScopeBase&&) = delete; + + ~ContextScopeBase() override = default; + + protected: + ContextScopeBase(); + ContextScopeBase(ContextScopeBase&& other) noexcept; + + void destroy(); + bool need_to_destroy{true}; + }; + + template class ContextScope : public ContextScopeBase + { + L lambda_; + + public: + explicit ContextScope(const L &lambda) : lambda_(lambda) {} + explicit ContextScope(L&& lambda) : lambda_(static_cast(lambda)) { } + + ContextScope(const ContextScope&) = delete; + ContextScope(ContextScope&&) noexcept = default; + + ContextScope& operator=(const ContextScope&) = delete; + ContextScope& operator=(ContextScope&&) = delete; + + void stringify(std::ostream* s) const override { lambda_(s); } + + ~ContextScope() override { + if (need_to_destroy) { + destroy(); + } + } + }; + + struct DOCTEST_INTERFACE MessageBuilder : public MessageData + { + std::ostream* m_stream; + bool logged = false; + + MessageBuilder(const char* file, int line, assertType::Enum severity); + + MessageBuilder(const MessageBuilder&) = delete; + MessageBuilder(MessageBuilder&&) = delete; + + MessageBuilder& operator=(const MessageBuilder&) = delete; + MessageBuilder& operator=(MessageBuilder&&) = delete; + + ~MessageBuilder(); + + // the preferred way of chaining parameters for stringification +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4866) + template + MessageBuilder& operator,(const T& in) { + *m_stream << (DOCTEST_STRINGIFY(in)); + return *this; + } +DOCTEST_MSVC_SUPPRESS_WARNING_POP + + // kept here just for backwards-compatibility - the comma operator should be preferred now + template + MessageBuilder& operator<<(const T& in) { return this->operator,(in); } + + // the `,` operator has the lowest operator precedence - if `<<` is used by the user then + // the `,` operator will be called last which is not what we want and thus the `*` operator + // is used first (has higher operator precedence compared to `<<`) so that we guarantee that + // an operator of the MessageBuilder class is called first before the rest of the parameters + template + MessageBuilder& operator*(const T& in) { return this->operator,(in); } + + bool log(); + void react(); + }; + + template + ContextScope MakeContextScope(const L &lambda) { + return ContextScope(lambda); + } +} // namespace detail + +#define DOCTEST_DEFINE_DECORATOR(name, type, def) \ + struct name \ + { \ + type data; \ + name(type in = def) \ + : data(in) {} \ + void fill(detail::TestCase& state) const { state.DOCTEST_CAT(m_, name) = data; } \ + void fill(detail::TestSuite& state) const { state.DOCTEST_CAT(m_, name) = data; } \ + } + +DOCTEST_DEFINE_DECORATOR(test_suite, const char*, ""); +DOCTEST_DEFINE_DECORATOR(description, const char*, ""); +DOCTEST_DEFINE_DECORATOR(skip, bool, true); +DOCTEST_DEFINE_DECORATOR(no_breaks, bool, true); +DOCTEST_DEFINE_DECORATOR(no_output, bool, true); +DOCTEST_DEFINE_DECORATOR(timeout, double, 0); +DOCTEST_DEFINE_DECORATOR(may_fail, bool, true); +DOCTEST_DEFINE_DECORATOR(should_fail, bool, true); +DOCTEST_DEFINE_DECORATOR(expected_failures, int, 0); + +template +int registerExceptionTranslator(String (*translateFunction)(T)) { + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors") + static detail::ExceptionTranslator exceptionTranslator(translateFunction); + DOCTEST_CLANG_SUPPRESS_WARNING_POP + detail::registerExceptionTranslatorImpl(&exceptionTranslator); + return 0; +} + +} // namespace doctest + +// in a separate namespace outside of doctest because the DOCTEST_TEST_SUITE macro +// introduces an anonymous namespace in which getCurrentTestSuite gets overridden +namespace doctest_detail_test_suite_ns { +DOCTEST_INTERFACE doctest::detail::TestSuite& getCurrentTestSuite(); +} // namespace doctest_detail_test_suite_ns + +namespace doctest { +#else // DOCTEST_CONFIG_DISABLE +template +int registerExceptionTranslator(String (*)(T)) { + return 0; +} +#endif // DOCTEST_CONFIG_DISABLE + +namespace detail { + using assert_handler = void (*)(const AssertData&); + struct ContextState; +} // namespace detail + +class DOCTEST_INTERFACE Context +{ + detail::ContextState* p; + + void parseArgs(int argc, const char* const* argv, bool withDefaults = false); + +public: + explicit Context(int argc = 0, const char* const* argv = nullptr); + + Context(const Context&) = delete; + Context(Context&&) = delete; + + Context& operator=(const Context&) = delete; + Context& operator=(Context&&) = delete; + + ~Context(); // NOLINT(performance-trivially-destructible) + + void applyCommandLine(int argc, const char* const* argv); + + void addFilter(const char* filter, const char* value); + void clearFilters(); + void setOption(const char* option, bool value); + void setOption(const char* option, int value); + void setOption(const char* option, const char* value); + + bool shouldExit(); + + void setAsDefaultForAssertsOutOfTestCases(); + + void setAssertHandler(detail::assert_handler ah); + + void setCout(std::ostream* out); + + int run(); +}; + +namespace TestCaseFailureReason { + enum Enum + { + None = 0, + AssertFailure = 1, // an assertion has failed in the test case + Exception = 2, // test case threw an exception + Crash = 4, // a crash... + TooManyFailedAsserts = 8, // the abort-after option + Timeout = 16, // see the timeout decorator + ShouldHaveFailedButDidnt = 32, // see the should_fail decorator + ShouldHaveFailedAndDid = 64, // see the should_fail decorator + DidntFailExactlyNumTimes = 128, // see the expected_failures decorator + FailedExactlyNumTimes = 256, // see the expected_failures decorator + CouldHaveFailedAndDid = 512 // see the may_fail decorator + }; +} // namespace TestCaseFailureReason + +struct DOCTEST_INTERFACE CurrentTestCaseStats +{ + int numAssertsCurrentTest; + int numAssertsFailedCurrentTest; + double seconds; + int failure_flags; // use TestCaseFailureReason::Enum + bool testCaseSuccess; +}; + +struct DOCTEST_INTERFACE TestCaseException +{ + String error_string; + bool is_crash; +}; + +struct DOCTEST_INTERFACE TestRunStats +{ + unsigned numTestCases; + unsigned numTestCasesPassingFilters; + unsigned numTestSuitesPassingFilters; + unsigned numTestCasesFailed; + int numAsserts; + int numAssertsFailed; +}; + +struct QueryData +{ + const TestRunStats* run_stats = nullptr; + const TestCaseData** data = nullptr; + unsigned num_data = 0; +}; + +struct DOCTEST_INTERFACE IReporter +{ + // The constructor has to accept "const ContextOptions&" as a single argument + // which has most of the options for the run + a pointer to the stdout stream + // Reporter(const ContextOptions& in) + + // called when a query should be reported (listing test cases, printing the version, etc.) + virtual void report_query(const QueryData&) = 0; + + // called when the whole test run starts + virtual void test_run_start() = 0; + // called when the whole test run ends (caching a pointer to the input doesn't make sense here) + virtual void test_run_end(const TestRunStats&) = 0; + + // called when a test case is started (safe to cache a pointer to the input) + virtual void test_case_start(const TestCaseData&) = 0; + // called when a test case is reentered because of unfinished subcases (safe to cache a pointer to the input) + virtual void test_case_reenter(const TestCaseData&) = 0; + // called when a test case has ended + virtual void test_case_end(const CurrentTestCaseStats&) = 0; + + // called when an exception is thrown from the test case (or it crashes) + virtual void test_case_exception(const TestCaseException&) = 0; + + // called whenever a subcase is entered (don't cache pointers to the input) + virtual void subcase_start(const SubcaseSignature&) = 0; + // called whenever a subcase is exited (don't cache pointers to the input) + virtual void subcase_end() = 0; + + // called for each assert (don't cache pointers to the input) + virtual void log_assert(const AssertData&) = 0; + // called for each message (don't cache pointers to the input) + virtual void log_message(const MessageData&) = 0; + + // called when a test case is skipped either because it doesn't pass the filters, has a skip decorator + // or isn't in the execution range (between first and last) (safe to cache a pointer to the input) + virtual void test_case_skipped(const TestCaseData&) = 0; + + DOCTEST_DECLARE_INTERFACE(IReporter) + + // can obtain all currently active contexts and stringify them if one wishes to do so + static int get_num_active_contexts(); + static const IContextScope* const* get_active_contexts(); + + // can iterate through contexts which have been stringified automatically in their destructors when an exception has been thrown + static int get_num_stringified_contexts(); + static const String* get_stringified_contexts(); +}; + +namespace detail { + using reporterCreatorFunc = IReporter* (*)(const ContextOptions&); + + DOCTEST_INTERFACE void registerReporterImpl(const char* name, int prio, reporterCreatorFunc c, bool isReporter); + + template + IReporter* reporterCreator(const ContextOptions& o) { + return new Reporter(o); + } +} // namespace detail + +template +int registerReporter(const char* name, int priority, bool isReporter) { + detail::registerReporterImpl(name, priority, detail::reporterCreator, isReporter); + return 0; +} +} // namespace doctest + +#ifdef DOCTEST_CONFIG_ASSERTS_RETURN_VALUES +#define DOCTEST_FUNC_EMPTY [] { return false; }() +#else +#define DOCTEST_FUNC_EMPTY (void)0 +#endif + +// if registering is not disabled +#ifndef DOCTEST_CONFIG_DISABLE + +#ifdef DOCTEST_CONFIG_ASSERTS_RETURN_VALUES +#define DOCTEST_FUNC_SCOPE_BEGIN [&] +#define DOCTEST_FUNC_SCOPE_END () +#define DOCTEST_FUNC_SCOPE_RET(v) return v +#else +#define DOCTEST_FUNC_SCOPE_BEGIN do +#define DOCTEST_FUNC_SCOPE_END while(false) +#define DOCTEST_FUNC_SCOPE_RET(v) (void)0 +#endif + +// common code in asserts - for convenience +#define DOCTEST_ASSERT_LOG_REACT_RETURN(b) \ + if(b.log()) DOCTEST_BREAK_INTO_DEBUGGER(); \ + b.react(); \ + DOCTEST_FUNC_SCOPE_RET(!b.m_failed) + +#ifdef DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#define DOCTEST_WRAP_IN_TRY(x) x; +#else // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#define DOCTEST_WRAP_IN_TRY(x) \ + try { \ + x; \ + } catch(...) { DOCTEST_RB.translateException(); } +#endif // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS + +#ifdef DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS +#define DOCTEST_CAST_TO_VOID(...) \ + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wuseless-cast") \ + static_cast(__VA_ARGS__); \ + DOCTEST_GCC_SUPPRESS_WARNING_POP +#else // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS +#define DOCTEST_CAST_TO_VOID(...) __VA_ARGS__; +#endif // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS + +// registers the test by initializing a dummy var with a function +#define DOCTEST_REGISTER_FUNCTION(global_prefix, f, decorators) \ + global_prefix DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), /* NOLINT */ \ + doctest::detail::regTest( \ + doctest::detail::TestCase( \ + f, __FILE__, __LINE__, \ + doctest_detail_test_suite_ns::getCurrentTestSuite()) * \ + decorators)) + +#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, decorators) \ + namespace { /* NOLINT */ \ + struct der : public base \ + { \ + void f(); \ + }; \ + static DOCTEST_INLINE_NOINLINE void func() { \ + der v; \ + v.f(); \ + } \ + DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, func, decorators) \ + } \ + DOCTEST_INLINE_NOINLINE void der::f() // NOLINT(misc-definitions-in-headers) + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, decorators) \ + static void f(); \ + DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, f, decorators) \ + static void f() + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS(f, proxy, decorators) \ + static doctest::detail::funcType proxy() { return f; } \ + DOCTEST_REGISTER_FUNCTION(inline, proxy(), decorators) \ + static void f() + +// for registering tests +#define DOCTEST_TEST_CASE(decorators) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), decorators) + +// for registering tests in classes - requires C++17 for inline variables! +#if DOCTEST_CPLUSPLUS >= 201703L +#define DOCTEST_TEST_CASE_CLASS(decorators) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_PROXY_), \ + decorators) +#else // DOCTEST_TEST_CASE_CLASS +#define DOCTEST_TEST_CASE_CLASS(...) \ + TEST_CASES_CAN_BE_REGISTERED_IN_CLASSES_ONLY_IN_CPP17_MODE_OR_WITH_VS_2017_OR_NEWER +#endif // DOCTEST_TEST_CASE_CLASS + +// for registering tests with a fixture +#define DOCTEST_TEST_CASE_FIXTURE(c, decorators) \ + DOCTEST_IMPLEMENT_FIXTURE(DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), c, \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), decorators) + +// for converting types to strings without the header and demangling +#define DOCTEST_TYPE_TO_STRING_AS(str, ...) \ + namespace doctest { \ + template <> \ + inline String toString<__VA_ARGS__>() { \ + return str; \ + } \ + } \ + static_assert(true, "") + +#define DOCTEST_TYPE_TO_STRING(...) DOCTEST_TYPE_TO_STRING_AS(#__VA_ARGS__, __VA_ARGS__) + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, iter, func) \ + template \ + static void func(); \ + namespace { /* NOLINT */ \ + template \ + struct iter; \ + template \ + struct iter> \ + { \ + iter(const char* file, unsigned line, int index) { \ + doctest::detail::regTest(doctest::detail::TestCase(func, file, line, \ + doctest_detail_test_suite_ns::getCurrentTestSuite(), \ + doctest::toString(), \ + int(line) * 1000 + index) \ + * dec); \ + iter>(file, line, index + 1); \ + } \ + }; \ + template <> \ + struct iter> \ + { \ + iter(const char*, unsigned, int) {} \ + }; \ + } \ + template \ + static void func() + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(dec, T, id) \ + DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(id, ITERATOR), \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)) + +#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, anon, ...) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_CAT(anon, DUMMY), /* NOLINT(cert-err58-cpp, fuchsia-statically-constructed-objects) */ \ + doctest::detail::instantiationHelper( \ + DOCTEST_CAT(id, ITERATOR)<__VA_ARGS__>(__FILE__, __LINE__, 0))) + +#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), std::tuple<__VA_ARGS__>) \ + static_assert(true, "") + +#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), __VA_ARGS__) \ + static_assert(true, "") + +#define DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, anon, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(anon, ITERATOR), anon); \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(anon, anon, std::tuple<__VA_ARGS__>) \ + template \ + static void anon() + +#define DOCTEST_TEST_CASE_TEMPLATE(dec, T, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), __VA_ARGS__) + +// for subcases +#define DOCTEST_SUBCASE(name) \ + if(const doctest::detail::Subcase & DOCTEST_ANONYMOUS(DOCTEST_ANON_SUBCASE_) DOCTEST_UNUSED = \ + doctest::detail::Subcase(name, __FILE__, __LINE__)) + +// for grouping tests in test suites by using code blocks +#define DOCTEST_TEST_SUITE_IMPL(decorators, ns_name) \ + namespace ns_name { namespace doctest_detail_test_suite_ns { \ + static DOCTEST_NOINLINE doctest::detail::TestSuite& getCurrentTestSuite() noexcept { \ + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4640) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors") \ + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wmissing-field-initializers") \ + static doctest::detail::TestSuite data{}; \ + static bool inited = false; \ + DOCTEST_MSVC_SUPPRESS_WARNING_POP \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP \ + DOCTEST_GCC_SUPPRESS_WARNING_POP \ + if(!inited) { \ + data* decorators; \ + inited = true; \ + } \ + return data; \ + } \ + } \ + } \ + namespace ns_name + +#define DOCTEST_TEST_SUITE(decorators) \ + DOCTEST_TEST_SUITE_IMPL(decorators, DOCTEST_ANONYMOUS(DOCTEST_ANON_SUITE_)) + +// for starting a testsuite block +#define DOCTEST_TEST_SUITE_BEGIN(decorators) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), /* NOLINT(cert-err58-cpp) */ \ + doctest::detail::setTestSuite(doctest::detail::TestSuite() * decorators)) \ + static_assert(true, "") + +// for ending a testsuite block +#define DOCTEST_TEST_SUITE_END \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), /* NOLINT(cert-err58-cpp) */ \ + doctest::detail::setTestSuite(doctest::detail::TestSuite() * "")) \ + using DOCTEST_ANONYMOUS(DOCTEST_ANON_FOR_SEMICOLON_) = int + +// for registering exception translators +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(translatorName, signature) \ + inline doctest::String translatorName(signature); \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_), /* NOLINT(cert-err58-cpp) */ \ + doctest::registerExceptionTranslator(translatorName)) \ + doctest::String translatorName(signature) + +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \ + DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_), \ + signature) + +// for registering reporters +#define DOCTEST_REGISTER_REPORTER(name, priority, reporter) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_REPORTER_), /* NOLINT(cert-err58-cpp) */ \ + doctest::registerReporter(name, priority, true)) \ + static_assert(true, "") + +// for registering listeners +#define DOCTEST_REGISTER_LISTENER(name, priority, reporter) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_REPORTER_), /* NOLINT(cert-err58-cpp) */ \ + doctest::registerReporter(name, priority, false)) \ + static_assert(true, "") + +// clang-format off +// for logging - disabling formatting because it's important to have these on 2 separate lines - see PR #557 +#define DOCTEST_INFO(...) \ + DOCTEST_INFO_IMPL(DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_), \ + DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_OTHER_), \ + __VA_ARGS__) +// clang-format on + +#define DOCTEST_INFO_IMPL(mb_name, s_name, ...) \ + auto DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_) = doctest::detail::MakeContextScope( \ + [&](std::ostream* s_name) { \ + doctest::detail::MessageBuilder mb_name(__FILE__, __LINE__, doctest::assertType::is_warn); \ + mb_name.m_stream = s_name; \ + mb_name * __VA_ARGS__; \ + }) + +#define DOCTEST_CAPTURE(x) DOCTEST_INFO(#x " := ", x) + +#define DOCTEST_ADD_AT_IMPL(type, file, line, mb, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + doctest::detail::MessageBuilder mb(file, line, doctest::assertType::type); \ + mb * __VA_ARGS__; \ + if(mb.log()) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + mb.react(); \ + } DOCTEST_FUNC_SCOPE_END + +// clang-format off +#define DOCTEST_ADD_MESSAGE_AT(file, line, ...) DOCTEST_ADD_AT_IMPL(is_warn, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) +#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, ...) DOCTEST_ADD_AT_IMPL(is_check, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) +#define DOCTEST_ADD_FAIL_AT(file, line, ...) DOCTEST_ADD_AT_IMPL(is_require, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) +// clang-format on + +#define DOCTEST_MESSAGE(...) DOCTEST_ADD_MESSAGE_AT(__FILE__, __LINE__, __VA_ARGS__) +#define DOCTEST_FAIL_CHECK(...) DOCTEST_ADD_FAIL_CHECK_AT(__FILE__, __LINE__, __VA_ARGS__) +#define DOCTEST_FAIL(...) DOCTEST_ADD_FAIL_AT(__FILE__, __LINE__, __VA_ARGS__) + +#define DOCTEST_TO_LVALUE(...) __VA_ARGS__ // Not removed to keep backwards compatibility. + +#ifndef DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_ASSERT_IMPLEMENT_2(assert_type, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \ + /* NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) */ \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY(DOCTEST_RB.setResult( \ + doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) \ + << __VA_ARGS__)) /* NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) */ \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB) \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + DOCTEST_ASSERT_IMPLEMENT_2(assert_type, __VA_ARGS__); \ + } DOCTEST_FUNC_SCOPE_END // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + +#define DOCTEST_BINARY_ASSERT(assert_type, comp, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY( \ + DOCTEST_RB.binary_assert( \ + __VA_ARGS__)) \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } DOCTEST_FUNC_SCOPE_END + +#define DOCTEST_UNARY_ASSERT(assert_type, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY(DOCTEST_RB.unary_assert(__VA_ARGS__)) \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } DOCTEST_FUNC_SCOPE_END + +#else // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +// necessary for _MESSAGE +#define DOCTEST_ASSERT_IMPLEMENT_2 DOCTEST_ASSERT_IMPLEMENT_1 + +#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \ + doctest::detail::decomp_assert( \ + doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, \ + doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) \ + << __VA_ARGS__) DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#define DOCTEST_BINARY_ASSERT(assert_type, comparison, ...) \ + doctest::detail::binary_assert( \ + doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, __VA_ARGS__) + +#define DOCTEST_UNARY_ASSERT(assert_type, ...) \ + doctest::detail::unary_assert(doctest::assertType::assert_type, __FILE__, __LINE__, \ + #__VA_ARGS__, __VA_ARGS__) + +#endif // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_WARN(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN, __VA_ARGS__) +#define DOCTEST_CHECK(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK, __VA_ARGS__) +#define DOCTEST_REQUIRE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE, __VA_ARGS__) +#define DOCTEST_WARN_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN_FALSE, __VA_ARGS__) +#define DOCTEST_CHECK_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK_FALSE, __VA_ARGS__) +#define DOCTEST_REQUIRE_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE_FALSE, __VA_ARGS__) + +// clang-format off +#define DOCTEST_WARN_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN_FALSE, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK_FALSE, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE_FALSE, cond); } DOCTEST_FUNC_SCOPE_END +// clang-format on + +#define DOCTEST_WARN_EQ(...) DOCTEST_BINARY_ASSERT(DT_WARN_EQ, eq, __VA_ARGS__) +#define DOCTEST_CHECK_EQ(...) DOCTEST_BINARY_ASSERT(DT_CHECK_EQ, eq, __VA_ARGS__) +#define DOCTEST_REQUIRE_EQ(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_EQ, eq, __VA_ARGS__) +#define DOCTEST_WARN_NE(...) DOCTEST_BINARY_ASSERT(DT_WARN_NE, ne, __VA_ARGS__) +#define DOCTEST_CHECK_NE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_NE, ne, __VA_ARGS__) +#define DOCTEST_REQUIRE_NE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_NE, ne, __VA_ARGS__) +#define DOCTEST_WARN_GT(...) DOCTEST_BINARY_ASSERT(DT_WARN_GT, gt, __VA_ARGS__) +#define DOCTEST_CHECK_GT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GT, gt, __VA_ARGS__) +#define DOCTEST_REQUIRE_GT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GT, gt, __VA_ARGS__) +#define DOCTEST_WARN_LT(...) DOCTEST_BINARY_ASSERT(DT_WARN_LT, lt, __VA_ARGS__) +#define DOCTEST_CHECK_LT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LT, lt, __VA_ARGS__) +#define DOCTEST_REQUIRE_LT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LT, lt, __VA_ARGS__) +#define DOCTEST_WARN_GE(...) DOCTEST_BINARY_ASSERT(DT_WARN_GE, ge, __VA_ARGS__) +#define DOCTEST_CHECK_GE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GE, ge, __VA_ARGS__) +#define DOCTEST_REQUIRE_GE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GE, ge, __VA_ARGS__) +#define DOCTEST_WARN_LE(...) DOCTEST_BINARY_ASSERT(DT_WARN_LE, le, __VA_ARGS__) +#define DOCTEST_CHECK_LE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LE, le, __VA_ARGS__) +#define DOCTEST_REQUIRE_LE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LE, le, __VA_ARGS__) + +#define DOCTEST_WARN_UNARY(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY, __VA_ARGS__) +#define DOCTEST_CHECK_UNARY(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY, __VA_ARGS__) +#define DOCTEST_REQUIRE_UNARY(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY, __VA_ARGS__) +#define DOCTEST_WARN_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY_FALSE, __VA_ARGS__) +#define DOCTEST_CHECK_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY_FALSE, __VA_ARGS__) +#define DOCTEST_REQUIRE_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY_FALSE, __VA_ARGS__) + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + +#define DOCTEST_ASSERT_THROWS_AS(expr, assert_type, message, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + if(!doctest::getContextOptions()->no_throw) { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #expr, #__VA_ARGS__, message); \ + try { \ + DOCTEST_CAST_TO_VOID(expr) \ + } catch(const typename doctest::detail::types::remove_const< \ + typename doctest::detail::types::remove_reference<__VA_ARGS__>::type>::type&) {\ + DOCTEST_RB.translateException(); \ + DOCTEST_RB.m_threw_as = true; \ + } catch(...) { DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } else { /* NOLINT(*-else-after-return) */ \ + DOCTEST_FUNC_SCOPE_RET(false); \ + } \ + } DOCTEST_FUNC_SCOPE_END + +#define DOCTEST_ASSERT_THROWS_WITH(expr, expr_str, assert_type, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + if(!doctest::getContextOptions()->no_throw) { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, expr_str, "", __VA_ARGS__); \ + try { \ + DOCTEST_CAST_TO_VOID(expr) \ + } catch(...) { DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } else { /* NOLINT(*-else-after-return) */ \ + DOCTEST_FUNC_SCOPE_RET(false); \ + } \ + } DOCTEST_FUNC_SCOPE_END + +#define DOCTEST_ASSERT_NOTHROW(assert_type, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + try { \ + DOCTEST_CAST_TO_VOID(__VA_ARGS__) \ + } catch(...) { DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } DOCTEST_FUNC_SCOPE_END + +// clang-format off +#define DOCTEST_WARN_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_WARN_THROWS, "") +#define DOCTEST_CHECK_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_CHECK_THROWS, "") +#define DOCTEST_REQUIRE_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_REQUIRE_THROWS, "") + +#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_AS, "", __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_AS, "", __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_AS, "", __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_WARN_THROWS_WITH, __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_CHECK_THROWS_WITH, __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_REQUIRE_THROWS_WITH, __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_WITH_AS, message, __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_WITH_AS, message, __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_WITH_AS, message, __VA_ARGS__) + +#define DOCTEST_WARN_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_WARN_NOTHROW, __VA_ARGS__) +#define DOCTEST_CHECK_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_CHECK_NOTHROW, __VA_ARGS__) +#define DOCTEST_REQUIRE_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_REQUIRE_NOTHROW, __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_AS(expr, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_AS(expr, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_AS(expr, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_WITH(expr, with); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_WITH(expr, with); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_WITH(expr, with); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_WITH_AS(expr, with, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_NOTHROW(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_NOTHROW(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_NOTHROW(expr); } DOCTEST_FUNC_SCOPE_END +// clang-format on + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +// ================================================================================================= +// == WHAT FOLLOWS IS VERSIONS OF THE MACROS THAT DO NOT DO ANY REGISTERING! == +// == THIS CAN BE ENABLED BY DEFINING DOCTEST_CONFIG_DISABLE GLOBALLY! == +// ================================================================================================= +#else // DOCTEST_CONFIG_DISABLE + +#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, name) \ + namespace /* NOLINT */ { \ + template \ + struct der : public base \ + { void f(); }; \ + } \ + template \ + inline void der::f() + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, name) \ + template \ + static inline void f() + +// for registering tests +#define DOCTEST_TEST_CASE(name) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) + +// for registering tests in classes +#define DOCTEST_TEST_CASE_CLASS(name) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) + +// for registering tests with a fixture +#define DOCTEST_TEST_CASE_FIXTURE(x, name) \ + DOCTEST_IMPLEMENT_FIXTURE(DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), x, \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) + +// for converting types to strings without the header and demangling +#define DOCTEST_TYPE_TO_STRING_AS(str, ...) static_assert(true, "") +#define DOCTEST_TYPE_TO_STRING(...) static_assert(true, "") + +// for typed tests +#define DOCTEST_TEST_CASE_TEMPLATE(name, type, ...) \ + template \ + inline void DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)() + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(name, type, id) \ + template \ + inline void DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)() + +#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) static_assert(true, "") +#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) static_assert(true, "") + +// for subcases +#define DOCTEST_SUBCASE(name) + +// for a testsuite block +#define DOCTEST_TEST_SUITE(name) namespace // NOLINT + +// for starting a testsuite block +#define DOCTEST_TEST_SUITE_BEGIN(name) static_assert(true, "") + +// for ending a testsuite block +#define DOCTEST_TEST_SUITE_END using DOCTEST_ANONYMOUS(DOCTEST_ANON_FOR_SEMICOLON_) = int + +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \ + template \ + static inline doctest::String DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_)(signature) + +#define DOCTEST_REGISTER_REPORTER(name, priority, reporter) +#define DOCTEST_REGISTER_LISTENER(name, priority, reporter) + +#define DOCTEST_INFO(...) (static_cast(0)) +#define DOCTEST_CAPTURE(x) (static_cast(0)) +#define DOCTEST_ADD_MESSAGE_AT(file, line, ...) (static_cast(0)) +#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, ...) (static_cast(0)) +#define DOCTEST_ADD_FAIL_AT(file, line, ...) (static_cast(0)) +#define DOCTEST_MESSAGE(...) (static_cast(0)) +#define DOCTEST_FAIL_CHECK(...) (static_cast(0)) +#define DOCTEST_FAIL(...) (static_cast(0)) + +#if defined(DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED) \ + && defined(DOCTEST_CONFIG_ASSERTS_RETURN_VALUES) + +#define DOCTEST_WARN(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_CHECK(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_REQUIRE(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_WARN_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_CHECK_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_FALSE(...) [&] { return !(__VA_ARGS__); }() + +#define DOCTEST_WARN_MESSAGE(cond, ...) [&] { return cond; }() +#define DOCTEST_CHECK_MESSAGE(cond, ...) [&] { return cond; }() +#define DOCTEST_REQUIRE_MESSAGE(cond, ...) [&] { return cond; }() +#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() + +namespace doctest { +namespace detail { +#define DOCTEST_RELATIONAL_OP(name, op) \ + template \ + bool name(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) { return lhs op rhs; } + + DOCTEST_RELATIONAL_OP(eq, ==) + DOCTEST_RELATIONAL_OP(ne, !=) + DOCTEST_RELATIONAL_OP(lt, <) + DOCTEST_RELATIONAL_OP(gt, >) + DOCTEST_RELATIONAL_OP(le, <=) + DOCTEST_RELATIONAL_OP(ge, >=) +} // namespace detail +} // namespace doctest + +#define DOCTEST_WARN_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() +#define DOCTEST_CHECK_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() +#define DOCTEST_WARN_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() +#define DOCTEST_CHECK_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() +#define DOCTEST_WARN_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() +#define DOCTEST_CHECK_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() +#define DOCTEST_WARN_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() +#define DOCTEST_CHECK_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() +#define DOCTEST_WARN_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() +#define DOCTEST_CHECK_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() +#define DOCTEST_WARN_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() +#define DOCTEST_CHECK_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() +#define DOCTEST_WARN_UNARY(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_CHECK_UNARY(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_REQUIRE_UNARY(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_WARN_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_CHECK_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + +#define DOCTEST_WARN_THROWS_WITH(expr, with, ...) [] { static_assert(false, "Exception translation is not available when doctest is disabled."); return false; }() +#define DOCTEST_CHECK_THROWS_WITH(expr, with, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_REQUIRE_THROWS_WITH(expr, with, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,) + +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,) + +#define DOCTEST_WARN_THROWS(...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_CHECK_THROWS(...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_REQUIRE_THROWS(...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_WARN_THROWS_AS(expr, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_CHECK_THROWS_AS(expr, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_WARN_NOTHROW(...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +#define DOCTEST_CHECK_NOTHROW(...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +#define DOCTEST_REQUIRE_NOTHROW(...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +#else // DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED + +#define DOCTEST_WARN(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_FALSE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_FALSE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_FALSE(...) DOCTEST_FUNC_EMPTY + +#define DOCTEST_WARN_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY + +#define DOCTEST_WARN_EQ(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_EQ(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_EQ(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_NE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_NE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_NE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_GT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_GT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_GT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_LT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_LT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_LT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_GE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_GE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_GE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_LE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_LE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_LE(...) DOCTEST_FUNC_EMPTY + +#define DOCTEST_WARN_UNARY(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_UNARY(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_UNARY(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_UNARY_FALSE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_UNARY_FALSE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_UNARY_FALSE(...) DOCTEST_FUNC_EMPTY + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + +#define DOCTEST_WARN_THROWS(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_NOTHROW(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_NOTHROW(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_NOTHROW(...) DOCTEST_FUNC_EMPTY + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +#endif // DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED + +#endif // DOCTEST_CONFIG_DISABLE + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#define DOCTEST_EXCEPTION_EMPTY_FUNC DOCTEST_FUNC_EMPTY +#else // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#define DOCTEST_EXCEPTION_EMPTY_FUNC [] { static_assert(false, "Exceptions are disabled! " \ + "Use DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS if you want to compile with exceptions disabled."); return false; }() + +#undef DOCTEST_REQUIRE +#undef DOCTEST_REQUIRE_FALSE +#undef DOCTEST_REQUIRE_MESSAGE +#undef DOCTEST_REQUIRE_FALSE_MESSAGE +#undef DOCTEST_REQUIRE_EQ +#undef DOCTEST_REQUIRE_NE +#undef DOCTEST_REQUIRE_GT +#undef DOCTEST_REQUIRE_LT +#undef DOCTEST_REQUIRE_GE +#undef DOCTEST_REQUIRE_LE +#undef DOCTEST_REQUIRE_UNARY +#undef DOCTEST_REQUIRE_UNARY_FALSE + +#define DOCTEST_REQUIRE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_FALSE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_MESSAGE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_FALSE_MESSAGE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_EQ DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_NE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_GT DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_LT DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_GE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_LE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_UNARY DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_UNARY_FALSE DOCTEST_EXCEPTION_EMPTY_FUNC + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#define DOCTEST_WARN_THROWS(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_NOTHROW(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_NOTHROW(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_NOTHROW(...) DOCTEST_EXCEPTION_EMPTY_FUNC + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +// clang-format off +// KEPT FOR BACKWARDS COMPATIBILITY - FORWARDING TO THE RIGHT MACROS +#define DOCTEST_FAST_WARN_EQ DOCTEST_WARN_EQ +#define DOCTEST_FAST_CHECK_EQ DOCTEST_CHECK_EQ +#define DOCTEST_FAST_REQUIRE_EQ DOCTEST_REQUIRE_EQ +#define DOCTEST_FAST_WARN_NE DOCTEST_WARN_NE +#define DOCTEST_FAST_CHECK_NE DOCTEST_CHECK_NE +#define DOCTEST_FAST_REQUIRE_NE DOCTEST_REQUIRE_NE +#define DOCTEST_FAST_WARN_GT DOCTEST_WARN_GT +#define DOCTEST_FAST_CHECK_GT DOCTEST_CHECK_GT +#define DOCTEST_FAST_REQUIRE_GT DOCTEST_REQUIRE_GT +#define DOCTEST_FAST_WARN_LT DOCTEST_WARN_LT +#define DOCTEST_FAST_CHECK_LT DOCTEST_CHECK_LT +#define DOCTEST_FAST_REQUIRE_LT DOCTEST_REQUIRE_LT +#define DOCTEST_FAST_WARN_GE DOCTEST_WARN_GE +#define DOCTEST_FAST_CHECK_GE DOCTEST_CHECK_GE +#define DOCTEST_FAST_REQUIRE_GE DOCTEST_REQUIRE_GE +#define DOCTEST_FAST_WARN_LE DOCTEST_WARN_LE +#define DOCTEST_FAST_CHECK_LE DOCTEST_CHECK_LE +#define DOCTEST_FAST_REQUIRE_LE DOCTEST_REQUIRE_LE + +#define DOCTEST_FAST_WARN_UNARY DOCTEST_WARN_UNARY +#define DOCTEST_FAST_CHECK_UNARY DOCTEST_CHECK_UNARY +#define DOCTEST_FAST_REQUIRE_UNARY DOCTEST_REQUIRE_UNARY +#define DOCTEST_FAST_WARN_UNARY_FALSE DOCTEST_WARN_UNARY_FALSE +#define DOCTEST_FAST_CHECK_UNARY_FALSE DOCTEST_CHECK_UNARY_FALSE +#define DOCTEST_FAST_REQUIRE_UNARY_FALSE DOCTEST_REQUIRE_UNARY_FALSE + +#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id,__VA_ARGS__) +// clang-format on + +// BDD style macros +// clang-format off +#define DOCTEST_SCENARIO(name) DOCTEST_TEST_CASE(" Scenario: " name) +#define DOCTEST_SCENARIO_CLASS(name) DOCTEST_TEST_CASE_CLASS(" Scenario: " name) +#define DOCTEST_SCENARIO_TEMPLATE(name, T, ...) DOCTEST_TEST_CASE_TEMPLATE(" Scenario: " name, T, __VA_ARGS__) +#define DOCTEST_SCENARIO_TEMPLATE_DEFINE(name, T, id) DOCTEST_TEST_CASE_TEMPLATE_DEFINE(" Scenario: " name, T, id) + +#define DOCTEST_GIVEN(name) DOCTEST_SUBCASE(" Given: " name) +#define DOCTEST_WHEN(name) DOCTEST_SUBCASE(" When: " name) +#define DOCTEST_AND_WHEN(name) DOCTEST_SUBCASE("And when: " name) +#define DOCTEST_THEN(name) DOCTEST_SUBCASE(" Then: " name) +#define DOCTEST_AND_THEN(name) DOCTEST_SUBCASE(" And: " name) +// clang-format on + +// == SHORT VERSIONS OF THE MACROS +#ifndef DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES + +#define TEST_CASE(name) DOCTEST_TEST_CASE(name) +#define TEST_CASE_CLASS(name) DOCTEST_TEST_CASE_CLASS(name) +#define TEST_CASE_FIXTURE(x, name) DOCTEST_TEST_CASE_FIXTURE(x, name) +#define TYPE_TO_STRING_AS(str, ...) DOCTEST_TYPE_TO_STRING_AS(str, __VA_ARGS__) +#define TYPE_TO_STRING(...) DOCTEST_TYPE_TO_STRING(__VA_ARGS__) +#define TEST_CASE_TEMPLATE(name, T, ...) DOCTEST_TEST_CASE_TEMPLATE(name, T, __VA_ARGS__) +#define TEST_CASE_TEMPLATE_DEFINE(name, T, id) DOCTEST_TEST_CASE_TEMPLATE_DEFINE(name, T, id) +#define TEST_CASE_TEMPLATE_INVOKE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, __VA_ARGS__) +#define TEST_CASE_TEMPLATE_APPLY(id, ...) DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, __VA_ARGS__) +#define SUBCASE(name) DOCTEST_SUBCASE(name) +#define TEST_SUITE(decorators) DOCTEST_TEST_SUITE(decorators) +#define TEST_SUITE_BEGIN(name) DOCTEST_TEST_SUITE_BEGIN(name) +#define TEST_SUITE_END DOCTEST_TEST_SUITE_END +#define REGISTER_EXCEPTION_TRANSLATOR(signature) DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) +#define REGISTER_REPORTER(name, priority, reporter) DOCTEST_REGISTER_REPORTER(name, priority, reporter) +#define REGISTER_LISTENER(name, priority, reporter) DOCTEST_REGISTER_LISTENER(name, priority, reporter) +#define INFO(...) DOCTEST_INFO(__VA_ARGS__) +#define CAPTURE(x) DOCTEST_CAPTURE(x) +#define ADD_MESSAGE_AT(file, line, ...) DOCTEST_ADD_MESSAGE_AT(file, line, __VA_ARGS__) +#define ADD_FAIL_CHECK_AT(file, line, ...) DOCTEST_ADD_FAIL_CHECK_AT(file, line, __VA_ARGS__) +#define ADD_FAIL_AT(file, line, ...) DOCTEST_ADD_FAIL_AT(file, line, __VA_ARGS__) +#define MESSAGE(...) DOCTEST_MESSAGE(__VA_ARGS__) +#define FAIL_CHECK(...) DOCTEST_FAIL_CHECK(__VA_ARGS__) +#define FAIL(...) DOCTEST_FAIL(__VA_ARGS__) +#define TO_LVALUE(...) DOCTEST_TO_LVALUE(__VA_ARGS__) + +#define WARN(...) DOCTEST_WARN(__VA_ARGS__) +#define WARN_FALSE(...) DOCTEST_WARN_FALSE(__VA_ARGS__) +#define WARN_THROWS(...) DOCTEST_WARN_THROWS(__VA_ARGS__) +#define WARN_THROWS_AS(expr, ...) DOCTEST_WARN_THROWS_AS(expr, __VA_ARGS__) +#define WARN_THROWS_WITH(expr, ...) DOCTEST_WARN_THROWS_WITH(expr, __VA_ARGS__) +#define WARN_THROWS_WITH_AS(expr, with, ...) DOCTEST_WARN_THROWS_WITH_AS(expr, with, __VA_ARGS__) +#define WARN_NOTHROW(...) DOCTEST_WARN_NOTHROW(__VA_ARGS__) +#define CHECK(...) DOCTEST_CHECK(__VA_ARGS__) +#define CHECK_FALSE(...) DOCTEST_CHECK_FALSE(__VA_ARGS__) +#define CHECK_THROWS(...) DOCTEST_CHECK_THROWS(__VA_ARGS__) +#define CHECK_THROWS_AS(expr, ...) DOCTEST_CHECK_THROWS_AS(expr, __VA_ARGS__) +#define CHECK_THROWS_WITH(expr, ...) DOCTEST_CHECK_THROWS_WITH(expr, __VA_ARGS__) +#define CHECK_THROWS_WITH_AS(expr, with, ...) DOCTEST_CHECK_THROWS_WITH_AS(expr, with, __VA_ARGS__) +#define CHECK_NOTHROW(...) DOCTEST_CHECK_NOTHROW(__VA_ARGS__) +#define REQUIRE(...) DOCTEST_REQUIRE(__VA_ARGS__) +#define REQUIRE_FALSE(...) DOCTEST_REQUIRE_FALSE(__VA_ARGS__) +#define REQUIRE_THROWS(...) DOCTEST_REQUIRE_THROWS(__VA_ARGS__) +#define REQUIRE_THROWS_AS(expr, ...) DOCTEST_REQUIRE_THROWS_AS(expr, __VA_ARGS__) +#define REQUIRE_THROWS_WITH(expr, ...) DOCTEST_REQUIRE_THROWS_WITH(expr, __VA_ARGS__) +#define REQUIRE_THROWS_WITH_AS(expr, with, ...) DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, __VA_ARGS__) +#define REQUIRE_NOTHROW(...) DOCTEST_REQUIRE_NOTHROW(__VA_ARGS__) + +#define WARN_MESSAGE(cond, ...) DOCTEST_WARN_MESSAGE(cond, __VA_ARGS__) +#define WARN_FALSE_MESSAGE(cond, ...) DOCTEST_WARN_FALSE_MESSAGE(cond, __VA_ARGS__) +#define WARN_THROWS_MESSAGE(expr, ...) DOCTEST_WARN_THROWS_MESSAGE(expr, __VA_ARGS__) +#define WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) +#define WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) +#define WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) +#define WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_WARN_NOTHROW_MESSAGE(expr, __VA_ARGS__) +#define CHECK_MESSAGE(cond, ...) DOCTEST_CHECK_MESSAGE(cond, __VA_ARGS__) +#define CHECK_FALSE_MESSAGE(cond, ...) DOCTEST_CHECK_FALSE_MESSAGE(cond, __VA_ARGS__) +#define CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_CHECK_THROWS_MESSAGE(expr, __VA_ARGS__) +#define CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) +#define CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) +#define CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) +#define CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_CHECK_NOTHROW_MESSAGE(expr, __VA_ARGS__) +#define REQUIRE_MESSAGE(cond, ...) DOCTEST_REQUIRE_MESSAGE(cond, __VA_ARGS__) +#define REQUIRE_FALSE_MESSAGE(cond, ...) DOCTEST_REQUIRE_FALSE_MESSAGE(cond, __VA_ARGS__) +#define REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_REQUIRE_THROWS_MESSAGE(expr, __VA_ARGS__) +#define REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) +#define REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) +#define REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) +#define REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, __VA_ARGS__) + +#define SCENARIO(name) DOCTEST_SCENARIO(name) +#define SCENARIO_CLASS(name) DOCTEST_SCENARIO_CLASS(name) +#define SCENARIO_TEMPLATE(name, T, ...) DOCTEST_SCENARIO_TEMPLATE(name, T, __VA_ARGS__) +#define SCENARIO_TEMPLATE_DEFINE(name, T, id) DOCTEST_SCENARIO_TEMPLATE_DEFINE(name, T, id) +#define GIVEN(name) DOCTEST_GIVEN(name) +#define WHEN(name) DOCTEST_WHEN(name) +#define AND_WHEN(name) DOCTEST_AND_WHEN(name) +#define THEN(name) DOCTEST_THEN(name) +#define AND_THEN(name) DOCTEST_AND_THEN(name) + +#define WARN_EQ(...) DOCTEST_WARN_EQ(__VA_ARGS__) +#define CHECK_EQ(...) DOCTEST_CHECK_EQ(__VA_ARGS__) +#define REQUIRE_EQ(...) DOCTEST_REQUIRE_EQ(__VA_ARGS__) +#define WARN_NE(...) DOCTEST_WARN_NE(__VA_ARGS__) +#define CHECK_NE(...) DOCTEST_CHECK_NE(__VA_ARGS__) +#define REQUIRE_NE(...) DOCTEST_REQUIRE_NE(__VA_ARGS__) +#define WARN_GT(...) DOCTEST_WARN_GT(__VA_ARGS__) +#define CHECK_GT(...) DOCTEST_CHECK_GT(__VA_ARGS__) +#define REQUIRE_GT(...) DOCTEST_REQUIRE_GT(__VA_ARGS__) +#define WARN_LT(...) DOCTEST_WARN_LT(__VA_ARGS__) +#define CHECK_LT(...) DOCTEST_CHECK_LT(__VA_ARGS__) +#define REQUIRE_LT(...) DOCTEST_REQUIRE_LT(__VA_ARGS__) +#define WARN_GE(...) DOCTEST_WARN_GE(__VA_ARGS__) +#define CHECK_GE(...) DOCTEST_CHECK_GE(__VA_ARGS__) +#define REQUIRE_GE(...) DOCTEST_REQUIRE_GE(__VA_ARGS__) +#define WARN_LE(...) DOCTEST_WARN_LE(__VA_ARGS__) +#define CHECK_LE(...) DOCTEST_CHECK_LE(__VA_ARGS__) +#define REQUIRE_LE(...) DOCTEST_REQUIRE_LE(__VA_ARGS__) +#define WARN_UNARY(...) DOCTEST_WARN_UNARY(__VA_ARGS__) +#define CHECK_UNARY(...) DOCTEST_CHECK_UNARY(__VA_ARGS__) +#define REQUIRE_UNARY(...) DOCTEST_REQUIRE_UNARY(__VA_ARGS__) +#define WARN_UNARY_FALSE(...) DOCTEST_WARN_UNARY_FALSE(__VA_ARGS__) +#define CHECK_UNARY_FALSE(...) DOCTEST_CHECK_UNARY_FALSE(__VA_ARGS__) +#define REQUIRE_UNARY_FALSE(...) DOCTEST_REQUIRE_UNARY_FALSE(__VA_ARGS__) + +// KEPT FOR BACKWARDS COMPATIBILITY +#define FAST_WARN_EQ(...) DOCTEST_FAST_WARN_EQ(__VA_ARGS__) +#define FAST_CHECK_EQ(...) DOCTEST_FAST_CHECK_EQ(__VA_ARGS__) +#define FAST_REQUIRE_EQ(...) DOCTEST_FAST_REQUIRE_EQ(__VA_ARGS__) +#define FAST_WARN_NE(...) DOCTEST_FAST_WARN_NE(__VA_ARGS__) +#define FAST_CHECK_NE(...) DOCTEST_FAST_CHECK_NE(__VA_ARGS__) +#define FAST_REQUIRE_NE(...) DOCTEST_FAST_REQUIRE_NE(__VA_ARGS__) +#define FAST_WARN_GT(...) DOCTEST_FAST_WARN_GT(__VA_ARGS__) +#define FAST_CHECK_GT(...) DOCTEST_FAST_CHECK_GT(__VA_ARGS__) +#define FAST_REQUIRE_GT(...) DOCTEST_FAST_REQUIRE_GT(__VA_ARGS__) +#define FAST_WARN_LT(...) DOCTEST_FAST_WARN_LT(__VA_ARGS__) +#define FAST_CHECK_LT(...) DOCTEST_FAST_CHECK_LT(__VA_ARGS__) +#define FAST_REQUIRE_LT(...) DOCTEST_FAST_REQUIRE_LT(__VA_ARGS__) +#define FAST_WARN_GE(...) DOCTEST_FAST_WARN_GE(__VA_ARGS__) +#define FAST_CHECK_GE(...) DOCTEST_FAST_CHECK_GE(__VA_ARGS__) +#define FAST_REQUIRE_GE(...) DOCTEST_FAST_REQUIRE_GE(__VA_ARGS__) +#define FAST_WARN_LE(...) DOCTEST_FAST_WARN_LE(__VA_ARGS__) +#define FAST_CHECK_LE(...) DOCTEST_FAST_CHECK_LE(__VA_ARGS__) +#define FAST_REQUIRE_LE(...) DOCTEST_FAST_REQUIRE_LE(__VA_ARGS__) + +#define FAST_WARN_UNARY(...) DOCTEST_FAST_WARN_UNARY(__VA_ARGS__) +#define FAST_CHECK_UNARY(...) DOCTEST_FAST_CHECK_UNARY(__VA_ARGS__) +#define FAST_REQUIRE_UNARY(...) DOCTEST_FAST_REQUIRE_UNARY(__VA_ARGS__) +#define FAST_WARN_UNARY_FALSE(...) DOCTEST_FAST_WARN_UNARY_FALSE(__VA_ARGS__) +#define FAST_CHECK_UNARY_FALSE(...) DOCTEST_FAST_CHECK_UNARY_FALSE(__VA_ARGS__) +#define FAST_REQUIRE_UNARY_FALSE(...) DOCTEST_FAST_REQUIRE_UNARY_FALSE(__VA_ARGS__) + +#define TEST_CASE_TEMPLATE_INSTANTIATE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE(id, __VA_ARGS__) + +#endif // DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES + +#ifndef DOCTEST_CONFIG_DISABLE + +// this is here to clear the 'current test suite' for the current translation unit - at the top +DOCTEST_TEST_SUITE_END(); + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_MSVC_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +DOCTEST_SUPPRESS_COMMON_WARNINGS_POP + +#endif // DOCTEST_LIBRARY_INCLUDED + +#ifndef DOCTEST_SINGLE_HEADER +#define DOCTEST_SINGLE_HEADER +#endif // DOCTEST_SINGLE_HEADER + +#if defined(DOCTEST_CONFIG_IMPLEMENT) || !defined(DOCTEST_SINGLE_HEADER) + +#ifndef DOCTEST_SINGLE_HEADER +#include "doctest_fwd.h" +#endif // DOCTEST_SINGLE_HEADER + +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-macros") + +#ifndef DOCTEST_LIBRARY_IMPLEMENTATION +#define DOCTEST_LIBRARY_IMPLEMENTATION + +DOCTEST_CLANG_SUPPRESS_WARNING_POP + +DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH + +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wglobal-constructors") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wshorten-64-to-32") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-variable-declarations") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch-enum") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wcovered-switch-default") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-noreturn") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wdisabled-macro-expansion") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-braces") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-field-initializers") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-member-function") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wnonportable-system-include-path") + +DOCTEST_GCC_SUPPRESS_WARNING_PUSH +DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-field-initializers") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-braces") +DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch") +DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-enum") +DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-default") +DOCTEST_GCC_SUPPRESS_WARNING("-Wunsafe-loop-optimizations") +DOCTEST_GCC_SUPPRESS_WARNING("-Wold-style-cast") +DOCTEST_GCC_SUPPRESS_WARNING("-Wunused-function") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmultiple-inheritance") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsuggest-attribute") + +DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +DOCTEST_MSVC_SUPPRESS_WARNING(4267) // 'var' : conversion from 'x' to 'y', possible loss of data +DOCTEST_MSVC_SUPPRESS_WARNING(4530) // C++ exception handler used, but unwind semantics not enabled +DOCTEST_MSVC_SUPPRESS_WARNING(4577) // 'noexcept' used with no exception handling mode specified +DOCTEST_MSVC_SUPPRESS_WARNING(4774) // format string expected in argument is not a string literal +DOCTEST_MSVC_SUPPRESS_WARNING(4365) // conversion from 'int' to 'unsigned', signed/unsigned mismatch +DOCTEST_MSVC_SUPPRESS_WARNING(5039) // pointer to potentially throwing function passed to extern C +DOCTEST_MSVC_SUPPRESS_WARNING(4800) // forcing value to bool 'true' or 'false' (performance warning) +DOCTEST_MSVC_SUPPRESS_WARNING(5245) // unreferenced function with internal linkage has been removed + +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN + +// required includes - will go only in one translation unit! +#include +#include +#include +// borland (Embarcadero) compiler requires math.h and not cmath - https://github.com/doctest/doctest/pull/37 +#ifdef __BORLANDC__ +#include +#endif // __BORLANDC__ +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM +#include +#endif // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM +#include +#include +#include +#ifndef DOCTEST_CONFIG_NO_MULTITHREADING +#include +#include +#define DOCTEST_DECLARE_MUTEX(name) std::mutex name; +#define DOCTEST_DECLARE_STATIC_MUTEX(name) static DOCTEST_DECLARE_MUTEX(name) +#define DOCTEST_LOCK_MUTEX(name) std::lock_guard DOCTEST_ANONYMOUS(DOCTEST_ANON_LOCK_)(name); +#else // DOCTEST_CONFIG_NO_MULTITHREADING +#define DOCTEST_DECLARE_MUTEX(name) +#define DOCTEST_DECLARE_STATIC_MUTEX(name) +#define DOCTEST_LOCK_MUTEX(name) +#endif // DOCTEST_CONFIG_NO_MULTITHREADING +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef DOCTEST_PLATFORM_MAC +#include +#include +#include +#endif // DOCTEST_PLATFORM_MAC + +#ifdef DOCTEST_PLATFORM_WINDOWS + +// defines for a leaner windows.h +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#define DOCTEST_UNDEF_WIN32_LEAN_AND_MEAN +#endif // WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#define DOCTEST_UNDEF_NOMINMAX +#endif // NOMINMAX + +// not sure what AfxWin.h is for - here I do what Catch does +#ifdef __AFXDLL +#include +#else +#include +#endif +#include + +#else // DOCTEST_PLATFORM_WINDOWS + +#include +#include + +#endif // DOCTEST_PLATFORM_WINDOWS + +// this is a fix for https://github.com/doctest/doctest/issues/348 +// https://mail.gnome.org/archives/xml/2012-January/msg00000.html +#if !defined(HAVE_UNISTD_H) && !defined(STDOUT_FILENO) +#define STDOUT_FILENO fileno(stdout) +#endif // HAVE_UNISTD_H + +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END + +// counts the number of elements in a C array +#define DOCTEST_COUNTOF(x) (sizeof(x) / sizeof(x[0])) + +#ifdef DOCTEST_CONFIG_DISABLE +#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_disabled +#else // DOCTEST_CONFIG_DISABLE +#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_not_disabled +#endif // DOCTEST_CONFIG_DISABLE + +#ifndef DOCTEST_CONFIG_OPTIONS_PREFIX +#define DOCTEST_CONFIG_OPTIONS_PREFIX "dt-" +#endif + +#ifndef DOCTEST_CONFIG_OPTIONS_FILE_PREFIX_SEPARATOR +#define DOCTEST_CONFIG_OPTIONS_FILE_PREFIX_SEPARATOR ':' +#endif + +#ifndef DOCTEST_THREAD_LOCAL +#if defined(DOCTEST_CONFIG_NO_MULTITHREADING) || DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_THREAD_LOCAL +#else // DOCTEST_MSVC +#define DOCTEST_THREAD_LOCAL thread_local +#endif // DOCTEST_MSVC +#endif // DOCTEST_THREAD_LOCAL + +#ifndef DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES +#define DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES 32 +#endif + +#ifndef DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE +#define DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE 64 +#endif + +#ifdef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS +#define DOCTEST_OPTIONS_PREFIX_DISPLAY DOCTEST_CONFIG_OPTIONS_PREFIX +#else +#define DOCTEST_OPTIONS_PREFIX_DISPLAY "" +#endif + +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) +#define DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS +#endif + +#ifndef DOCTEST_CDECL +#define DOCTEST_CDECL __cdecl +#endif + +namespace doctest { + +bool is_running_in_test = false; + +namespace { + using namespace detail; + + template + DOCTEST_NORETURN void throw_exception(Ex const& e) { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + throw e; +#else // DOCTEST_CONFIG_NO_EXCEPTIONS +#ifdef DOCTEST_CONFIG_HANDLE_EXCEPTION + DOCTEST_CONFIG_HANDLE_EXCEPTION(e); +#else // DOCTEST_CONFIG_HANDLE_EXCEPTION +#ifndef DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + std::cerr << "doctest will terminate because it needed to throw an exception.\n" + << "The message was: " << e.what() << '\n'; +#endif // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM +#endif // DOCTEST_CONFIG_HANDLE_EXCEPTION + std::terminate(); +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + } + +#ifndef DOCTEST_INTERNAL_ERROR +#define DOCTEST_INTERNAL_ERROR(msg) \ + throw_exception(std::logic_error( \ + __FILE__ ":" DOCTEST_TOSTR(__LINE__) ": Internal doctest error: " msg)) +#endif // DOCTEST_INTERNAL_ERROR + + // case insensitive strcmp + int stricmp(const char* a, const char* b) { + for(;; a++, b++) { + const int d = tolower(*a) - tolower(*b); + if(d != 0 || !*a) + return d; + } + } + + struct Endianness + { + enum Arch + { + Big, + Little + }; + + static Arch which() { + int x = 1; + // casting any data pointer to char* is allowed + auto ptr = reinterpret_cast(&x); + if(*ptr) + return Little; + return Big; + } + }; +} // namespace + +namespace detail { + DOCTEST_THREAD_LOCAL class + { + std::vector stack; + std::stringstream ss; + + public: + std::ostream* push() { + stack.push_back(ss.tellp()); + return &ss; + } + + String pop() { + if (stack.empty()) + DOCTEST_INTERNAL_ERROR("TLSS was empty when trying to pop!"); + + std::streampos pos = stack.back(); + stack.pop_back(); + unsigned sz = static_cast(ss.tellp() - pos); + ss.rdbuf()->pubseekpos(pos, std::ios::in | std::ios::out); + return String(ss, sz); + } + } g_oss; + + std::ostream* tlssPush() { + return g_oss.push(); + } + + String tlssPop() { + return g_oss.pop(); + } + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace timer_large_integer +{ + +#if defined(DOCTEST_PLATFORM_WINDOWS) + using type = ULONGLONG; +#else // DOCTEST_PLATFORM_WINDOWS + using type = std::uint64_t; +#endif // DOCTEST_PLATFORM_WINDOWS +} + +using ticks_t = timer_large_integer::type; + +#ifdef DOCTEST_CONFIG_GETCURRENTTICKS + ticks_t getCurrentTicks() { return DOCTEST_CONFIG_GETCURRENTTICKS(); } +#elif defined(DOCTEST_PLATFORM_WINDOWS) + ticks_t getCurrentTicks() { + static LARGE_INTEGER hz = { {0} }, hzo = { {0} }; + if(!hz.QuadPart) { + QueryPerformanceFrequency(&hz); + QueryPerformanceCounter(&hzo); + } + LARGE_INTEGER t; + QueryPerformanceCounter(&t); + return ((t.QuadPart - hzo.QuadPart) * LONGLONG(1000000)) / hz.QuadPart; + } +#else // DOCTEST_PLATFORM_WINDOWS + ticks_t getCurrentTicks() { + timeval t; + gettimeofday(&t, nullptr); + return static_cast(t.tv_sec) * 1000000 + static_cast(t.tv_usec); + } +#endif // DOCTEST_PLATFORM_WINDOWS + + struct Timer + { + void start() { m_ticks = getCurrentTicks(); } + unsigned int getElapsedMicroseconds() const { + return static_cast(getCurrentTicks() - m_ticks); + } + //unsigned int getElapsedMilliseconds() const { + // return static_cast(getElapsedMicroseconds() / 1000); + //} + double getElapsedSeconds() const { return static_cast(getCurrentTicks() - m_ticks) / 1000000.0; } + + private: + ticks_t m_ticks = 0; + }; + +#ifdef DOCTEST_CONFIG_NO_MULTITHREADING + template + using Atomic = T; +#else // DOCTEST_CONFIG_NO_MULTITHREADING + template + using Atomic = std::atomic; +#endif // DOCTEST_CONFIG_NO_MULTITHREADING + +#if defined(DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS) || defined(DOCTEST_CONFIG_NO_MULTITHREADING) + template + using MultiLaneAtomic = Atomic; +#else // DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS + // Provides a multilane implementation of an atomic variable that supports add, sub, load, + // store. Instead of using a single atomic variable, this splits up into multiple ones, + // each sitting on a separate cache line. The goal is to provide a speedup when most + // operations are modifying. It achieves this with two properties: + // + // * Multiple atomics are used, so chance of congestion from the same atomic is reduced. + // * Each atomic sits on a separate cache line, so false sharing is reduced. + // + // The disadvantage is that there is a small overhead due to the use of TLS, and load/store + // is slower because all atomics have to be accessed. + template + class MultiLaneAtomic + { + struct CacheLineAlignedAtomic + { + Atomic atomic{}; + char padding[DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE - sizeof(Atomic)]; + }; + CacheLineAlignedAtomic m_atomics[DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES]; + + static_assert(sizeof(CacheLineAlignedAtomic) == DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE, + "guarantee one atomic takes exactly one cache line"); + + public: + T operator++() DOCTEST_NOEXCEPT { return fetch_add(1) + 1; } + + T operator++(int) DOCTEST_NOEXCEPT { return fetch_add(1); } + + T fetch_add(T arg, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { + return myAtomic().fetch_add(arg, order); + } + + T fetch_sub(T arg, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { + return myAtomic().fetch_sub(arg, order); + } + + operator T() const DOCTEST_NOEXCEPT { return load(); } + + T load(std::memory_order order = std::memory_order_seq_cst) const DOCTEST_NOEXCEPT { + auto result = T(); + for(auto const& c : m_atomics) { + result += c.atomic.load(order); + } + return result; + } + + T operator=(T desired) DOCTEST_NOEXCEPT { // lgtm [cpp/assignment-does-not-return-this] + store(desired); + return desired; + } + + void store(T desired, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { + // first value becomes desired", all others become 0. + for(auto& c : m_atomics) { + c.atomic.store(desired, order); + desired = {}; + } + } + + private: + // Each thread has a different atomic that it operates on. If more than NumLanes threads + // use this, some will use the same atomic. So performance will degrade a bit, but still + // everything will work. + // + // The logic here is a bit tricky. The call should be as fast as possible, so that there + // is minimal to no overhead in determining the correct atomic for the current thread. + // + // 1. A global static counter laneCounter counts continuously up. + // 2. Each successive thread will use modulo operation of that counter so it gets an atomic + // assigned in a round-robin fashion. + // 3. This tlsLaneIdx is stored in the thread local data, so it is directly available with + // little overhead. + Atomic& myAtomic() DOCTEST_NOEXCEPT { + static Atomic laneCounter; + DOCTEST_THREAD_LOCAL size_t tlsLaneIdx = + laneCounter++ % DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES; + + return m_atomics[tlsLaneIdx].atomic; + } + }; +#endif // DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS + + // this holds both parameters from the command line and runtime data for tests + struct ContextState : ContextOptions, TestRunStats, CurrentTestCaseStats + { + MultiLaneAtomic numAssertsCurrentTest_atomic; + MultiLaneAtomic numAssertsFailedCurrentTest_atomic; + + std::vector> filters = decltype(filters)(9); // 9 different filters + + std::vector reporters_currently_used; + + assert_handler ah = nullptr; + + Timer timer; + + std::vector stringifiedContexts; // logging from INFO() due to an exception + + // stuff for subcases + bool reachedLeaf; + std::vector subcaseStack; + std::vector nextSubcaseStack; + std::unordered_set fullyTraversedSubcases; + size_t currentSubcaseDepth; + Atomic shouldLogCurrentException; + + void resetRunData() { + numTestCases = 0; + numTestCasesPassingFilters = 0; + numTestSuitesPassingFilters = 0; + numTestCasesFailed = 0; + numAsserts = 0; + numAssertsFailed = 0; + numAssertsCurrentTest = 0; + numAssertsFailedCurrentTest = 0; + } + + void finalizeTestCaseData() { + seconds = timer.getElapsedSeconds(); + + // update the non-atomic counters + numAsserts += numAssertsCurrentTest_atomic; + numAssertsFailed += numAssertsFailedCurrentTest_atomic; + numAssertsCurrentTest = numAssertsCurrentTest_atomic; + numAssertsFailedCurrentTest = numAssertsFailedCurrentTest_atomic; + + if(numAssertsFailedCurrentTest) + failure_flags |= TestCaseFailureReason::AssertFailure; + + if(Approx(currentTest->m_timeout).epsilon(DBL_EPSILON) != 0 && + Approx(seconds).epsilon(DBL_EPSILON) > currentTest->m_timeout) + failure_flags |= TestCaseFailureReason::Timeout; + + if(currentTest->m_should_fail) { + if(failure_flags) { + failure_flags |= TestCaseFailureReason::ShouldHaveFailedAndDid; + } else { + failure_flags |= TestCaseFailureReason::ShouldHaveFailedButDidnt; + } + } else if(failure_flags && currentTest->m_may_fail) { + failure_flags |= TestCaseFailureReason::CouldHaveFailedAndDid; + } else if(currentTest->m_expected_failures > 0) { + if(numAssertsFailedCurrentTest == currentTest->m_expected_failures) { + failure_flags |= TestCaseFailureReason::FailedExactlyNumTimes; + } else { + failure_flags |= TestCaseFailureReason::DidntFailExactlyNumTimes; + } + } + + bool ok_to_fail = (TestCaseFailureReason::ShouldHaveFailedAndDid & failure_flags) || + (TestCaseFailureReason::CouldHaveFailedAndDid & failure_flags) || + (TestCaseFailureReason::FailedExactlyNumTimes & failure_flags); + + // if any subcase has failed - the whole test case has failed + testCaseSuccess = !(failure_flags && !ok_to_fail); + if(!testCaseSuccess) + numTestCasesFailed++; + } + }; + + ContextState* g_cs = nullptr; + + // used to avoid locks for the debug output + // TODO: figure out if this is indeed necessary/correct - seems like either there still + // could be a race or that there wouldn't be a race even if using the context directly + DOCTEST_THREAD_LOCAL bool g_no_colors; + +#endif // DOCTEST_CONFIG_DISABLE +} // namespace detail + +char* String::allocate(size_type sz) { + if (sz <= last) { + buf[sz] = '\0'; + setLast(last - sz); + return buf; + } else { + setOnHeap(); + data.size = sz; + data.capacity = data.size + 1; + data.ptr = new char[data.capacity]; + data.ptr[sz] = '\0'; + return data.ptr; + } +} + +void String::setOnHeap() noexcept { *reinterpret_cast(&buf[last]) = 128; } +void String::setLast(size_type in) noexcept { buf[last] = char(in); } +void String::setSize(size_type sz) noexcept { + if (isOnStack()) { buf[sz] = '\0'; setLast(last - sz); } + else { data.ptr[sz] = '\0'; data.size = sz; } +} + +void String::copy(const String& other) { + if(other.isOnStack()) { + memcpy(buf, other.buf, len); + } else { + memcpy(allocate(other.data.size), other.data.ptr, other.data.size); + } +} + +String::String() noexcept { + buf[0] = '\0'; + setLast(); +} + +String::~String() { + if(!isOnStack()) + delete[] data.ptr; +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + +String::String(const char* in) + : String(in, strlen(in)) {} + +String::String(const char* in, size_type in_size) { + memcpy(allocate(in_size), in, in_size); +} + +String::String(std::istream& in, size_type in_size) { + in.read(allocate(in_size), in_size); +} + +String::String(const String& other) { copy(other); } + +String& String::operator=(const String& other) { + if(this != &other) { + if(!isOnStack()) + delete[] data.ptr; + + copy(other); + } + + return *this; +} + +String& String::operator+=(const String& other) { + const size_type my_old_size = size(); + const size_type other_size = other.size(); + const size_type total_size = my_old_size + other_size; + if(isOnStack()) { + if(total_size < len) { + // append to the current stack space + memcpy(buf + my_old_size, other.c_str(), other_size + 1); + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) + setLast(last - total_size); + } else { + // alloc new chunk + char* temp = new char[total_size + 1]; + // copy current data to new location before writing in the union + memcpy(temp, buf, my_old_size); // skip the +1 ('\0') for speed + // update data in union + setOnHeap(); + data.size = total_size; + data.capacity = data.size + 1; + data.ptr = temp; + // transfer the rest of the data + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } + } else { + if(data.capacity > total_size) { + // append to the current heap block + data.size = total_size; + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } else { + // resize + data.capacity *= 2; + if(data.capacity <= total_size) + data.capacity = total_size + 1; + // alloc new chunk + char* temp = new char[data.capacity]; + // copy current data to new location before releasing it + memcpy(temp, data.ptr, my_old_size); // skip the +1 ('\0') for speed + // release old chunk + delete[] data.ptr; + // update the rest of the union members + data.size = total_size; + data.ptr = temp; + // transfer the rest of the data + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } + } + + return *this; +} + +String::String(String&& other) noexcept { + memcpy(buf, other.buf, len); + other.buf[0] = '\0'; + other.setLast(); +} + +String& String::operator=(String&& other) noexcept { + if(this != &other) { + if(!isOnStack()) + delete[] data.ptr; + memcpy(buf, other.buf, len); + other.buf[0] = '\0'; + other.setLast(); + } + return *this; +} + +char String::operator[](size_type i) const { + return const_cast(this)->operator[](i); +} + +char& String::operator[](size_type i) { + if(isOnStack()) + return reinterpret_cast(buf)[i]; + return data.ptr[i]; +} + +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wmaybe-uninitialized") +String::size_type String::size() const { + if(isOnStack()) + return last - (size_type(buf[last]) & 31); // using "last" would work only if "len" is 32 + return data.size; +} +DOCTEST_GCC_SUPPRESS_WARNING_POP + +String::size_type String::capacity() const { + if(isOnStack()) + return len; + return data.capacity; +} + +String String::substr(size_type pos, size_type cnt) && { + cnt = std::min(cnt, size() - pos); + char* cptr = c_str(); + memmove(cptr, cptr + pos, cnt); + setSize(cnt); + return std::move(*this); +} + +String String::substr(size_type pos, size_type cnt) const & { + cnt = std::min(cnt, size() - pos); + return String{ c_str() + pos, cnt }; +} + +String::size_type String::find(char ch, size_type pos) const { + const char* begin = c_str(); + const char* end = begin + size(); + const char* it = begin + pos; + for (; it < end && *it != ch; it++); + if (it < end) { return static_cast(it - begin); } + else { return npos; } +} + +String::size_type String::rfind(char ch, size_type pos) const { + const char* begin = c_str(); + const char* it = begin + std::min(pos, size() - 1); + for (; it >= begin && *it != ch; it--); + if (it >= begin) { return static_cast(it - begin); } + else { return npos; } +} + +int String::compare(const char* other, bool no_case) const { + if(no_case) + return doctest::stricmp(c_str(), other); + return std::strcmp(c_str(), other); +} + +int String::compare(const String& other, bool no_case) const { + return compare(other.c_str(), no_case); +} + +String operator+(const String& lhs, const String& rhs) { return String(lhs) += rhs; } + +bool operator==(const String& lhs, const String& rhs) { return lhs.compare(rhs) == 0; } +bool operator!=(const String& lhs, const String& rhs) { return lhs.compare(rhs) != 0; } +bool operator< (const String& lhs, const String& rhs) { return lhs.compare(rhs) < 0; } +bool operator> (const String& lhs, const String& rhs) { return lhs.compare(rhs) > 0; } +bool operator<=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) < 0 : true; } +bool operator>=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) > 0 : true; } + +std::ostream& operator<<(std::ostream& s, const String& in) { return s << in.c_str(); } + +Contains::Contains(const String& str) : string(str) { } + +bool Contains::checkWith(const String& other) const { + return strstr(other.c_str(), string.c_str()) != nullptr; +} + +String toString(const Contains& in) { + return "Contains( " + in.string + " )"; +} + +bool operator==(const String& lhs, const Contains& rhs) { return rhs.checkWith(lhs); } +bool operator==(const Contains& lhs, const String& rhs) { return lhs.checkWith(rhs); } +bool operator!=(const String& lhs, const Contains& rhs) { return !rhs.checkWith(lhs); } +bool operator!=(const Contains& lhs, const String& rhs) { return !lhs.checkWith(rhs); } + +namespace { + void color_to_stream(std::ostream&, Color::Enum) DOCTEST_BRANCH_ON_DISABLED({}, ;) +} // namespace + +namespace Color { + std::ostream& operator<<(std::ostream& s, Color::Enum code) { + color_to_stream(s, code); + return s; + } +} // namespace Color + +// clang-format off +const char* assertString(assertType::Enum at) { + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4061) // enum 'x' in switch of enum 'y' is not explicitly handled + #define DOCTEST_GENERATE_ASSERT_TYPE_CASE(assert_type) case assertType::DT_ ## assert_type: return #assert_type + #define DOCTEST_GENERATE_ASSERT_TYPE_CASES(assert_type) \ + DOCTEST_GENERATE_ASSERT_TYPE_CASE(WARN_ ## assert_type); \ + DOCTEST_GENERATE_ASSERT_TYPE_CASE(CHECK_ ## assert_type); \ + DOCTEST_GENERATE_ASSERT_TYPE_CASE(REQUIRE_ ## assert_type) + switch(at) { + DOCTEST_GENERATE_ASSERT_TYPE_CASE(WARN); + DOCTEST_GENERATE_ASSERT_TYPE_CASE(CHECK); + DOCTEST_GENERATE_ASSERT_TYPE_CASE(REQUIRE); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(FALSE); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS_AS); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS_WITH); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS_WITH_AS); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(NOTHROW); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(EQ); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(NE); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(GT); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(LT); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(GE); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(LE); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(UNARY); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(UNARY_FALSE); + + default: DOCTEST_INTERNAL_ERROR("Tried stringifying invalid assert type!"); + } + DOCTEST_MSVC_SUPPRESS_WARNING_POP +} +// clang-format on + +const char* failureString(assertType::Enum at) { + if(at & assertType::is_warn) //!OCLINT bitwise operator in conditional + return "WARNING"; + if(at & assertType::is_check) //!OCLINT bitwise operator in conditional + return "ERROR"; + if(at & assertType::is_require) //!OCLINT bitwise operator in conditional + return "FATAL ERROR"; + return ""; +} + +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference") +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference") +// depending on the current options this will remove the path of filenames +const char* skipPathFromFilename(const char* file) { +#ifndef DOCTEST_CONFIG_DISABLE + if(getContextOptions()->no_path_in_filenames) { + auto back = std::strrchr(file, '\\'); + auto forward = std::strrchr(file, '/'); + if(back || forward) { + if(back > forward) + forward = back; + return forward + 1; + } + } else { + const auto prefixes = getContextOptions()->strip_file_prefixes; + const char separator = DOCTEST_CONFIG_OPTIONS_FILE_PREFIX_SEPARATOR; + String::size_type longest_match = 0U; + for(String::size_type pos = 0U; pos < prefixes.size(); ++pos) + { + const auto prefix_start = pos; + pos = std::min(prefixes.find(separator, prefix_start), prefixes.size()); + + const auto prefix_size = pos - prefix_start; + if(prefix_size > longest_match) + { + // TODO under DOCTEST_MSVC: does the comparison need strnicmp() to work with drive letter capitalization? + if(0 == std::strncmp(prefixes.c_str() + prefix_start, file, prefix_size)) + { + longest_match = prefix_size; + } + } + } + return &file[longest_match]; + } +#endif // DOCTEST_CONFIG_DISABLE + return file; +} +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +bool SubcaseSignature::operator==(const SubcaseSignature& other) const { + return m_line == other.m_line + && std::strcmp(m_file, other.m_file) == 0 + && m_name == other.m_name; +} + +bool SubcaseSignature::operator<(const SubcaseSignature& other) const { + if(m_line != other.m_line) + return m_line < other.m_line; + if(std::strcmp(m_file, other.m_file) != 0) + return std::strcmp(m_file, other.m_file) < 0; + return m_name.compare(other.m_name) < 0; +} + +DOCTEST_DEFINE_INTERFACE(IContextScope) + +namespace detail { + void filldata::fill(std::ostream* stream, const void* in) { + if (in) { *stream << in; } + else { *stream << "nullptr"; } + } + + template + String toStreamLit(T t) { + std::ostream* os = tlssPush(); + os->operator<<(t); + return tlssPop(); + } +} + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +String toString(const char* in) { return String("\"") + (in ? in : "{null string}") + "\""; } +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 +String toString(const std::string& in) { return in.c_str(); } +#endif // VS 2019 + +String toString(String in) { return in; } + +String toString(std::nullptr_t) { return "nullptr"; } + +String toString(bool in) { return in ? "true" : "false"; } + +String toString(float in) { return toStreamLit(in); } +String toString(double in) { return toStreamLit(in); } +String toString(double long in) { return toStreamLit(in); } + +String toString(char in) { return toStreamLit(static_cast(in)); } +String toString(char signed in) { return toStreamLit(static_cast(in)); } +String toString(char unsigned in) { return toStreamLit(static_cast(in)); } +String toString(short in) { return toStreamLit(in); } +String toString(short unsigned in) { return toStreamLit(in); } +String toString(signed in) { return toStreamLit(in); } +String toString(unsigned in) { return toStreamLit(in); } +String toString(long in) { return toStreamLit(in); } +String toString(long unsigned in) { return toStreamLit(in); } +String toString(long long in) { return toStreamLit(in); } +String toString(long long unsigned in) { return toStreamLit(in); } + +Approx::Approx(double value) + : m_epsilon(static_cast(std::numeric_limits::epsilon()) * 100) + , m_scale(1.0) + , m_value(value) {} + +Approx Approx::operator()(double value) const { + Approx approx(value); + approx.epsilon(m_epsilon); + approx.scale(m_scale); + return approx; +} + +Approx& Approx::epsilon(double newEpsilon) { + m_epsilon = newEpsilon; + return *this; +} +Approx& Approx::scale(double newScale) { + m_scale = newScale; + return *this; +} + +bool operator==(double lhs, const Approx& rhs) { + // Thanks to Richard Harris for his help refining this formula + return std::fabs(lhs - rhs.m_value) < + rhs.m_epsilon * (rhs.m_scale + std::max(std::fabs(lhs), std::fabs(rhs.m_value))); +} +bool operator==(const Approx& lhs, double rhs) { return operator==(rhs, lhs); } +bool operator!=(double lhs, const Approx& rhs) { return !operator==(lhs, rhs); } +bool operator!=(const Approx& lhs, double rhs) { return !operator==(rhs, lhs); } +bool operator<=(double lhs, const Approx& rhs) { return lhs < rhs.m_value || lhs == rhs; } +bool operator<=(const Approx& lhs, double rhs) { return lhs.m_value < rhs || lhs == rhs; } +bool operator>=(double lhs, const Approx& rhs) { return lhs > rhs.m_value || lhs == rhs; } +bool operator>=(const Approx& lhs, double rhs) { return lhs.m_value > rhs || lhs == rhs; } +bool operator<(double lhs, const Approx& rhs) { return lhs < rhs.m_value && lhs != rhs; } +bool operator<(const Approx& lhs, double rhs) { return lhs.m_value < rhs && lhs != rhs; } +bool operator>(double lhs, const Approx& rhs) { return lhs > rhs.m_value && lhs != rhs; } +bool operator>(const Approx& lhs, double rhs) { return lhs.m_value > rhs && lhs != rhs; } + +String toString(const Approx& in) { + return "Approx( " + doctest::toString(in.m_value) + " )"; +} +const ContextOptions* getContextOptions() { return DOCTEST_BRANCH_ON_DISABLED(nullptr, g_cs); } + +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4738) +template +IsNaN::operator bool() const { + return std::isnan(value) ^ flipped; +} +DOCTEST_MSVC_SUPPRESS_WARNING_POP +template struct DOCTEST_INTERFACE_DEF IsNaN; +template struct DOCTEST_INTERFACE_DEF IsNaN; +template struct DOCTEST_INTERFACE_DEF IsNaN; +template +String toString(IsNaN in) { return String(in.flipped ? "! " : "") + "IsNaN( " + doctest::toString(in.value) + " )"; } +String toString(IsNaN in) { return toString(in); } +String toString(IsNaN in) { return toString(in); } +String toString(IsNaN in) { return toString(in); } + +} // namespace doctest + +#ifdef DOCTEST_CONFIG_DISABLE +namespace doctest { +Context::Context(int, const char* const*) {} +Context::~Context() = default; +void Context::applyCommandLine(int, const char* const*) {} +void Context::addFilter(const char*, const char*) {} +void Context::clearFilters() {} +void Context::setOption(const char*, bool) {} +void Context::setOption(const char*, int) {} +void Context::setOption(const char*, const char*) {} +bool Context::shouldExit() { return false; } +void Context::setAsDefaultForAssertsOutOfTestCases() {} +void Context::setAssertHandler(detail::assert_handler) {} +void Context::setCout(std::ostream*) {} +int Context::run() { return 0; } + +int IReporter::get_num_active_contexts() { return 0; } +const IContextScope* const* IReporter::get_active_contexts() { return nullptr; } +int IReporter::get_num_stringified_contexts() { return 0; } +const String* IReporter::get_stringified_contexts() { return nullptr; } + +int registerReporter(const char*, int, IReporter*) { return 0; } + +} // namespace doctest +#else // DOCTEST_CONFIG_DISABLE + +#if !defined(DOCTEST_CONFIG_COLORS_NONE) +#if !defined(DOCTEST_CONFIG_COLORS_WINDOWS) && !defined(DOCTEST_CONFIG_COLORS_ANSI) +#ifdef DOCTEST_PLATFORM_WINDOWS +#define DOCTEST_CONFIG_COLORS_WINDOWS +#else // linux +#define DOCTEST_CONFIG_COLORS_ANSI +#endif // platform +#endif // DOCTEST_CONFIG_COLORS_WINDOWS && DOCTEST_CONFIG_COLORS_ANSI +#endif // DOCTEST_CONFIG_COLORS_NONE + +namespace doctest_detail_test_suite_ns { +// holds the current test suite +doctest::detail::TestSuite& getCurrentTestSuite() { + static doctest::detail::TestSuite data{}; + return data; +} +} // namespace doctest_detail_test_suite_ns + +namespace doctest { +namespace { + // the int (priority) is part of the key for automatic sorting - sadly one can register a + // reporter with a duplicate name and a different priority but hopefully that won't happen often :| + using reporterMap = std::map, reporterCreatorFunc>; + + reporterMap& getReporters() { + static reporterMap data; + return data; + } + reporterMap& getListeners() { + static reporterMap data; + return data; + } +} // namespace +namespace detail { +#define DOCTEST_ITERATE_THROUGH_REPORTERS(function, ...) \ + for(auto& curr_rep : g_cs->reporters_currently_used) \ + curr_rep->function(__VA_ARGS__) + + bool checkIfShouldThrow(assertType::Enum at) { + if(at & assertType::is_require) //!OCLINT bitwise operator in conditional + return true; + + if((at & assertType::is_check) //!OCLINT bitwise operator in conditional + && getContextOptions()->abort_after > 0 && + (g_cs->numAssertsFailed + g_cs->numAssertsFailedCurrentTest_atomic) >= + getContextOptions()->abort_after) + return true; + + return false; + } + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + DOCTEST_NORETURN void throwException() { + g_cs->shouldLogCurrentException = false; + throw TestFailureException(); // NOLINT(hicpp-exception-baseclass) + } +#else // DOCTEST_CONFIG_NO_EXCEPTIONS + void throwException() {} +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +} // namespace detail + +namespace { + using namespace detail; + // matching of a string against a wildcard mask (case sensitivity configurable) taken from + // https://www.codeproject.com/Articles/1088/Wildcard-string-compare-globbing + int wildcmp(const char* str, const char* wild, bool caseSensitive) { + const char* cp = str; + const char* mp = wild; + + while((*str) && (*wild != '*')) { + if((caseSensitive ? (*wild != *str) : (tolower(*wild) != tolower(*str))) && + (*wild != '?')) { + return 0; + } + wild++; + str++; + } + + while(*str) { + if(*wild == '*') { + if(!*++wild) { + return 1; + } + mp = wild; + cp = str + 1; + } else if((caseSensitive ? (*wild == *str) : (tolower(*wild) == tolower(*str))) || + (*wild == '?')) { + wild++; + str++; + } else { + wild = mp; //!OCLINT parameter reassignment + str = cp++; //!OCLINT parameter reassignment + } + } + + while(*wild == '*') { + wild++; + } + return !*wild; + } + + // checks if the name matches any of the filters (and can be configured what to do when empty) + bool matchesAny(const char* name, const std::vector& filters, bool matchEmpty, + bool caseSensitive) { + if (filters.empty() && matchEmpty) + return true; + for (auto& curr : filters) + if (wildcmp(name, curr.c_str(), caseSensitive)) + return true; + return false; + } + + DOCTEST_NO_SANITIZE_INTEGER + unsigned long long hash(unsigned long long a, unsigned long long b) { + return (a << 5) + b; + } + + // C string hash function (djb2) - taken from http://www.cse.yorku.ca/~oz/hash.html + DOCTEST_NO_SANITIZE_INTEGER + unsigned long long hash(const char* str) { + unsigned long long hash = 5381; + char c; + while ((c = *str++)) + hash = ((hash << 5) + hash) + c; // hash * 33 + c + return hash; + } + + unsigned long long hash(const SubcaseSignature& sig) { + return hash(hash(hash(sig.m_file), hash(sig.m_name.c_str())), sig.m_line); + } + + unsigned long long hash(const std::vector& sigs, size_t count) { + unsigned long long running = 0; + auto end = sigs.begin() + count; + for (auto it = sigs.begin(); it != end; it++) { + running = hash(running, hash(*it)); + } + return running; + } + + unsigned long long hash(const std::vector& sigs) { + unsigned long long running = 0; + for (const SubcaseSignature& sig : sigs) { + running = hash(running, hash(sig)); + } + return running; + } +} // namespace +namespace detail { + bool Subcase::checkFilters() { + if (g_cs->subcaseStack.size() < size_t(g_cs->subcase_filter_levels)) { + if (!matchesAny(m_signature.m_name.c_str(), g_cs->filters[6], true, g_cs->case_sensitive)) + return true; + if (matchesAny(m_signature.m_name.c_str(), g_cs->filters[7], false, g_cs->case_sensitive)) + return true; + } + return false; + } + + Subcase::Subcase(const String& name, const char* file, int line) + : m_signature({name, file, line}) { + if (!g_cs->reachedLeaf) { + if (g_cs->nextSubcaseStack.size() <= g_cs->subcaseStack.size() + || g_cs->nextSubcaseStack[g_cs->subcaseStack.size()] == m_signature) { + // Going down. + if (checkFilters()) { return; } + + g_cs->subcaseStack.push_back(m_signature); + g_cs->currentSubcaseDepth++; + m_entered = true; + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_start, m_signature); + } + } else { + if (g_cs->subcaseStack[g_cs->currentSubcaseDepth] == m_signature) { + // This subcase is reentered via control flow. + g_cs->currentSubcaseDepth++; + m_entered = true; + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_start, m_signature); + } else if (g_cs->nextSubcaseStack.size() <= g_cs->currentSubcaseDepth + && g_cs->fullyTraversedSubcases.find(hash(hash(g_cs->subcaseStack, g_cs->currentSubcaseDepth), hash(m_signature))) + == g_cs->fullyTraversedSubcases.end()) { + if (checkFilters()) { return; } + // This subcase is part of the one to be executed next. + g_cs->nextSubcaseStack.clear(); + g_cs->nextSubcaseStack.insert(g_cs->nextSubcaseStack.end(), + g_cs->subcaseStack.begin(), g_cs->subcaseStack.begin() + g_cs->currentSubcaseDepth); + g_cs->nextSubcaseStack.push_back(m_signature); + } + } + } + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4996) // std::uncaught_exception is deprecated in C++17 + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + + Subcase::~Subcase() { + if (m_entered) { + g_cs->currentSubcaseDepth--; + + if (!g_cs->reachedLeaf) { + // Leaf. + g_cs->fullyTraversedSubcases.insert(hash(g_cs->subcaseStack)); + g_cs->nextSubcaseStack.clear(); + g_cs->reachedLeaf = true; + } else if (g_cs->nextSubcaseStack.empty()) { + // All children are finished. + g_cs->fullyTraversedSubcases.insert(hash(g_cs->subcaseStack)); + } + +#if defined(__cpp_lib_uncaught_exceptions) && __cpp_lib_uncaught_exceptions >= 201411L && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200) + if(std::uncaught_exceptions() > 0 +#else + if(std::uncaught_exception() +#endif + && g_cs->shouldLogCurrentException) { + DOCTEST_ITERATE_THROUGH_REPORTERS( + test_case_exception, {"exception thrown in subcase - will translate later " + "when the whole test case has been exited (cannot " + "translate while there is an active exception)", + false}); + g_cs->shouldLogCurrentException = false; + } + + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY); + } + } + + DOCTEST_CLANG_SUPPRESS_WARNING_POP + DOCTEST_GCC_SUPPRESS_WARNING_POP + DOCTEST_MSVC_SUPPRESS_WARNING_POP + + Subcase::operator bool() const { return m_entered; } + + Result::Result(bool passed, const String& decomposition) + : m_passed(passed) + , m_decomp(decomposition) {} + + ExpressionDecomposer::ExpressionDecomposer(assertType::Enum at) + : m_at(at) {} + + TestSuite& TestSuite::operator*(const char* in) { + m_test_suite = in; + return *this; + } + + TestCase::TestCase(funcType test, const char* file, unsigned line, const TestSuite& test_suite, + const String& type, int template_id) { + m_file = file; + m_line = line; + m_name = nullptr; // will be later overridden in operator* + m_test_suite = test_suite.m_test_suite; + m_description = test_suite.m_description; + m_skip = test_suite.m_skip; + m_no_breaks = test_suite.m_no_breaks; + m_no_output = test_suite.m_no_output; + m_may_fail = test_suite.m_may_fail; + m_should_fail = test_suite.m_should_fail; + m_expected_failures = test_suite.m_expected_failures; + m_timeout = test_suite.m_timeout; + + m_test = test; + m_type = type; + m_template_id = template_id; + } + + TestCase::TestCase(const TestCase& other) + : TestCaseData() { + *this = other; + } + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function + TestCase& TestCase::operator=(const TestCase& other) { + TestCaseData::operator=(other); + m_test = other.m_test; + m_type = other.m_type; + m_template_id = other.m_template_id; + m_full_name = other.m_full_name; + + if(m_template_id != -1) + m_name = m_full_name.c_str(); + return *this; + } + DOCTEST_MSVC_SUPPRESS_WARNING_POP + + TestCase& TestCase::operator*(const char* in) { + m_name = in; + // make a new name with an appended type for templated test case + if(m_template_id != -1) { + m_full_name = String(m_name) + "<" + m_type + ">"; + // redirect the name to point to the newly constructed full name + m_name = m_full_name.c_str(); + } + return *this; + } + + bool TestCase::operator<(const TestCase& other) const { + // this will be used only to differentiate between test cases - not relevant for sorting + if(m_line != other.m_line) + return m_line < other.m_line; + const int name_cmp = strcmp(m_name, other.m_name); + if(name_cmp != 0) + return name_cmp < 0; + const int file_cmp = m_file.compare(other.m_file); + if(file_cmp != 0) + return file_cmp < 0; + return m_template_id < other.m_template_id; + } + + // all the registered tests + std::set& getRegisteredTests() { + static std::set data; + return data; + } +} // namespace detail +namespace { + using namespace detail; + // for sorting tests by file/line + bool fileOrderComparator(const TestCase* lhs, const TestCase* rhs) { + // this is needed because MSVC gives different case for drive letters + // for __FILE__ when evaluated in a header and a source file + const int res = lhs->m_file.compare(rhs->m_file, bool(DOCTEST_MSVC)); + if(res != 0) + return res < 0; + if(lhs->m_line != rhs->m_line) + return lhs->m_line < rhs->m_line; + return lhs->m_template_id < rhs->m_template_id; + } + + // for sorting tests by suite/file/line + bool suiteOrderComparator(const TestCase* lhs, const TestCase* rhs) { + const int res = std::strcmp(lhs->m_test_suite, rhs->m_test_suite); + if(res != 0) + return res < 0; + return fileOrderComparator(lhs, rhs); + } + + // for sorting tests by name/suite/file/line + bool nameOrderComparator(const TestCase* lhs, const TestCase* rhs) { + const int res = std::strcmp(lhs->m_name, rhs->m_name); + if(res != 0) + return res < 0; + return suiteOrderComparator(lhs, rhs); + } + + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + void color_to_stream(std::ostream& s, Color::Enum code) { + static_cast(s); // for DOCTEST_CONFIG_COLORS_NONE or DOCTEST_CONFIG_COLORS_WINDOWS + static_cast(code); // for DOCTEST_CONFIG_COLORS_NONE +#ifdef DOCTEST_CONFIG_COLORS_ANSI + if(g_no_colors || + (isatty(STDOUT_FILENO) == false && getContextOptions()->force_colors == false)) + return; + + auto col = ""; + // clang-format off + switch(code) { //!OCLINT missing break in switch statement / unnecessary default statement in covered switch statement + case Color::Red: col = "[0;31m"; break; + case Color::Green: col = "[0;32m"; break; + case Color::Blue: col = "[0;34m"; break; + case Color::Cyan: col = "[0;36m"; break; + case Color::Yellow: col = "[0;33m"; break; + case Color::Grey: col = "[1;30m"; break; + case Color::LightGrey: col = "[0;37m"; break; + case Color::BrightRed: col = "[1;31m"; break; + case Color::BrightGreen: col = "[1;32m"; break; + case Color::BrightWhite: col = "[1;37m"; break; + case Color::Bright: // invalid + case Color::None: + case Color::White: + default: col = "[0m"; + } + // clang-format on + s << "\033" << col; +#endif // DOCTEST_CONFIG_COLORS_ANSI + +#ifdef DOCTEST_CONFIG_COLORS_WINDOWS + if(g_no_colors || + (_isatty(_fileno(stdout)) == false && getContextOptions()->force_colors == false)) + return; + + static struct ConsoleHelper { + HANDLE stdoutHandle; + WORD origFgAttrs; + WORD origBgAttrs; + + ConsoleHelper() { + stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); + CONSOLE_SCREEN_BUFFER_INFO csbiInfo; + GetConsoleScreenBufferInfo(stdoutHandle, &csbiInfo); + origFgAttrs = csbiInfo.wAttributes & ~(BACKGROUND_GREEN | BACKGROUND_RED | + BACKGROUND_BLUE | BACKGROUND_INTENSITY); + origBgAttrs = csbiInfo.wAttributes & ~(FOREGROUND_GREEN | FOREGROUND_RED | + FOREGROUND_BLUE | FOREGROUND_INTENSITY); + } + } ch; + +#define DOCTEST_SET_ATTR(x) SetConsoleTextAttribute(ch.stdoutHandle, x | ch.origBgAttrs) + + // clang-format off + switch (code) { + case Color::White: DOCTEST_SET_ATTR(FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break; + case Color::Red: DOCTEST_SET_ATTR(FOREGROUND_RED); break; + case Color::Green: DOCTEST_SET_ATTR(FOREGROUND_GREEN); break; + case Color::Blue: DOCTEST_SET_ATTR(FOREGROUND_BLUE); break; + case Color::Cyan: DOCTEST_SET_ATTR(FOREGROUND_BLUE | FOREGROUND_GREEN); break; + case Color::Yellow: DOCTEST_SET_ATTR(FOREGROUND_RED | FOREGROUND_GREEN); break; + case Color::Grey: DOCTEST_SET_ATTR(0); break; + case Color::LightGrey: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY); break; + case Color::BrightRed: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_RED); break; + case Color::BrightGreen: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN); break; + case Color::BrightWhite: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break; + case Color::None: + case Color::Bright: // invalid + default: DOCTEST_SET_ATTR(ch.origFgAttrs); + } + // clang-format on +#endif // DOCTEST_CONFIG_COLORS_WINDOWS + } + DOCTEST_CLANG_SUPPRESS_WARNING_POP + + std::vector& getExceptionTranslators() { + static std::vector data; + return data; + } + + String translateActiveException() { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + String res; + auto& translators = getExceptionTranslators(); + for(auto& curr : translators) + if(curr->translate(res)) + return res; + // clang-format off + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wcatch-value") + try { + throw; + } catch(std::exception& ex) { + return ex.what(); + } catch(std::string& msg) { + return msg.c_str(); + } catch(const char* msg) { + return msg; + } catch(...) { + return "unknown exception"; + } + DOCTEST_GCC_SUPPRESS_WARNING_POP +// clang-format on +#else // DOCTEST_CONFIG_NO_EXCEPTIONS + return ""; +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + } +} // namespace + +namespace detail { + // used by the macros for registering tests + int regTest(const TestCase& tc) { + getRegisteredTests().insert(tc); + return 0; + } + + // sets the current test suite + int setTestSuite(const TestSuite& ts) { + doctest_detail_test_suite_ns::getCurrentTestSuite() = ts; + return 0; + } + +#ifdef DOCTEST_IS_DEBUGGER_ACTIVE + bool isDebuggerActive() { return DOCTEST_IS_DEBUGGER_ACTIVE(); } +#else // DOCTEST_IS_DEBUGGER_ACTIVE +#ifdef DOCTEST_PLATFORM_LINUX + class ErrnoGuard { + public: + ErrnoGuard() : m_oldErrno(errno) {} + ~ErrnoGuard() { errno = m_oldErrno; } + private: + int m_oldErrno; + }; + // See the comments in Catch2 for the reasoning behind this implementation: + // https://github.com/catchorg/Catch2/blob/v2.13.1/include/internal/catch_debugger.cpp#L79-L102 + bool isDebuggerActive() { + ErrnoGuard guard; + std::ifstream in("/proc/self/status"); + for(std::string line; std::getline(in, line);) { + static const int PREFIX_LEN = 11; + if(line.compare(0, PREFIX_LEN, "TracerPid:\t") == 0) { + return line.length() > PREFIX_LEN && line[PREFIX_LEN] != '0'; + } + } + return false; + } +#elif defined(DOCTEST_PLATFORM_MAC) + // The following function is taken directly from the following technical note: + // https://developer.apple.com/library/archive/qa/qa1361/_index.html + // Returns true if the current process is being debugged (either + // running under the debugger or has a debugger attached post facto). + bool isDebuggerActive() { + int mib[4]; + kinfo_proc info; + size_t size; + // Initialize the flags so that, if sysctl fails for some bizarre + // reason, we get a predictable result. + info.kp_proc.p_flag = 0; + // Initialize mib, which tells sysctl the info we want, in this case + // we're looking for information about a specific process ID. + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = getpid(); + // Call sysctl. + size = sizeof(info); + if(sysctl(mib, DOCTEST_COUNTOF(mib), &info, &size, 0, 0) != 0) { + std::cerr << "\nCall to sysctl failed - unable to determine if debugger is active **\n"; + return false; + } + // We're being debugged if the P_TRACED flag is set. + return ((info.kp_proc.p_flag & P_TRACED) != 0); + } +#elif DOCTEST_MSVC || defined(__MINGW32__) || defined(__MINGW64__) + bool isDebuggerActive() { return ::IsDebuggerPresent() != 0; } +#else + bool isDebuggerActive() { return false; } +#endif // Platform +#endif // DOCTEST_IS_DEBUGGER_ACTIVE + + void registerExceptionTranslatorImpl(const IExceptionTranslator* et) { + if(std::find(getExceptionTranslators().begin(), getExceptionTranslators().end(), et) == + getExceptionTranslators().end()) + getExceptionTranslators().push_back(et); + } + + DOCTEST_THREAD_LOCAL std::vector g_infoContexts; // for logging with INFO() + + ContextScopeBase::ContextScopeBase() { + g_infoContexts.push_back(this); + } + + ContextScopeBase::ContextScopeBase(ContextScopeBase&& other) noexcept { + if (other.need_to_destroy) { + other.destroy(); + } + other.need_to_destroy = false; + g_infoContexts.push_back(this); + } + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4996) // std::uncaught_exception is deprecated in C++17 + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + + // destroy cannot be inlined into the destructor because that would mean calling stringify after + // ContextScope has been destroyed (base class destructors run after derived class destructors). + // Instead, ContextScope calls this method directly from its destructor. + void ContextScopeBase::destroy() { +#if defined(__cpp_lib_uncaught_exceptions) && __cpp_lib_uncaught_exceptions >= 201411L && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200) + if(std::uncaught_exceptions() > 0) { +#else + if(std::uncaught_exception()) { +#endif + std::ostringstream s; + this->stringify(&s); + g_cs->stringifiedContexts.push_back(s.str().c_str()); + } + g_infoContexts.pop_back(); + } + + DOCTEST_CLANG_SUPPRESS_WARNING_POP + DOCTEST_GCC_SUPPRESS_WARNING_POP + DOCTEST_MSVC_SUPPRESS_WARNING_POP +} // namespace detail +namespace { + using namespace detail; + +#if !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && !defined(DOCTEST_CONFIG_WINDOWS_SEH) + struct FatalConditionHandler + { + static void reset() {} + static void allocateAltStackMem() {} + static void freeAltStackMem() {} + }; +#else // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + + void reportFatal(const std::string&); + +#ifdef DOCTEST_PLATFORM_WINDOWS + + struct SignalDefs + { + DWORD id; + const char* name; + }; + // There is no 1-1 mapping between signals and windows exceptions. + // Windows can easily distinguish between SO and SigSegV, + // but SigInt, SigTerm, etc are handled differently. + SignalDefs signalDefs[] = { + {static_cast(EXCEPTION_ILLEGAL_INSTRUCTION), + "SIGILL - Illegal instruction signal"}, + {static_cast(EXCEPTION_STACK_OVERFLOW), "SIGSEGV - Stack overflow"}, + {static_cast(EXCEPTION_ACCESS_VIOLATION), + "SIGSEGV - Segmentation violation signal"}, + {static_cast(EXCEPTION_INT_DIVIDE_BY_ZERO), "Divide by zero error"}, + }; + + struct FatalConditionHandler + { + static LONG CALLBACK handleException(PEXCEPTION_POINTERS ExceptionInfo) { + // Multiple threads may enter this filter/handler at once. We want the error message to be printed on the + // console just once no matter how many threads have crashed. + DOCTEST_DECLARE_STATIC_MUTEX(mutex) + static bool execute = true; + { + DOCTEST_LOCK_MUTEX(mutex) + if(execute) { + bool reported = false; + for(size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + if(ExceptionInfo->ExceptionRecord->ExceptionCode == signalDefs[i].id) { + reportFatal(signalDefs[i].name); + reported = true; + break; + } + } + if(reported == false) + reportFatal("Unhandled SEH exception caught"); + if(isDebuggerActive() && !g_cs->no_breaks) + DOCTEST_BREAK_INTO_DEBUGGER(); + } + execute = false; + } + std::exit(EXIT_FAILURE); + } + + static void allocateAltStackMem() {} + static void freeAltStackMem() {} + + FatalConditionHandler() { + isSet = true; + // 32k seems enough for doctest to handle stack overflow, + // but the value was found experimentally, so there is no strong guarantee + guaranteeSize = 32 * 1024; + // Register an unhandled exception filter + previousTop = SetUnhandledExceptionFilter(handleException); + // Pass in guarantee size to be filled + SetThreadStackGuarantee(&guaranteeSize); + + // On Windows uncaught exceptions from another thread, exceptions from + // destructors, or calls to std::terminate are not a SEH exception + + // The terminal handler gets called when: + // - std::terminate is called FROM THE TEST RUNNER THREAD + // - an exception is thrown from a destructor FROM THE TEST RUNNER THREAD + original_terminate_handler = std::get_terminate(); + std::set_terminate([]() DOCTEST_NOEXCEPT { + reportFatal("Terminate handler called"); + if(isDebuggerActive() && !g_cs->no_breaks) + DOCTEST_BREAK_INTO_DEBUGGER(); + std::exit(EXIT_FAILURE); // explicitly exit - otherwise the SIGABRT handler may be called as well + }); + + // SIGABRT is raised when: + // - std::terminate is called FROM A DIFFERENT THREAD + // - an exception is thrown from a destructor FROM A DIFFERENT THREAD + // - an uncaught exception is thrown FROM A DIFFERENT THREAD + prev_sigabrt_handler = std::signal(SIGABRT, [](int signal) DOCTEST_NOEXCEPT { + if(signal == SIGABRT) { + reportFatal("SIGABRT - Abort (abnormal termination) signal"); + if(isDebuggerActive() && !g_cs->no_breaks) + DOCTEST_BREAK_INTO_DEBUGGER(); + std::exit(EXIT_FAILURE); + } + }); + + // The following settings are taken from google test, and more + // specifically from UnitTest::Run() inside of gtest.cc + + // the user does not want to see pop-up dialogs about crashes + prev_error_mode_1 = SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOALIGNMENTFAULTEXCEPT | + SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX); + // This forces the abort message to go to stderr in all circumstances. + prev_error_mode_2 = _set_error_mode(_OUT_TO_STDERR); + // In the debug version, Visual Studio pops up a separate dialog + // offering a choice to debug the aborted program - we want to disable that. + prev_abort_behavior = _set_abort_behavior(0x0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + // In debug mode, the Windows CRT can crash with an assertion over invalid + // input (e.g. passing an invalid file descriptor). The default handling + // for these assertions is to pop up a dialog and wait for user input. + // Instead ask the CRT to dump such assertions to stderr non-interactively. + prev_report_mode = _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG); + prev_report_file = _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR); + } + + static void reset() { + if(isSet) { + // Unregister handler and restore the old guarantee + SetUnhandledExceptionFilter(previousTop); + SetThreadStackGuarantee(&guaranteeSize); + std::set_terminate(original_terminate_handler); + std::signal(SIGABRT, prev_sigabrt_handler); + SetErrorMode(prev_error_mode_1); + _set_error_mode(prev_error_mode_2); + _set_abort_behavior(prev_abort_behavior, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + static_cast(_CrtSetReportMode(_CRT_ASSERT, prev_report_mode)); + static_cast(_CrtSetReportFile(_CRT_ASSERT, prev_report_file)); + isSet = false; + } + } + + ~FatalConditionHandler() { reset(); } + + private: + static UINT prev_error_mode_1; + static int prev_error_mode_2; + static unsigned int prev_abort_behavior; + static int prev_report_mode; + static _HFILE prev_report_file; + static void (DOCTEST_CDECL *prev_sigabrt_handler)(int); + static std::terminate_handler original_terminate_handler; + static bool isSet; + static ULONG guaranteeSize; + static LPTOP_LEVEL_EXCEPTION_FILTER previousTop; + }; + + UINT FatalConditionHandler::prev_error_mode_1; + int FatalConditionHandler::prev_error_mode_2; + unsigned int FatalConditionHandler::prev_abort_behavior; + int FatalConditionHandler::prev_report_mode; + _HFILE FatalConditionHandler::prev_report_file; + void (DOCTEST_CDECL *FatalConditionHandler::prev_sigabrt_handler)(int); + std::terminate_handler FatalConditionHandler::original_terminate_handler; + bool FatalConditionHandler::isSet = false; + ULONG FatalConditionHandler::guaranteeSize = 0; + LPTOP_LEVEL_EXCEPTION_FILTER FatalConditionHandler::previousTop = nullptr; + +#else // DOCTEST_PLATFORM_WINDOWS + + struct SignalDefs + { + int id; + const char* name; + }; + SignalDefs signalDefs[] = {{SIGINT, "SIGINT - Terminal interrupt signal"}, + {SIGILL, "SIGILL - Illegal instruction signal"}, + {SIGFPE, "SIGFPE - Floating point error signal"}, + {SIGSEGV, "SIGSEGV - Segmentation violation signal"}, + {SIGTERM, "SIGTERM - Termination request signal"}, + {SIGABRT, "SIGABRT - Abort (abnormal termination) signal"}}; + + struct FatalConditionHandler + { + static bool isSet; + static struct sigaction oldSigActions[DOCTEST_COUNTOF(signalDefs)]; + static stack_t oldSigStack; + static size_t altStackSize; + static char* altStackMem; + + static void handleSignal(int sig) { + const char* name = ""; + for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + SignalDefs& def = signalDefs[i]; + if(sig == def.id) { + name = def.name; + break; + } + } + reset(); + reportFatal(name); + raise(sig); + } + + static void allocateAltStackMem() { + altStackMem = new char[altStackSize]; + } + + static void freeAltStackMem() { + delete[] altStackMem; + } + + FatalConditionHandler() { + isSet = true; + stack_t sigStack; + sigStack.ss_sp = altStackMem; + sigStack.ss_size = altStackSize; + sigStack.ss_flags = 0; + sigaltstack(&sigStack, &oldSigStack); + struct sigaction sa = {}; + sa.sa_handler = handleSignal; + sa.sa_flags = SA_ONSTACK; + for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + sigaction(signalDefs[i].id, &sa, &oldSigActions[i]); + } + } + + ~FatalConditionHandler() { reset(); } + static void reset() { + if(isSet) { + // Set signals back to previous values -- hopefully nobody overwrote them in the meantime + for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + sigaction(signalDefs[i].id, &oldSigActions[i], nullptr); + } + // Return the old stack + sigaltstack(&oldSigStack, nullptr); + isSet = false; + } + } + }; + + bool FatalConditionHandler::isSet = false; + struct sigaction FatalConditionHandler::oldSigActions[DOCTEST_COUNTOF(signalDefs)] = {}; + stack_t FatalConditionHandler::oldSigStack = {}; + size_t FatalConditionHandler::altStackSize = 4 * SIGSTKSZ; + char* FatalConditionHandler::altStackMem = nullptr; + +#endif // DOCTEST_PLATFORM_WINDOWS +#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + +} // namespace + +namespace { + using namespace detail; + +#ifdef DOCTEST_PLATFORM_WINDOWS +#define DOCTEST_OUTPUT_DEBUG_STRING(text) ::OutputDebugStringA(text) +#else + // TODO: integration with XCode and other IDEs +#define DOCTEST_OUTPUT_DEBUG_STRING(text) +#endif // Platform + + void addAssert(assertType::Enum at) { + if((at & assertType::is_warn) == 0) //!OCLINT bitwise operator in conditional + g_cs->numAssertsCurrentTest_atomic++; + } + + void addFailedAssert(assertType::Enum at) { + if((at & assertType::is_warn) == 0) //!OCLINT bitwise operator in conditional + g_cs->numAssertsFailedCurrentTest_atomic++; + } + +#if defined(DOCTEST_CONFIG_POSIX_SIGNALS) || defined(DOCTEST_CONFIG_WINDOWS_SEH) + void reportFatal(const std::string& message) { + g_cs->failure_flags |= TestCaseFailureReason::Crash; + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception, {message.c_str(), true}); + + while (g_cs->subcaseStack.size()) { + g_cs->subcaseStack.pop_back(); + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY); + } + + g_cs->finalizeTestCaseData(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs); + } +#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH +} // namespace + +AssertData::AssertData(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type, const StringContains& exception_string) + : m_test_case(g_cs->currentTest), m_at(at), m_file(file), m_line(line), m_expr(expr), + m_failed(true), m_threw(false), m_threw_as(false), m_exception_type(exception_type), + m_exception_string(exception_string) { +#if DOCTEST_MSVC + if (m_expr[0] == ' ') // this happens when variadic macros are disabled under MSVC + ++m_expr; +#endif // MSVC +} + +namespace detail { + ResultBuilder::ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type, const String& exception_string) + : AssertData(at, file, line, expr, exception_type, exception_string) { } + + ResultBuilder::ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type, const Contains& exception_string) + : AssertData(at, file, line, expr, exception_type, exception_string) { } + + void ResultBuilder::setResult(const Result& res) { + m_decomp = res.m_decomp; + m_failed = !res.m_passed; + } + + void ResultBuilder::translateException() { + m_threw = true; + m_exception = translateActiveException(); + } + + bool ResultBuilder::log() { + if(m_at & assertType::is_throws) { //!OCLINT bitwise operator in conditional + m_failed = !m_threw; + } else if((m_at & assertType::is_throws_as) && (m_at & assertType::is_throws_with)) { //!OCLINT + m_failed = !m_threw_as || !m_exception_string.check(m_exception); + } else if(m_at & assertType::is_throws_as) { //!OCLINT bitwise operator in conditional + m_failed = !m_threw_as; + } else if(m_at & assertType::is_throws_with) { //!OCLINT bitwise operator in conditional + m_failed = !m_exception_string.check(m_exception); + } else if(m_at & assertType::is_nothrow) { //!OCLINT bitwise operator in conditional + m_failed = m_threw; + } + + if(m_exception.size()) + m_exception = "\"" + m_exception + "\""; + + if(is_running_in_test) { + addAssert(m_at); + DOCTEST_ITERATE_THROUGH_REPORTERS(log_assert, *this); + + if(m_failed) + addFailedAssert(m_at); + } else if(m_failed) { + failed_out_of_a_testing_context(*this); + } + + return m_failed && isDebuggerActive() && !getContextOptions()->no_breaks && + (g_cs->currentTest == nullptr || !g_cs->currentTest->m_no_breaks); // break into debugger + } + + void ResultBuilder::react() const { + if(m_failed && checkIfShouldThrow(m_at)) + throwException(); + } + + void failed_out_of_a_testing_context(const AssertData& ad) { + if(g_cs->ah) + g_cs->ah(ad); + else + std::abort(); + } + + bool decomp_assert(assertType::Enum at, const char* file, int line, const char* expr, + const Result& result) { + bool failed = !result.m_passed; + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS(result.m_decomp); + DOCTEST_ASSERT_IN_TESTS(result.m_decomp); + return !failed; + } + + MessageBuilder::MessageBuilder(const char* file, int line, assertType::Enum severity) { + m_stream = tlssPush(); + m_file = file; + m_line = line; + m_severity = severity; + } + + MessageBuilder::~MessageBuilder() { + if (!logged) + tlssPop(); + } + + DOCTEST_DEFINE_INTERFACE(IExceptionTranslator) + + bool MessageBuilder::log() { + if (!logged) { + m_string = tlssPop(); + logged = true; + } + + DOCTEST_ITERATE_THROUGH_REPORTERS(log_message, *this); + + const bool isWarn = m_severity & assertType::is_warn; + + // warn is just a message in this context so we don't treat it as an assert + if(!isWarn) { + addAssert(m_severity); + addFailedAssert(m_severity); + } + + return isDebuggerActive() && !getContextOptions()->no_breaks && !isWarn && + (g_cs->currentTest == nullptr || !g_cs->currentTest->m_no_breaks); // break into debugger + } + + void MessageBuilder::react() { + if(m_severity & assertType::is_require) //!OCLINT bitwise operator in conditional + throwException(); + } +} // namespace detail +namespace { + using namespace detail; + + // clang-format off + +// ================================================================================================= +// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.h/cpp +// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched. +// ================================================================================================= + + class XmlEncode { + public: + enum ForWhat { ForTextNodes, ForAttributes }; + + XmlEncode( std::string const& str, ForWhat forWhat = ForTextNodes ); + + void encodeTo( std::ostream& os ) const; + + friend std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ); + + private: + std::string m_str; + ForWhat m_forWhat; + }; + + class XmlWriter { + public: + + class ScopedElement { + public: + ScopedElement( XmlWriter* writer ); + + ScopedElement( ScopedElement&& other ) DOCTEST_NOEXCEPT; + ScopedElement& operator=( ScopedElement&& other ) DOCTEST_NOEXCEPT; + + ~ScopedElement(); + + ScopedElement& writeText( std::string const& text, bool indent = true ); + + template + ScopedElement& writeAttribute( std::string const& name, T const& attribute ) { + m_writer->writeAttribute( name, attribute ); + return *this; + } + + private: + mutable XmlWriter* m_writer = nullptr; + }; + +#ifndef DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + XmlWriter( std::ostream& os = std::cout ); +#else // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + XmlWriter( std::ostream& os ); +#endif // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + ~XmlWriter(); + + XmlWriter( XmlWriter const& ) = delete; + XmlWriter& operator=( XmlWriter const& ) = delete; + + XmlWriter& startElement( std::string const& name ); + + ScopedElement scopedElement( std::string const& name ); + + XmlWriter& endElement(); + + XmlWriter& writeAttribute( std::string const& name, std::string const& attribute ); + + XmlWriter& writeAttribute( std::string const& name, const char* attribute ); + + XmlWriter& writeAttribute( std::string const& name, bool attribute ); + + template + XmlWriter& writeAttribute( std::string const& name, T const& attribute ) { + std::stringstream rss; + rss << attribute; + return writeAttribute( name, rss.str() ); + } + + XmlWriter& writeText( std::string const& text, bool indent = true ); + + //XmlWriter& writeComment( std::string const& text ); + + //void writeStylesheetRef( std::string const& url ); + + //XmlWriter& writeBlankLine(); + + void ensureTagClosed(); + + void writeDeclaration(); + + private: + + void newlineIfNecessary(); + + bool m_tagIsOpen = false; + bool m_needsNewline = false; + std::vector m_tags; + std::string m_indent; + std::ostream& m_os; + }; + +// ================================================================================================= +// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.h/cpp +// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched. +// ================================================================================================= + +using uchar = unsigned char; + +namespace { + + size_t trailingBytes(unsigned char c) { + if ((c & 0xE0) == 0xC0) { + return 2; + } + if ((c & 0xF0) == 0xE0) { + return 3; + } + if ((c & 0xF8) == 0xF0) { + return 4; + } + DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered"); + } + + uint32_t headerValue(unsigned char c) { + if ((c & 0xE0) == 0xC0) { + return c & 0x1F; + } + if ((c & 0xF0) == 0xE0) { + return c & 0x0F; + } + if ((c & 0xF8) == 0xF0) { + return c & 0x07; + } + DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered"); + } + + void hexEscapeChar(std::ostream& os, unsigned char c) { + std::ios_base::fmtflags f(os.flags()); + os << "\\x" + << std::uppercase << std::hex << std::setfill('0') << std::setw(2) + << static_cast(c); + os.flags(f); + } + +} // anonymous namespace + + XmlEncode::XmlEncode( std::string const& str, ForWhat forWhat ) + : m_str( str ), + m_forWhat( forWhat ) + {} + + void XmlEncode::encodeTo( std::ostream& os ) const { + // Apostrophe escaping not necessary if we always use " to write attributes + // (see: https://www.w3.org/TR/xml/#syntax) + + for( std::size_t idx = 0; idx < m_str.size(); ++ idx ) { + uchar c = m_str[idx]; + switch (c) { + case '<': os << "<"; break; + case '&': os << "&"; break; + + case '>': + // See: https://www.w3.org/TR/xml/#syntax + if (idx > 2 && m_str[idx - 1] == ']' && m_str[idx - 2] == ']') + os << ">"; + else + os << c; + break; + + case '\"': + if (m_forWhat == ForAttributes) + os << """; + else + os << c; + break; + + default: + // Check for control characters and invalid utf-8 + + // Escape control characters in standard ascii + // see https://stackoverflow.com/questions/404107/why-are-control-characters-illegal-in-xml-1-0 + if (c < 0x09 || (c > 0x0D && c < 0x20) || c == 0x7F) { + hexEscapeChar(os, c); + break; + } + + // Plain ASCII: Write it to stream + if (c < 0x7F) { + os << c; + break; + } + + // UTF-8 territory + // Check if the encoding is valid and if it is not, hex escape bytes. + // Important: We do not check the exact decoded values for validity, only the encoding format + // First check that this bytes is a valid lead byte: + // This means that it is not encoded as 1111 1XXX + // Or as 10XX XXXX + if (c < 0xC0 || + c >= 0xF8) { + hexEscapeChar(os, c); + break; + } + + auto encBytes = trailingBytes(c); + // Are there enough bytes left to avoid accessing out-of-bounds memory? + if (idx + encBytes - 1 >= m_str.size()) { + hexEscapeChar(os, c); + break; + } + // The header is valid, check data + // The next encBytes bytes must together be a valid utf-8 + // This means: bitpattern 10XX XXXX and the extracted value is sane (ish) + bool valid = true; + uint32_t value = headerValue(c); + for (std::size_t n = 1; n < encBytes; ++n) { + uchar nc = m_str[idx + n]; + valid &= ((nc & 0xC0) == 0x80); + value = (value << 6) | (nc & 0x3F); + } + + if ( + // Wrong bit pattern of following bytes + (!valid) || + // Overlong encodings + (value < 0x80) || + ( value < 0x800 && encBytes > 2) || // removed "0x80 <= value &&" because redundant + (0x800 < value && value < 0x10000 && encBytes > 3) || + // Encoded value out of range + (value >= 0x110000) + ) { + hexEscapeChar(os, c); + break; + } + + // If we got here, this is in fact a valid(ish) utf-8 sequence + for (std::size_t n = 0; n < encBytes; ++n) { + os << m_str[idx + n]; + } + idx += encBytes - 1; + break; + } + } + } + + std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ) { + xmlEncode.encodeTo( os ); + return os; + } + + XmlWriter::ScopedElement::ScopedElement( XmlWriter* writer ) + : m_writer( writer ) + {} + + XmlWriter::ScopedElement::ScopedElement( ScopedElement&& other ) DOCTEST_NOEXCEPT + : m_writer( other.m_writer ){ + other.m_writer = nullptr; + } + XmlWriter::ScopedElement& XmlWriter::ScopedElement::operator=( ScopedElement&& other ) DOCTEST_NOEXCEPT { + if ( m_writer ) { + m_writer->endElement(); + } + m_writer = other.m_writer; + other.m_writer = nullptr; + return *this; + } + + + XmlWriter::ScopedElement::~ScopedElement() { + if( m_writer ) + m_writer->endElement(); + } + + XmlWriter::ScopedElement& XmlWriter::ScopedElement::writeText( std::string const& text, bool indent ) { + m_writer->writeText( text, indent ); + return *this; + } + + XmlWriter::XmlWriter( std::ostream& os ) : m_os( os ) + { + // writeDeclaration(); // called explicitly by the reporters that use the writer class - see issue #627 + } + + XmlWriter::~XmlWriter() { + while( !m_tags.empty() ) + endElement(); + } + + XmlWriter& XmlWriter::startElement( std::string const& name ) { + ensureTagClosed(); + newlineIfNecessary(); + m_os << m_indent << '<' << name; + m_tags.push_back( name ); + m_indent += " "; + m_tagIsOpen = true; + return *this; + } + + XmlWriter::ScopedElement XmlWriter::scopedElement( std::string const& name ) { + ScopedElement scoped( this ); + startElement( name ); + return scoped; + } + + XmlWriter& XmlWriter::endElement() { + newlineIfNecessary(); + m_indent = m_indent.substr( 0, m_indent.size()-2 ); + if( m_tagIsOpen ) { + m_os << "/>"; + m_tagIsOpen = false; + } + else { + m_os << m_indent << ""; + } + m_os << std::endl; + m_tags.pop_back(); + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, std::string const& attribute ) { + if( !name.empty() && !attribute.empty() ) + m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, const char* attribute ) { + if( !name.empty() && attribute && attribute[0] != '\0' ) + m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, bool attribute ) { + m_os << ' ' << name << "=\"" << ( attribute ? "true" : "false" ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeText( std::string const& text, bool indent ) { + if( !text.empty() ){ + bool tagWasOpen = m_tagIsOpen; + ensureTagClosed(); + if( tagWasOpen && indent ) + m_os << m_indent; + m_os << XmlEncode( text ); + m_needsNewline = true; + } + return *this; + } + + //XmlWriter& XmlWriter::writeComment( std::string const& text ) { + // ensureTagClosed(); + // m_os << m_indent << ""; + // m_needsNewline = true; + // return *this; + //} + + //void XmlWriter::writeStylesheetRef( std::string const& url ) { + // m_os << "\n"; + //} + + //XmlWriter& XmlWriter::writeBlankLine() { + // ensureTagClosed(); + // m_os << '\n'; + // return *this; + //} + + void XmlWriter::ensureTagClosed() { + if( m_tagIsOpen ) { + m_os << ">" << std::endl; + m_tagIsOpen = false; + } + } + + void XmlWriter::writeDeclaration() { + m_os << "\n"; + } + + void XmlWriter::newlineIfNecessary() { + if( m_needsNewline ) { + m_os << std::endl; + m_needsNewline = false; + } + } + +// ================================================================================================= +// End of copy-pasted code from Catch +// ================================================================================================= + + // clang-format on + + struct XmlReporter : public IReporter + { + XmlWriter xml; + DOCTEST_DECLARE_MUTEX(mutex) + + // caching pointers/references to objects of these types - safe to do + const ContextOptions& opt; + const TestCaseData* tc = nullptr; + + XmlReporter(const ContextOptions& co) + : xml(*co.cout) + , opt(co) {} + + void log_contexts() { + int num_contexts = get_num_active_contexts(); + if(num_contexts) { + auto contexts = get_active_contexts(); + std::stringstream ss; + for(int i = 0; i < num_contexts; ++i) { + contexts[i]->stringify(&ss); + xml.scopedElement("Info").writeText(ss.str()); + ss.str(""); + } + } + } + + unsigned line(unsigned l) const { return opt.no_line_numbers ? 0 : l; } + + void test_case_start_impl(const TestCaseData& in) { + bool open_ts_tag = false; + if(tc != nullptr) { // we have already opened a test suite + if(std::strcmp(tc->m_test_suite, in.m_test_suite) != 0) { + xml.endElement(); + open_ts_tag = true; + } + } + else { + open_ts_tag = true; // first test case ==> first test suite + } + + if(open_ts_tag) { + xml.startElement("TestSuite"); + xml.writeAttribute("name", in.m_test_suite); + } + + tc = ∈ + xml.startElement("TestCase") + .writeAttribute("name", in.m_name) + .writeAttribute("filename", skipPathFromFilename(in.m_file.c_str())) + .writeAttribute("line", line(in.m_line)) + .writeAttribute("description", in.m_description); + + if(Approx(in.m_timeout) != 0) + xml.writeAttribute("timeout", in.m_timeout); + if(in.m_may_fail) + xml.writeAttribute("may_fail", true); + if(in.m_should_fail) + xml.writeAttribute("should_fail", true); + } + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData& in) override { + test_run_start(); + if(opt.list_reporters) { + for(auto& curr : getListeners()) + xml.scopedElement("Listener") + .writeAttribute("priority", curr.first.first) + .writeAttribute("name", curr.first.second); + for(auto& curr : getReporters()) + xml.scopedElement("Reporter") + .writeAttribute("priority", curr.first.first) + .writeAttribute("name", curr.first.second); + } else if(opt.count || opt.list_test_cases) { + for(unsigned i = 0; i < in.num_data; ++i) { + xml.scopedElement("TestCase").writeAttribute("name", in.data[i]->m_name) + .writeAttribute("testsuite", in.data[i]->m_test_suite) + .writeAttribute("filename", skipPathFromFilename(in.data[i]->m_file.c_str())) + .writeAttribute("line", line(in.data[i]->m_line)) + .writeAttribute("skipped", in.data[i]->m_skip); + } + xml.scopedElement("OverallResultsTestCases") + .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters); + } else if(opt.list_test_suites) { + for(unsigned i = 0; i < in.num_data; ++i) + xml.scopedElement("TestSuite").writeAttribute("name", in.data[i]->m_test_suite); + xml.scopedElement("OverallResultsTestCases") + .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters); + xml.scopedElement("OverallResultsTestSuites") + .writeAttribute("unskipped", in.run_stats->numTestSuitesPassingFilters); + } + xml.endElement(); + } + + void test_run_start() override { + xml.writeDeclaration(); + + // remove .exe extension - mainly to have the same output on UNIX and Windows + std::string binary_name = skipPathFromFilename(opt.binary_name.c_str()); +#ifdef DOCTEST_PLATFORM_WINDOWS + if(binary_name.rfind(".exe") != std::string::npos) + binary_name = binary_name.substr(0, binary_name.length() - 4); +#endif // DOCTEST_PLATFORM_WINDOWS + + xml.startElement("doctest").writeAttribute("binary", binary_name); + if(opt.no_version == false) + xml.writeAttribute("version", DOCTEST_VERSION_STR); + + // only the consequential ones (TODO: filters) + xml.scopedElement("Options") + .writeAttribute("order_by", opt.order_by.c_str()) + .writeAttribute("rand_seed", opt.rand_seed) + .writeAttribute("first", opt.first) + .writeAttribute("last", opt.last) + .writeAttribute("abort_after", opt.abort_after) + .writeAttribute("subcase_filter_levels", opt.subcase_filter_levels) + .writeAttribute("case_sensitive", opt.case_sensitive) + .writeAttribute("no_throw", opt.no_throw) + .writeAttribute("no_skip", opt.no_skip); + } + + void test_run_end(const TestRunStats& p) override { + if(tc) // the TestSuite tag - only if there has been at least 1 test case + xml.endElement(); + + xml.scopedElement("OverallResultsAsserts") + .writeAttribute("successes", p.numAsserts - p.numAssertsFailed) + .writeAttribute("failures", p.numAssertsFailed); + + xml.startElement("OverallResultsTestCases") + .writeAttribute("successes", + p.numTestCasesPassingFilters - p.numTestCasesFailed) + .writeAttribute("failures", p.numTestCasesFailed); + if(opt.no_skipped_summary == false) + xml.writeAttribute("skipped", p.numTestCases - p.numTestCasesPassingFilters); + xml.endElement(); + + xml.endElement(); + } + + void test_case_start(const TestCaseData& in) override { + test_case_start_impl(in); + xml.ensureTagClosed(); + } + + void test_case_reenter(const TestCaseData&) override {} + + void test_case_end(const CurrentTestCaseStats& st) override { + xml.startElement("OverallResultsAsserts") + .writeAttribute("successes", + st.numAssertsCurrentTest - st.numAssertsFailedCurrentTest) + .writeAttribute("failures", st.numAssertsFailedCurrentTest) + .writeAttribute("test_case_success", st.testCaseSuccess); + if(opt.duration) + xml.writeAttribute("duration", st.seconds); + if(tc->m_expected_failures) + xml.writeAttribute("expected_failures", tc->m_expected_failures); + xml.endElement(); + + xml.endElement(); + } + + void test_case_exception(const TestCaseException& e) override { + DOCTEST_LOCK_MUTEX(mutex) + + xml.scopedElement("Exception") + .writeAttribute("crash", e.is_crash) + .writeText(e.error_string.c_str()); + } + + void subcase_start(const SubcaseSignature& in) override { + xml.startElement("SubCase") + .writeAttribute("name", in.m_name) + .writeAttribute("filename", skipPathFromFilename(in.m_file)) + .writeAttribute("line", line(in.m_line)); + xml.ensureTagClosed(); + } + + void subcase_end() override { xml.endElement(); } + + void log_assert(const AssertData& rb) override { + if(!rb.m_failed && !opt.success) + return; + + DOCTEST_LOCK_MUTEX(mutex) + + xml.startElement("Expression") + .writeAttribute("success", !rb.m_failed) + .writeAttribute("type", assertString(rb.m_at)) + .writeAttribute("filename", skipPathFromFilename(rb.m_file)) + .writeAttribute("line", line(rb.m_line)); + + xml.scopedElement("Original").writeText(rb.m_expr); + + if(rb.m_threw) + xml.scopedElement("Exception").writeText(rb.m_exception.c_str()); + + if(rb.m_at & assertType::is_throws_as) + xml.scopedElement("ExpectedException").writeText(rb.m_exception_type); + if(rb.m_at & assertType::is_throws_with) + xml.scopedElement("ExpectedExceptionString").writeText(rb.m_exception_string.c_str()); + if((rb.m_at & assertType::is_normal) && !rb.m_threw) + xml.scopedElement("Expanded").writeText(rb.m_decomp.c_str()); + + log_contexts(); + + xml.endElement(); + } + + void log_message(const MessageData& mb) override { + DOCTEST_LOCK_MUTEX(mutex) + + xml.startElement("Message") + .writeAttribute("type", failureString(mb.m_severity)) + .writeAttribute("filename", skipPathFromFilename(mb.m_file)) + .writeAttribute("line", line(mb.m_line)); + + xml.scopedElement("Text").writeText(mb.m_string.c_str()); + + log_contexts(); + + xml.endElement(); + } + + void test_case_skipped(const TestCaseData& in) override { + if(opt.no_skipped_summary == false) { + test_case_start_impl(in); + xml.writeAttribute("skipped", "true"); + xml.endElement(); + } + } + }; + + DOCTEST_REGISTER_REPORTER("xml", 0, XmlReporter); + + void fulltext_log_assert_to_stream(std::ostream& s, const AssertData& rb) { + if((rb.m_at & (assertType::is_throws_as | assertType::is_throws_with)) == + 0) //!OCLINT bitwise operator in conditional + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << " ) " + << Color::None; + + if(rb.m_at & assertType::is_throws) { //!OCLINT bitwise operator in conditional + s << (rb.m_threw ? "threw as expected!" : "did NOT throw at all!") << "\n"; + } else if((rb.m_at & assertType::is_throws_as) && + (rb.m_at & assertType::is_throws_with)) { //!OCLINT + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \"" + << rb.m_exception_string.c_str() + << "\", " << rb.m_exception_type << " ) " << Color::None; + if(rb.m_threw) { + if(!rb.m_failed) { + s << "threw as expected!\n"; + } else { + s << "threw a DIFFERENT exception! (contents: " << rb.m_exception << ")\n"; + } + } else { + s << "did NOT throw at all!\n"; + } + } else if(rb.m_at & + assertType::is_throws_as) { //!OCLINT bitwise operator in conditional + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", " + << rb.m_exception_type << " ) " << Color::None + << (rb.m_threw ? (rb.m_threw_as ? "threw as expected!" : + "threw a DIFFERENT exception: ") : + "did NOT throw at all!") + << Color::Cyan << rb.m_exception << "\n"; + } else if(rb.m_at & + assertType::is_throws_with) { //!OCLINT bitwise operator in conditional + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \"" + << rb.m_exception_string.c_str() + << "\" ) " << Color::None + << (rb.m_threw ? (!rb.m_failed ? "threw as expected!" : + "threw a DIFFERENT exception: ") : + "did NOT throw at all!") + << Color::Cyan << rb.m_exception << "\n"; + } else if(rb.m_at & assertType::is_nothrow) { //!OCLINT bitwise operator in conditional + s << (rb.m_threw ? "THREW exception: " : "didn't throw!") << Color::Cyan + << rb.m_exception << "\n"; + } else { + s << (rb.m_threw ? "THREW exception: " : + (!rb.m_failed ? "is correct!\n" : "is NOT correct!\n")); + if(rb.m_threw) + s << rb.m_exception << "\n"; + else + s << " values: " << assertString(rb.m_at) << "( " << rb.m_decomp << " )\n"; + } + } + + // TODO: + // - log_message() + // - respond to queries + // - honor remaining options + // - more attributes in tags + struct JUnitReporter : public IReporter + { + XmlWriter xml; + DOCTEST_DECLARE_MUTEX(mutex) + Timer timer; + std::vector deepestSubcaseStackNames; + + struct JUnitTestCaseData + { + static std::string getCurrentTimestamp() { + // Beware, this is not reentrant because of backward compatibility issues + // Also, UTC only, again because of backward compatibility (%z is C++11) + time_t rawtime; + std::time(&rawtime); + auto const timeStampSize = sizeof("2017-01-16T17:06:45Z"); + + std::tm timeInfo; +#ifdef DOCTEST_PLATFORM_WINDOWS + gmtime_s(&timeInfo, &rawtime); +#else // DOCTEST_PLATFORM_WINDOWS + gmtime_r(&rawtime, &timeInfo); +#endif // DOCTEST_PLATFORM_WINDOWS + + char timeStamp[timeStampSize]; + const char* const fmt = "%Y-%m-%dT%H:%M:%SZ"; + + std::strftime(timeStamp, timeStampSize, fmt, &timeInfo); + return std::string(timeStamp); + } + + struct JUnitTestMessage + { + JUnitTestMessage(const std::string& _message, const std::string& _type, const std::string& _details) + : message(_message), type(_type), details(_details) {} + + JUnitTestMessage(const std::string& _message, const std::string& _details) + : message(_message), type(), details(_details) {} + + std::string message, type, details; + }; + + struct JUnitTestCase + { + JUnitTestCase(const std::string& _classname, const std::string& _name) + : classname(_classname), name(_name), time(0), failures() {} + + std::string classname, name; + double time; + std::vector failures, errors; + }; + + void add(const std::string& classname, const std::string& name) { + testcases.emplace_back(classname, name); + } + + void appendSubcaseNamesToLastTestcase(std::vector nameStack) { + for(auto& curr: nameStack) + if(curr.size()) + testcases.back().name += std::string("/") + curr.c_str(); + } + + void addTime(double time) { + if(time < 1e-4) + time = 0; + testcases.back().time = time; + totalSeconds += time; + } + + void addFailure(const std::string& message, const std::string& type, const std::string& details) { + testcases.back().failures.emplace_back(message, type, details); + ++totalFailures; + } + + void addError(const std::string& message, const std::string& details) { + testcases.back().errors.emplace_back(message, details); + ++totalErrors; + } + + std::vector testcases; + double totalSeconds = 0; + int totalErrors = 0, totalFailures = 0; + }; + + JUnitTestCaseData testCaseData; + + // caching pointers/references to objects of these types - safe to do + const ContextOptions& opt; + const TestCaseData* tc = nullptr; + + JUnitReporter(const ContextOptions& co) + : xml(*co.cout) + , opt(co) {} + + unsigned line(unsigned l) const { return opt.no_line_numbers ? 0 : l; } + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData&) override { + xml.writeDeclaration(); + } + + void test_run_start() override { + xml.writeDeclaration(); + } + + void test_run_end(const TestRunStats& p) override { + // remove .exe extension - mainly to have the same output on UNIX and Windows + std::string binary_name = skipPathFromFilename(opt.binary_name.c_str()); +#ifdef DOCTEST_PLATFORM_WINDOWS + if(binary_name.rfind(".exe") != std::string::npos) + binary_name = binary_name.substr(0, binary_name.length() - 4); +#endif // DOCTEST_PLATFORM_WINDOWS + xml.startElement("testsuites"); + xml.startElement("testsuite").writeAttribute("name", binary_name) + .writeAttribute("errors", testCaseData.totalErrors) + .writeAttribute("failures", testCaseData.totalFailures) + .writeAttribute("tests", p.numAsserts); + if(opt.no_time_in_output == false) { + xml.writeAttribute("time", testCaseData.totalSeconds); + xml.writeAttribute("timestamp", JUnitTestCaseData::getCurrentTimestamp()); + } + if(opt.no_version == false) + xml.writeAttribute("doctest_version", DOCTEST_VERSION_STR); + + for(const auto& testCase : testCaseData.testcases) { + xml.startElement("testcase") + .writeAttribute("classname", testCase.classname) + .writeAttribute("name", testCase.name); + if(opt.no_time_in_output == false) + xml.writeAttribute("time", testCase.time); + // This is not ideal, but it should be enough to mimic gtest's junit output. + xml.writeAttribute("status", "run"); + + for(const auto& failure : testCase.failures) { + xml.scopedElement("failure") + .writeAttribute("message", failure.message) + .writeAttribute("type", failure.type) + .writeText(failure.details, false); + } + + for(const auto& error : testCase.errors) { + xml.scopedElement("error") + .writeAttribute("message", error.message) + .writeText(error.details); + } + + xml.endElement(); + } + xml.endElement(); + xml.endElement(); + } + + void test_case_start(const TestCaseData& in) override { + testCaseData.add(skipPathFromFilename(in.m_file.c_str()), in.m_name); + timer.start(); + } + + void test_case_reenter(const TestCaseData& in) override { + testCaseData.addTime(timer.getElapsedSeconds()); + testCaseData.appendSubcaseNamesToLastTestcase(deepestSubcaseStackNames); + deepestSubcaseStackNames.clear(); + + timer.start(); + testCaseData.add(skipPathFromFilename(in.m_file.c_str()), in.m_name); + } + + void test_case_end(const CurrentTestCaseStats&) override { + testCaseData.addTime(timer.getElapsedSeconds()); + testCaseData.appendSubcaseNamesToLastTestcase(deepestSubcaseStackNames); + deepestSubcaseStackNames.clear(); + } + + void test_case_exception(const TestCaseException& e) override { + DOCTEST_LOCK_MUTEX(mutex) + testCaseData.addError("exception", e.error_string.c_str()); + } + + void subcase_start(const SubcaseSignature& in) override { + deepestSubcaseStackNames.push_back(in.m_name); + } + + void subcase_end() override {} + + void log_assert(const AssertData& rb) override { + if(!rb.m_failed) // report only failures & ignore the `success` option + return; + + DOCTEST_LOCK_MUTEX(mutex) + + std::ostringstream os; + os << skipPathFromFilename(rb.m_file) << (opt.gnu_file_line ? ":" : "(") + << line(rb.m_line) << (opt.gnu_file_line ? ":" : "):") << std::endl; + + fulltext_log_assert_to_stream(os, rb); + log_contexts(os); + testCaseData.addFailure(rb.m_decomp.c_str(), assertString(rb.m_at), os.str()); + } + + void log_message(const MessageData& mb) override { + if(mb.m_severity & assertType::is_warn) // report only failures + return; + + DOCTEST_LOCK_MUTEX(mutex) + + std::ostringstream os; + os << skipPathFromFilename(mb.m_file) << (opt.gnu_file_line ? ":" : "(") + << line(mb.m_line) << (opt.gnu_file_line ? ":" : "):") << std::endl; + + os << mb.m_string.c_str() << "\n"; + log_contexts(os); + + testCaseData.addFailure(mb.m_string.c_str(), + mb.m_severity & assertType::is_check ? "FAIL_CHECK" : "FAIL", os.str()); + } + + void test_case_skipped(const TestCaseData&) override {} + + void log_contexts(std::ostringstream& s) { + int num_contexts = get_num_active_contexts(); + if(num_contexts) { + auto contexts = get_active_contexts(); + + s << " logged: "; + for(int i = 0; i < num_contexts; ++i) { + s << (i == 0 ? "" : " "); + contexts[i]->stringify(&s); + s << std::endl; + } + } + } + }; + + DOCTEST_REGISTER_REPORTER("junit", 0, JUnitReporter); + + struct Whitespace + { + int nrSpaces; + explicit Whitespace(int nr) + : nrSpaces(nr) {} + }; + + std::ostream& operator<<(std::ostream& out, const Whitespace& ws) { + if(ws.nrSpaces != 0) + out << std::setw(ws.nrSpaces) << ' '; + return out; + } + + struct ConsoleReporter : public IReporter + { + std::ostream& s; + bool hasLoggedCurrentTestStart; + std::vector subcasesStack; + size_t currentSubcaseLevel; + DOCTEST_DECLARE_MUTEX(mutex) + + // caching pointers/references to objects of these types - safe to do + const ContextOptions& opt; + const TestCaseData* tc; + + ConsoleReporter(const ContextOptions& co) + : s(*co.cout) + , opt(co) {} + + ConsoleReporter(const ContextOptions& co, std::ostream& ostr) + : s(ostr) + , opt(co) {} + + // ========================================================================================= + // WHAT FOLLOWS ARE HELPERS USED BY THE OVERRIDES OF THE VIRTUAL METHODS OF THE INTERFACE + // ========================================================================================= + + void separator_to_stream() { + s << Color::Yellow + << "===============================================================================" + "\n"; + } + + const char* getSuccessOrFailString(bool success, assertType::Enum at, + const char* success_str) { + if(success) + return success_str; + return failureString(at); + } + + Color::Enum getSuccessOrFailColor(bool success, assertType::Enum at) { + return success ? Color::BrightGreen : + (at & assertType::is_warn) ? Color::Yellow : Color::Red; + } + + void successOrFailColoredStringToStream(bool success, assertType::Enum at, + const char* success_str = "SUCCESS") { + s << getSuccessOrFailColor(success, at) + << getSuccessOrFailString(success, at, success_str) << ": "; + } + + void log_contexts() { + int num_contexts = get_num_active_contexts(); + if(num_contexts) { + auto contexts = get_active_contexts(); + + s << Color::None << " logged: "; + for(int i = 0; i < num_contexts; ++i) { + s << (i == 0 ? "" : " "); + contexts[i]->stringify(&s); + s << "\n"; + } + } + + s << "\n"; + } + + // this was requested to be made virtual so users could override it + virtual void file_line_to_stream(const char* file, int line, + const char* tail = "") { + s << Color::LightGrey << skipPathFromFilename(file) << (opt.gnu_file_line ? ":" : "(") + << (opt.no_line_numbers ? 0 : line) // 0 or the real num depending on the option + << (opt.gnu_file_line ? ":" : "):") << tail; + } + + void logTestStart() { + if(hasLoggedCurrentTestStart) + return; + + separator_to_stream(); + file_line_to_stream(tc->m_file.c_str(), tc->m_line, "\n"); + if(tc->m_description) + s << Color::Yellow << "DESCRIPTION: " << Color::None << tc->m_description << "\n"; + if(tc->m_test_suite && tc->m_test_suite[0] != '\0') + s << Color::Yellow << "TEST SUITE: " << Color::None << tc->m_test_suite << "\n"; + if(strncmp(tc->m_name, " Scenario:", 11) != 0) + s << Color::Yellow << "TEST CASE: "; + s << Color::None << tc->m_name << "\n"; + + for(size_t i = 0; i < currentSubcaseLevel; ++i) { + if(subcasesStack[i].m_name[0] != '\0') + s << " " << subcasesStack[i].m_name << "\n"; + } + + if(currentSubcaseLevel != subcasesStack.size()) { + s << Color::Yellow << "\nDEEPEST SUBCASE STACK REACHED (DIFFERENT FROM THE CURRENT ONE):\n" << Color::None; + for(size_t i = 0; i < subcasesStack.size(); ++i) { + if(subcasesStack[i].m_name[0] != '\0') + s << " " << subcasesStack[i].m_name << "\n"; + } + } + + s << "\n"; + + hasLoggedCurrentTestStart = true; + } + + void printVersion() { + if(opt.no_version == false) + s << Color::Cyan << "[doctest] " << Color::None << "doctest version is \"" + << DOCTEST_VERSION_STR << "\"\n"; + } + + void printIntro() { + if(opt.no_intro == false) { + printVersion(); + s << Color::Cyan << "[doctest] " << Color::None + << "run with \"--" DOCTEST_OPTIONS_PREFIX_DISPLAY "help\" for options\n"; + } + } + + void printHelp() { + int sizePrefixDisplay = static_cast(strlen(DOCTEST_OPTIONS_PREFIX_DISPLAY)); + printVersion(); + // clang-format off + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "boolean values: \"1/on/yes/true\" or \"0/off/no/false\"\n"; + s << Color::Cyan << "[doctest] " << Color::None; + s << "filter values: \"str1,str2,str3\" (comma separated strings)\n"; + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "filters use wildcards for matching strings\n"; + s << Color::Cyan << "[doctest] " << Color::None; + s << "something passes a filter if any of the strings in a filter matches\n"; +#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "ALL FLAGS, OPTIONS AND FILTERS ALSO AVAILABLE WITH A \"" DOCTEST_CONFIG_OPTIONS_PREFIX "\" PREFIX!!!\n"; +#endif + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "Query flags - the program quits after them. Available:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "?, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "help, -" DOCTEST_OPTIONS_PREFIX_DISPLAY "h " + << Whitespace(sizePrefixDisplay*0) << "prints this message\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "v, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "version " + << Whitespace(sizePrefixDisplay*1) << "prints the version\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "c, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "count " + << Whitespace(sizePrefixDisplay*1) << "prints the number of matching tests\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ltc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-cases " + << Whitespace(sizePrefixDisplay*1) << "lists all matching tests by name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-suites " + << Whitespace(sizePrefixDisplay*1) << "lists all matching test suites\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-reporters " + << Whitespace(sizePrefixDisplay*1) << "lists all registered reporters\n\n"; + // ================================================================================== << 79 + s << Color::Cyan << "[doctest] " << Color::None; + s << "The available / options/filters are:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case= " + << Whitespace(sizePrefixDisplay*1) << "filters tests by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file= " + << Whitespace(sizePrefixDisplay*1) << "filters tests by their file\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sfe, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their file\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite= " + << Whitespace(sizePrefixDisplay*1) << "filters tests by their test suite\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tse, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their test suite\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase= " + << Whitespace(sizePrefixDisplay*1) << "filters subcases by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT subcases by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "r, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "reporters= " + << Whitespace(sizePrefixDisplay*1) << "reporters to use (console is default)\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "o, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "out= " + << Whitespace(sizePrefixDisplay*1) << "output filename\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ob, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "order-by= " + << Whitespace(sizePrefixDisplay*1) << "how the tests should be ordered\n"; + s << Whitespace(sizePrefixDisplay*3) << " - [file/suite/name/rand/none]\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "rs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "rand-seed= " + << Whitespace(sizePrefixDisplay*1) << "seed for random ordering\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "f, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "first= " + << Whitespace(sizePrefixDisplay*1) << "the first test passing the filters to\n"; + s << Whitespace(sizePrefixDisplay*3) << " execute - for range-based execution\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "l, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "last= " + << Whitespace(sizePrefixDisplay*1) << "the last test passing the filters to\n"; + s << Whitespace(sizePrefixDisplay*3) << " execute - for range-based execution\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "aa, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "abort-after= " + << Whitespace(sizePrefixDisplay*1) << "stop after failed assertions\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "scfl,--" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-filter-levels= " + << Whitespace(sizePrefixDisplay*1) << "apply filters for the first levels\n"; + s << Color::Cyan << "\n[doctest] " << Color::None; + s << "Bool options - can be used like flags and true is assumed. Available:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "s, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "success= " + << Whitespace(sizePrefixDisplay*1) << "include successful assertions in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "cs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "case-sensitive= " + << Whitespace(sizePrefixDisplay*1) << "filters being treated as case sensitive\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "e, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "exit= " + << Whitespace(sizePrefixDisplay*1) << "exits after the tests finish\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "d, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "duration= " + << Whitespace(sizePrefixDisplay*1) << "prints the time duration of each test\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "m, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "minimal= " + << Whitespace(sizePrefixDisplay*1) << "minimal console output (only failures)\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "q, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "quiet= " + << Whitespace(sizePrefixDisplay*1) << "no console output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nt, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-throw= " + << Whitespace(sizePrefixDisplay*1) << "skips exceptions-related assert checks\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ne, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-exitcode= " + << Whitespace(sizePrefixDisplay*1) << "returns (or exits) always with success\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-run= " + << Whitespace(sizePrefixDisplay*1) << "skips all runtime doctest operations\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ni, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-intro= " + << Whitespace(sizePrefixDisplay*1) << "omit the framework intro in the output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nv, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-version= " + << Whitespace(sizePrefixDisplay*1) << "omit the framework version in the output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-colors= " + << Whitespace(sizePrefixDisplay*1) << "disables colors in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "fc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "force-colors= " + << Whitespace(sizePrefixDisplay*1) << "use colors even when not in a tty\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nb, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-breaks= " + << Whitespace(sizePrefixDisplay*1) << "disables breakpoints in debuggers\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ns, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-skip= " + << Whitespace(sizePrefixDisplay*1) << "don't skip test cases marked as skip\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "gfl, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "gnu-file-line= " + << Whitespace(sizePrefixDisplay*1) << ":n: vs (n): for line numbers in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "npf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-path-filenames= " + << Whitespace(sizePrefixDisplay*1) << "only filenames and no paths in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "spp, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "skip-path-prefixes= " + << Whitespace(sizePrefixDisplay*1) << "whenever file paths start with this prefix, remove it from the output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nln, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-line-numbers= " + << Whitespace(sizePrefixDisplay*1) << "0 instead of real line numbers in output\n"; + // ================================================================================== << 79 + // clang-format on + + s << Color::Cyan << "\n[doctest] " << Color::None; + s << "for more information visit the project documentation\n\n"; + } + + void printRegisteredReporters() { + printVersion(); + auto printReporters = [this] (const reporterMap& reporters, const char* type) { + if(reporters.size()) { + s << Color::Cyan << "[doctest] " << Color::None << "listing all registered " << type << "\n"; + for(auto& curr : reporters) + s << "priority: " << std::setw(5) << curr.first.first + << " name: " << curr.first.second << "\n"; + } + }; + printReporters(getListeners(), "listeners"); + printReporters(getReporters(), "reporters"); + } + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData& in) override { + if(opt.version) { + printVersion(); + } else if(opt.help) { + printHelp(); + } else if(opt.list_reporters) { + printRegisteredReporters(); + } else if(opt.count || opt.list_test_cases) { + if(opt.list_test_cases) { + s << Color::Cyan << "[doctest] " << Color::None + << "listing all test case names\n"; + separator_to_stream(); + } + + for(unsigned i = 0; i < in.num_data; ++i) + s << Color::None << in.data[i]->m_name << "\n"; + + separator_to_stream(); + + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " + << g_cs->numTestCasesPassingFilters << "\n"; + + } else if(opt.list_test_suites) { + s << Color::Cyan << "[doctest] " << Color::None << "listing all test suites\n"; + separator_to_stream(); + + for(unsigned i = 0; i < in.num_data; ++i) + s << Color::None << in.data[i]->m_test_suite << "\n"; + + separator_to_stream(); + + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " + << g_cs->numTestCasesPassingFilters << "\n"; + s << Color::Cyan << "[doctest] " << Color::None + << "test suites with unskipped test cases passing the current filters: " + << g_cs->numTestSuitesPassingFilters << "\n"; + } + } + + void test_run_start() override { + if(!opt.minimal) + printIntro(); + } + + void test_run_end(const TestRunStats& p) override { + if(opt.minimal && p.numTestCasesFailed == 0) + return; + + separator_to_stream(); + s << std::dec; + + auto totwidth = int(std::ceil(log10(static_cast(std::max(p.numTestCasesPassingFilters, static_cast(p.numAsserts))) + 1))); + auto passwidth = int(std::ceil(log10(static_cast(std::max(p.numTestCasesPassingFilters - p.numTestCasesFailed, static_cast(p.numAsserts - p.numAssertsFailed))) + 1))); + auto failwidth = int(std::ceil(log10(static_cast(std::max(p.numTestCasesFailed, static_cast(p.numAssertsFailed))) + 1))); + const bool anythingFailed = p.numTestCasesFailed > 0 || p.numAssertsFailed > 0; + s << Color::Cyan << "[doctest] " << Color::None << "test cases: " << std::setw(totwidth) + << p.numTestCasesPassingFilters << " | " + << ((p.numTestCasesPassingFilters == 0 || anythingFailed) ? Color::None : + Color::Green) + << std::setw(passwidth) << p.numTestCasesPassingFilters - p.numTestCasesFailed << " passed" + << Color::None << " | " << (p.numTestCasesFailed > 0 ? Color::Red : Color::None) + << std::setw(failwidth) << p.numTestCasesFailed << " failed" << Color::None << " |"; + if(opt.no_skipped_summary == false) { + const int numSkipped = p.numTestCases - p.numTestCasesPassingFilters; + s << " " << (numSkipped == 0 ? Color::None : Color::Yellow) << numSkipped + << " skipped" << Color::None; + } + s << "\n"; + s << Color::Cyan << "[doctest] " << Color::None << "assertions: " << std::setw(totwidth) + << p.numAsserts << " | " + << ((p.numAsserts == 0 || anythingFailed) ? Color::None : Color::Green) + << std::setw(passwidth) << (p.numAsserts - p.numAssertsFailed) << " passed" << Color::None + << " | " << (p.numAssertsFailed > 0 ? Color::Red : Color::None) << std::setw(failwidth) + << p.numAssertsFailed << " failed" << Color::None << " |\n"; + s << Color::Cyan << "[doctest] " << Color::None + << "Status: " << (p.numTestCasesFailed > 0 ? Color::Red : Color::Green) + << ((p.numTestCasesFailed > 0) ? "FAILURE!" : "SUCCESS!") << Color::None << std::endl; + } + + void test_case_start(const TestCaseData& in) override { + hasLoggedCurrentTestStart = false; + tc = ∈ + subcasesStack.clear(); + currentSubcaseLevel = 0; + } + + void test_case_reenter(const TestCaseData&) override { + subcasesStack.clear(); + } + + void test_case_end(const CurrentTestCaseStats& st) override { + if(tc->m_no_output) + return; + + // log the preamble of the test case only if there is something + // else to print - something other than that an assert has failed + if(opt.duration || + (st.failure_flags && st.failure_flags != static_cast(TestCaseFailureReason::AssertFailure))) + logTestStart(); + + if(opt.duration) + s << Color::None << std::setprecision(6) << std::fixed << st.seconds + << " s: " << tc->m_name << "\n"; + + if(st.failure_flags & TestCaseFailureReason::Timeout) + s << Color::Red << "Test case exceeded time limit of " << std::setprecision(6) + << std::fixed << tc->m_timeout << "!\n"; + + if(st.failure_flags & TestCaseFailureReason::ShouldHaveFailedButDidnt) { + s << Color::Red << "Should have failed but didn't! Marking it as failed!\n"; + } else if(st.failure_flags & TestCaseFailureReason::ShouldHaveFailedAndDid) { + s << Color::Yellow << "Failed as expected so marking it as not failed\n"; + } else if(st.failure_flags & TestCaseFailureReason::CouldHaveFailedAndDid) { + s << Color::Yellow << "Allowed to fail so marking it as not failed\n"; + } else if(st.failure_flags & TestCaseFailureReason::DidntFailExactlyNumTimes) { + s << Color::Red << "Didn't fail exactly " << tc->m_expected_failures + << " times so marking it as failed!\n"; + } else if(st.failure_flags & TestCaseFailureReason::FailedExactlyNumTimes) { + s << Color::Yellow << "Failed exactly " << tc->m_expected_failures + << " times as expected so marking it as not failed!\n"; + } + if(st.failure_flags & TestCaseFailureReason::TooManyFailedAsserts) { + s << Color::Red << "Aborting - too many failed asserts!\n"; + } + s << Color::None; // lgtm [cpp/useless-expression] + } + + void test_case_exception(const TestCaseException& e) override { + DOCTEST_LOCK_MUTEX(mutex) + if(tc->m_no_output) + return; + + logTestStart(); + + file_line_to_stream(tc->m_file.c_str(), tc->m_line, " "); + successOrFailColoredStringToStream(false, e.is_crash ? assertType::is_require : + assertType::is_check); + s << Color::Red << (e.is_crash ? "test case CRASHED: " : "test case THREW exception: ") + << Color::Cyan << e.error_string << "\n"; + + int num_stringified_contexts = get_num_stringified_contexts(); + if(num_stringified_contexts) { + auto stringified_contexts = get_stringified_contexts(); + s << Color::None << " logged: "; + for(int i = num_stringified_contexts; i > 0; --i) { + s << (i == num_stringified_contexts ? "" : " ") + << stringified_contexts[i - 1] << "\n"; + } + } + s << "\n" << Color::None; + } + + void subcase_start(const SubcaseSignature& subc) override { + subcasesStack.push_back(subc); + ++currentSubcaseLevel; + hasLoggedCurrentTestStart = false; + } + + void subcase_end() override { + --currentSubcaseLevel; + hasLoggedCurrentTestStart = false; + } + + void log_assert(const AssertData& rb) override { + if((!rb.m_failed && !opt.success) || tc->m_no_output) + return; + + DOCTEST_LOCK_MUTEX(mutex) + + logTestStart(); + + file_line_to_stream(rb.m_file, rb.m_line, " "); + successOrFailColoredStringToStream(!rb.m_failed, rb.m_at); + + fulltext_log_assert_to_stream(s, rb); + + log_contexts(); + } + + void log_message(const MessageData& mb) override { + if(tc->m_no_output) + return; + + DOCTEST_LOCK_MUTEX(mutex) + + logTestStart(); + + file_line_to_stream(mb.m_file, mb.m_line, " "); + s << getSuccessOrFailColor(false, mb.m_severity) + << getSuccessOrFailString(mb.m_severity & assertType::is_warn, mb.m_severity, + "MESSAGE") << ": "; + s << Color::None << mb.m_string << "\n"; + log_contexts(); + } + + void test_case_skipped(const TestCaseData&) override {} + }; + + DOCTEST_REGISTER_REPORTER("console", 0, ConsoleReporter); + +#ifdef DOCTEST_PLATFORM_WINDOWS + struct DebugOutputWindowReporter : public ConsoleReporter + { + DOCTEST_THREAD_LOCAL static std::ostringstream oss; + + DebugOutputWindowReporter(const ContextOptions& co) + : ConsoleReporter(co, oss) {} + +#define DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(func, type, arg) \ + void func(type arg) override { \ + bool with_col = g_no_colors; \ + g_no_colors = false; \ + ConsoleReporter::func(arg); \ + if(oss.tellp() != std::streampos{}) { \ + DOCTEST_OUTPUT_DEBUG_STRING(oss.str().c_str()); \ + oss.str(""); \ + } \ + g_no_colors = with_col; \ + } + + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_start, DOCTEST_EMPTY, DOCTEST_EMPTY) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_end, const TestRunStats&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_start, const TestCaseData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_reenter, const TestCaseData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_end, const CurrentTestCaseStats&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_exception, const TestCaseException&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_start, const SubcaseSignature&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_end, DOCTEST_EMPTY, DOCTEST_EMPTY) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_assert, const AssertData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_message, const MessageData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_skipped, const TestCaseData&, in) + }; + + DOCTEST_THREAD_LOCAL std::ostringstream DebugOutputWindowReporter::oss; +#endif // DOCTEST_PLATFORM_WINDOWS + + // the implementation of parseOption() + bool parseOptionImpl(int argc, const char* const* argv, const char* pattern, String* value) { + // going from the end to the beginning and stopping on the first occurrence from the end + for(int i = argc; i > 0; --i) { + auto index = i - 1; + auto temp = std::strstr(argv[index], pattern); + if(temp && (value || strlen(temp) == strlen(pattern))) { //!OCLINT prefer early exits and continue + // eliminate matches in which the chars before the option are not '-' + bool noBadCharsFound = true; + auto curr = argv[index]; + while(curr != temp) { + if(*curr++ != '-') { + noBadCharsFound = false; + break; + } + } + if(noBadCharsFound && argv[index][0] == '-') { + if(value) { + // parsing the value of an option + temp += strlen(pattern); + const unsigned len = strlen(temp); + if(len) { + *value = temp; + return true; + } + } else { + // just a flag - no value + return true; + } + } + } + } + return false; + } + + // parses an option and returns the string after the '=' character + bool parseOption(int argc, const char* const* argv, const char* pattern, String* value = nullptr, + const String& defaultVal = String()) { + if(value) + *value = defaultVal; +#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + // offset (normally 3 for "dt-") to skip prefix + if(parseOptionImpl(argc, argv, pattern + strlen(DOCTEST_CONFIG_OPTIONS_PREFIX), value)) + return true; +#endif // DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + return parseOptionImpl(argc, argv, pattern, value); + } + + // locates a flag on the command line + bool parseFlag(int argc, const char* const* argv, const char* pattern) { + return parseOption(argc, argv, pattern); + } + + // parses a comma separated list of words after a pattern in one of the arguments in argv + bool parseCommaSepArgs(int argc, const char* const* argv, const char* pattern, + std::vector& res) { + String filtersString; + if(parseOption(argc, argv, pattern, &filtersString)) { + // tokenize with "," as a separator, unless escaped with backslash + std::ostringstream s; + auto flush = [&s, &res]() { + auto string = s.str(); + if(string.size() > 0) { + res.push_back(string.c_str()); + } + s.str(""); + }; + + bool seenBackslash = false; + const char* current = filtersString.c_str(); + const char* end = current + strlen(current); + while(current != end) { + char character = *current++; + if(seenBackslash) { + seenBackslash = false; + if(character == ',' || character == '\\') { + s.put(character); + continue; + } + s.put('\\'); + } + if(character == '\\') { + seenBackslash = true; + } else if(character == ',') { + flush(); + } else { + s.put(character); + } + } + + if(seenBackslash) { + s.put('\\'); + } + flush(); + return true; + } + return false; + } + + enum optionType + { + option_bool, + option_int + }; + + // parses an int/bool option from the command line + bool parseIntOption(int argc, const char* const* argv, const char* pattern, optionType type, + int& res) { + String parsedValue; + if(!parseOption(argc, argv, pattern, &parsedValue)) + return false; + + if(type) { + // integer + // TODO: change this to use std::stoi or something else! currently it uses undefined behavior - assumes '0' on failed parse... + int theInt = std::atoi(parsedValue.c_str()); + if (theInt != 0) { + res = theInt; //!OCLINT parameter reassignment + return true; + } + } else { + // boolean + const char positive[][5] = { "1", "true", "on", "yes" }; // 5 - strlen("true") + 1 + const char negative[][6] = { "0", "false", "off", "no" }; // 6 - strlen("false") + 1 + + // if the value matches any of the positive/negative possibilities + for (unsigned i = 0; i < 4; i++) { + if (parsedValue.compare(positive[i], true) == 0) { + res = 1; //!OCLINT parameter reassignment + return true; + } + if (parsedValue.compare(negative[i], true) == 0) { + res = 0; //!OCLINT parameter reassignment + return true; + } + } + } + return false; + } +} // namespace + +Context::Context(int argc, const char* const* argv) + : p(new detail::ContextState) { + parseArgs(argc, argv, true); + if(argc) + p->binary_name = argv[0]; +} + +Context::~Context() { + if(g_cs == p) + g_cs = nullptr; + delete p; +} + +void Context::applyCommandLine(int argc, const char* const* argv) { + parseArgs(argc, argv); + if(argc) + p->binary_name = argv[0]; +} + +// parses args +void Context::parseArgs(int argc, const char* const* argv, bool withDefaults) { + using namespace detail; + + // clang-format off + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file=", p->filters[0]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sf=", p->filters[0]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file-exclude=",p->filters[1]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sfe=", p->filters[1]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite=", p->filters[2]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ts=", p->filters[2]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite-exclude=", p->filters[3]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tse=", p->filters[3]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case=", p->filters[4]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tc=", p->filters[4]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case-exclude=", p->filters[5]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tce=", p->filters[5]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase=", p->filters[6]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sc=", p->filters[6]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase-exclude=", p->filters[7]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sce=", p->filters[7]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "reporters=", p->filters[8]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "r=", p->filters[8]); + // clang-format on + + int intRes = 0; + String strRes; + +#define DOCTEST_PARSE_AS_BOOL_OR_FLAG(name, sname, var, default) \ + if(parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_bool, intRes) || \ + parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_bool, intRes)) \ + p->var = static_cast(intRes); \ + else if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name) || \ + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname)) \ + p->var = true; \ + else if(withDefaults) \ + p->var = default + +#define DOCTEST_PARSE_INT_OPTION(name, sname, var, default) \ + if(parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_int, intRes) || \ + parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_int, intRes)) \ + p->var = intRes; \ + else if(withDefaults) \ + p->var = default + +#define DOCTEST_PARSE_STR_OPTION(name, sname, var, default) \ + if(parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", &strRes, default) || \ + parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", &strRes, default) || \ + withDefaults) \ + p->var = strRes + + // clang-format off + DOCTEST_PARSE_STR_OPTION("out", "o", out, ""); + DOCTEST_PARSE_STR_OPTION("order-by", "ob", order_by, "file"); + DOCTEST_PARSE_INT_OPTION("rand-seed", "rs", rand_seed, 0); + + DOCTEST_PARSE_INT_OPTION("first", "f", first, 0); + DOCTEST_PARSE_INT_OPTION("last", "l", last, UINT_MAX); + + DOCTEST_PARSE_INT_OPTION("abort-after", "aa", abort_after, 0); + DOCTEST_PARSE_INT_OPTION("subcase-filter-levels", "scfl", subcase_filter_levels, INT_MAX); + + DOCTEST_PARSE_AS_BOOL_OR_FLAG("success", "s", success, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("case-sensitive", "cs", case_sensitive, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("exit", "e", exit, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("duration", "d", duration, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("minimal", "m", minimal, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("quiet", "q", quiet, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-throw", "nt", no_throw, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-exitcode", "ne", no_exitcode, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-run", "nr", no_run, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-intro", "ni", no_intro, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-version", "nv", no_version, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-colors", "nc", no_colors, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("force-colors", "fc", force_colors, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-breaks", "nb", no_breaks, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skip", "ns", no_skip, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("gnu-file-line", "gfl", gnu_file_line, !bool(DOCTEST_MSVC)); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-path-filenames", "npf", no_path_in_filenames, false); + DOCTEST_PARSE_STR_OPTION("strip-file-prefixes", "sfp", strip_file_prefixes, ""); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-line-numbers", "nln", no_line_numbers, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-debug-output", "ndo", no_debug_output, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skipped-summary", "nss", no_skipped_summary, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-time-in-output", "ntio", no_time_in_output, false); + // clang-format on + + if(withDefaults) { + p->help = false; + p->version = false; + p->count = false; + p->list_test_cases = false; + p->list_test_suites = false; + p->list_reporters = false; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "help") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "h") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "?")) { + p->help = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "version") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "v")) { + p->version = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "count") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "c")) { + p->count = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-cases") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ltc")) { + p->list_test_cases = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-suites") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lts")) { + p->list_test_suites = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-reporters") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lr")) { + p->list_reporters = true; + p->exit = true; + } +} + +// allows the user to add procedurally to the filters from the command line +void Context::addFilter(const char* filter, const char* value) { setOption(filter, value); } + +// allows the user to clear all filters from the command line +void Context::clearFilters() { + for(auto& curr : p->filters) + curr.clear(); +} + +// allows the user to override procedurally the bool options from the command line +void Context::setOption(const char* option, bool value) { + setOption(option, value ? "true" : "false"); +} + +// allows the user to override procedurally the int options from the command line +void Context::setOption(const char* option, int value) { + setOption(option, toString(value).c_str()); +} + +// allows the user to override procedurally the string options from the command line +void Context::setOption(const char* option, const char* value) { + auto argv = String("-") + option + "=" + value; + auto lvalue = argv.c_str(); + parseArgs(1, &lvalue); +} + +// users should query this in their main() and exit the program if true +bool Context::shouldExit() { return p->exit; } + +void Context::setAsDefaultForAssertsOutOfTestCases() { g_cs = p; } + +void Context::setAssertHandler(detail::assert_handler ah) { p->ah = ah; } + +void Context::setCout(std::ostream* out) { p->cout = out; } + +static class DiscardOStream : public std::ostream +{ +private: + class : public std::streambuf + { + private: + // allowing some buffering decreases the amount of calls to overflow + char buf[1024]; + + protected: + std::streamsize xsputn(const char_type*, std::streamsize count) override { return count; } + + int_type overflow(int_type ch) override { + setp(std::begin(buf), std::end(buf)); + return traits_type::not_eof(ch); + } + } discardBuf; + +public: + DiscardOStream() + : std::ostream(&discardBuf) {} +} discardOut; + +// the main function that does all the filtering and test running +int Context::run() { + using namespace detail; + + // save the old context state in case such was setup - for using asserts out of a testing context + auto old_cs = g_cs; + // this is the current contest + g_cs = p; + is_running_in_test = true; + + g_no_colors = p->no_colors; + p->resetRunData(); + + std::fstream fstr; + if(p->cout == nullptr) { + if(p->quiet) { + p->cout = &discardOut; + } else if(p->out.size()) { + // to a file if specified + fstr.open(p->out.c_str(), std::fstream::out); + p->cout = &fstr; + } else { +#ifndef DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + // stdout by default + p->cout = &std::cout; +#else // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + return EXIT_FAILURE; +#endif // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + } + } + + FatalConditionHandler::allocateAltStackMem(); + + auto cleanup_and_return = [&]() { + FatalConditionHandler::freeAltStackMem(); + + if(fstr.is_open()) + fstr.close(); + + // restore context + g_cs = old_cs; + is_running_in_test = false; + + // we have to free the reporters which were allocated when the run started + for(auto& curr : p->reporters_currently_used) + delete curr; + p->reporters_currently_used.clear(); + + if(p->numTestCasesFailed && !p->no_exitcode) + return EXIT_FAILURE; + return EXIT_SUCCESS; + }; + + // setup default reporter if none is given through the command line + if(p->filters[8].empty()) + p->filters[8].push_back("console"); + + // check to see if any of the registered reporters has been selected + for(auto& curr : getReporters()) { + if(matchesAny(curr.first.second.c_str(), p->filters[8], false, p->case_sensitive)) + p->reporters_currently_used.push_back(curr.second(*g_cs)); + } + + // TODO: check if there is nothing in reporters_currently_used + + // prepend all listeners + for(auto& curr : getListeners()) + p->reporters_currently_used.insert(p->reporters_currently_used.begin(), curr.second(*g_cs)); + +#ifdef DOCTEST_PLATFORM_WINDOWS + if(isDebuggerActive() && p->no_debug_output == false) + p->reporters_currently_used.push_back(new DebugOutputWindowReporter(*g_cs)); +#endif // DOCTEST_PLATFORM_WINDOWS + + // handle version, help and no_run + if(p->no_run || p->version || p->help || p->list_reporters) { + DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, QueryData()); + + return cleanup_and_return(); + } + + std::vector testArray; + for(auto& curr : getRegisteredTests()) + testArray.push_back(&curr); + p->numTestCases = testArray.size(); + + // sort the collected records + if(!testArray.empty()) { + if(p->order_by.compare("file", true) == 0) { + std::sort(testArray.begin(), testArray.end(), fileOrderComparator); + } else if(p->order_by.compare("suite", true) == 0) { + std::sort(testArray.begin(), testArray.end(), suiteOrderComparator); + } else if(p->order_by.compare("name", true) == 0) { + std::sort(testArray.begin(), testArray.end(), nameOrderComparator); + } else if(p->order_by.compare("rand", true) == 0) { + std::srand(p->rand_seed); + + // random_shuffle implementation + const auto first = &testArray[0]; + for(size_t i = testArray.size() - 1; i > 0; --i) { + int idxToSwap = std::rand() % (i + 1); + + const auto temp = first[i]; + + first[i] = first[idxToSwap]; + first[idxToSwap] = temp; + } + } else if(p->order_by.compare("none", true) == 0) { + // means no sorting - beneficial for death tests which call into the executable + // with a specific test case in mind - we don't want to slow down the startup times + } + } + + std::set testSuitesPassingFilt; + + bool query_mode = p->count || p->list_test_cases || p->list_test_suites; + std::vector queryResults; + + if(!query_mode) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_start, DOCTEST_EMPTY); + + // invoke the registered functions if they match the filter criteria (or just count them) + for(auto& curr : testArray) { + const auto& tc = *curr; + + bool skip_me = false; + if(tc.m_skip && !p->no_skip) + skip_me = true; + + if(!matchesAny(tc.m_file.c_str(), p->filters[0], true, p->case_sensitive)) + skip_me = true; + if(matchesAny(tc.m_file.c_str(), p->filters[1], false, p->case_sensitive)) + skip_me = true; + if(!matchesAny(tc.m_test_suite, p->filters[2], true, p->case_sensitive)) + skip_me = true; + if(matchesAny(tc.m_test_suite, p->filters[3], false, p->case_sensitive)) + skip_me = true; + if(!matchesAny(tc.m_name, p->filters[4], true, p->case_sensitive)) + skip_me = true; + if(matchesAny(tc.m_name, p->filters[5], false, p->case_sensitive)) + skip_me = true; + + if(!skip_me) + p->numTestCasesPassingFilters++; + + // skip the test if it is not in the execution range + if((p->last < p->numTestCasesPassingFilters && p->first <= p->last) || + (p->first > p->numTestCasesPassingFilters)) + skip_me = true; + + if(skip_me) { + if(!query_mode) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_skipped, tc); + continue; + } + + // do not execute the test if we are to only count the number of filter passing tests + if(p->count) + continue; + + // print the name of the test and don't execute it + if(p->list_test_cases) { + queryResults.push_back(&tc); + continue; + } + + // print the name of the test suite if not done already and don't execute it + if(p->list_test_suites) { + if((testSuitesPassingFilt.count(tc.m_test_suite) == 0) && tc.m_test_suite[0] != '\0') { + queryResults.push_back(&tc); + testSuitesPassingFilt.insert(tc.m_test_suite); + p->numTestSuitesPassingFilters++; + } + continue; + } + + // execute the test if it passes all the filtering + { + p->currentTest = &tc; + + p->failure_flags = TestCaseFailureReason::None; + p->seconds = 0; + + // reset atomic counters + p->numAssertsFailedCurrentTest_atomic = 0; + p->numAssertsCurrentTest_atomic = 0; + + p->fullyTraversedSubcases.clear(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_start, tc); + + p->timer.start(); + + bool run_test = true; + + do { + // reset some of the fields for subcases (except for the set of fully passed ones) + p->reachedLeaf = false; + // May not be empty if previous subcase exited via exception. + p->subcaseStack.clear(); + p->currentSubcaseDepth = 0; + + p->shouldLogCurrentException = true; + + // reset stuff for logging with INFO() + p->stringifiedContexts.clear(); + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + try { +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +// MSVC 2015 diagnoses fatalConditionHandler as unused (because reset() is a static method) +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4101) // unreferenced local variable + FatalConditionHandler fatalConditionHandler; // Handle signals + // execute the test + tc.m_test(); + fatalConditionHandler.reset(); +DOCTEST_MSVC_SUPPRESS_WARNING_POP +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + } catch(const TestFailureException&) { + p->failure_flags |= TestCaseFailureReason::AssertFailure; + } catch(...) { + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception, + {translateActiveException(), false}); + p->failure_flags |= TestCaseFailureReason::Exception; + } +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + + // exit this loop if enough assertions have failed - even if there are more subcases + if(p->abort_after > 0 && + p->numAssertsFailed + p->numAssertsFailedCurrentTest_atomic >= p->abort_after) { + run_test = false; + p->failure_flags |= TestCaseFailureReason::TooManyFailedAsserts; + } + + if(!p->nextSubcaseStack.empty() && run_test) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_reenter, tc); + if(p->nextSubcaseStack.empty()) + run_test = false; + } while(run_test); + + p->finalizeTestCaseData(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs); + + p->currentTest = nullptr; + + // stop executing tests if enough assertions have failed + if(p->abort_after > 0 && p->numAssertsFailed >= p->abort_after) + break; + } + } + + if(!query_mode) { + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs); + } else { + QueryData qdata; + qdata.run_stats = g_cs; + qdata.data = queryResults.data(); + qdata.num_data = unsigned(queryResults.size()); + DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, qdata); + } + + return cleanup_and_return(); +} + +DOCTEST_DEFINE_INTERFACE(IReporter) + +int IReporter::get_num_active_contexts() { return detail::g_infoContexts.size(); } +const IContextScope* const* IReporter::get_active_contexts() { + return get_num_active_contexts() ? &detail::g_infoContexts[0] : nullptr; +} + +int IReporter::get_num_stringified_contexts() { return detail::g_cs->stringifiedContexts.size(); } +const String* IReporter::get_stringified_contexts() { + return get_num_stringified_contexts() ? &detail::g_cs->stringifiedContexts[0] : nullptr; +} + +namespace detail { + void registerReporterImpl(const char* name, int priority, reporterCreatorFunc c, bool isReporter) { + if(isReporter) + getReporters().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c)); + else + getListeners().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c)); + } +} // namespace detail + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +#ifdef DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4007) // 'function' : must be 'attribute' - see issue #182 +int main(int argc, char** argv) { return doctest::Context(argc, argv).run(); } +DOCTEST_MSVC_SUPPRESS_WARNING_POP +#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_MSVC_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +DOCTEST_SUPPRESS_COMMON_WARNINGS_POP + +#endif // DOCTEST_LIBRARY_IMPLEMENTATION +#endif // DOCTEST_CONFIG_IMPLEMENT + +#ifdef DOCTEST_UNDEF_WIN32_LEAN_AND_MEAN +#undef WIN32_LEAN_AND_MEAN +#undef DOCTEST_UNDEF_WIN32_LEAN_AND_MEAN +#endif // DOCTEST_UNDEF_WIN32_LEAN_AND_MEAN + +#ifdef DOCTEST_UNDEF_NOMINMAX +#undef NOMINMAX +#undef DOCTEST_UNDEF_NOMINMAX +#endif // DOCTEST_UNDEF_NOMINMAX diff --git a/test/test_basic.cpp b/test/test_basic.cpp new file mode 100644 index 0000000..2b2ee31 --- /dev/null +++ b/test/test_basic.cpp @@ -0,0 +1,447 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest.h" +#include "test_helpers.hpp" +#include "ctrack.hpp" + +// Test functions with predictable timing +void simple_function_5ms() +{ + CTRACK; + test_helpers::sleep_ms(5); +} + +void simple_function_10ms() +{ + CTRACK; + test_helpers::sleep_ms(10); +} + +void simple_function_20ms() +{ + CTRACK; + test_helpers::sleep_ms(20); +} + +void zero_duration_function() +{ + CTRACK; + // No delay - should have near-zero execution time +} + +void varying_sleep_function(int sleep_ms) +{ + CTRACK; + test_helpers::sleep_ms(sleep_ms); +} + +// Nested function hierarchy +void nested_child_function() +{ + CTRACK; + test_helpers::sleep_ms(5); +} + +void nested_parent_function() +{ + CTRACK; + test_helpers::sleep_ms(5); + nested_child_function(); +} + +// Recursive function +int recursive_factorial(int n) +{ + CTRACK; + test_helpers::sleep_ms(5); // 5ms delay per call + if (n <= 1) + return 1; + return n * recursive_factorial(n - 1); +} + +TEST_CASE("Basic single function tracking - 5ms sleep") +{ + test_helpers::clear_ctrack(); + + // Execute function 10 times with 5ms sleep + for (int i = 0; i < 100; i++) + { + simple_function_5ms(); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto &stats = tables.details.rows[0]; + + // Verify basic properties + CHECK(stats.function_name == "simple_function_5ms"); + CHECK(stats.calls == 100); + CHECK(stats.threads == 1); + CHECK(stats.line > 0); + + + CHECK(test_helpers::within_tolerance(stats.center_mean, std::chrono::milliseconds(5))); + CHECK(test_helpers::within_tolerance(stats.center_med, std::chrono::milliseconds(5))); + CHECK(stats.center_min <= stats.center_mean); + CHECK(stats.center_mean <= stats.center_max); + CHECK(stats.fastest_min <= stats.fastest_mean); + CHECK(stats.fastest_mean <= stats.center_mean); + CHECK(stats.center_mean <= stats.slowest_mean); + CHECK(stats.slowest_mean <= stats.slowest_max); + + // Verify accumulated time + CHECK(test_helpers::within_tolerance(stats.time_acc, std::chrono::milliseconds(500))); + + // CV should be relatively low for consistent timing + CHECK(stats.cv >= 0.0); + CHECK(stats.cv < 1.0); // Should be less than 100% +} + +TEST_CASE("Basic single function tracking - 10ms sleep") +{ + test_helpers::clear_ctrack(); + + // Execute function 5 times with 10ms sleep + for (int i = 0; i < 5; i++) + { + simple_function_10ms(); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto &stats = tables.details.rows[0]; + + // Verify basic properties + CHECK(stats.function_name == "simple_function_10ms"); + CHECK(stats.calls == 5); + CHECK(stats.threads == 1); + + // Verify timing statistics + CHECK(test_helpers::within_tolerance(stats.center_mean, std::chrono::milliseconds(10))); + CHECK(test_helpers::within_tolerance(stats.time_acc, std::chrono::milliseconds(50), std::chrono::milliseconds(10))); + + // Verify timing relationships + // CHECK(stats.center_min <= stats.center_max); + // CHECK(stats.fastest_min <= stats.slowest_max); +} + +TEST_CASE("Basic single function tracking - 20ms sleep") +{ + test_helpers::clear_ctrack(); + + // Execute function 3 times with 20ms sleep + for (int i = 0; i < 3; i++) + { + simple_function_20ms(); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto &stats = tables.details.rows[0]; + + // Verify basic properties + CHECK(stats.function_name == "simple_function_20ms"); + CHECK(stats.calls == 3); + CHECK(stats.threads == 1); + + // Verify timing statistics + CHECK(test_helpers::within_tolerance(stats.center_mean, std::chrono::milliseconds(20))); + CHECK(test_helpers::within_tolerance(stats.time_acc, std::chrono::milliseconds(60), std::chrono::milliseconds(6))); +} + +TEST_CASE("Zero duration function tracking") +{ + test_helpers::clear_ctrack(); + + // Execute zero-duration function 100 times + for (int i = 0; i < 100; i++) + { + zero_duration_function(); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto &stats = tables.details.rows[0]; + + // Verify basic properties + CHECK(stats.function_name == "zero_duration_function"); + CHECK(stats.calls == 100); + CHECK(stats.threads == 1); + + // Timing should be very small but non-negative + CHECK(stats.time_acc.count() >= 0); + CHECK(stats.center_mean.count() >= 0); + CHECK(stats.center_min.count() >= 0); + CHECK(stats.center_max.count() >= 0); + + // Should have very low execution times (less than 1ms total) + CHECK(stats.time_acc < std::chrono::milliseconds(1)); +} + +TEST_CASE("Single call scenario") +{ + test_helpers::clear_ctrack(); + + // Execute function only once + simple_function_10ms(); + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto &stats = tables.details.rows[0]; + + // Verify basic properties + CHECK(stats.function_name == "simple_function_10ms"); + CHECK(stats.calls == 1); + CHECK(stats.threads == 1); + + // For single call, min/mean/max should be very close + CHECK(stats.center_min <= stats.center_mean); + CHECK(stats.center_mean <= stats.center_max); + CHECK(test_helpers::within_tolerance(stats.center_mean, std::chrono::milliseconds(10))); + + // CV should be 0 or very small for single measurement + CHECK(stats.cv >= 0.0); +} + +TEST_CASE("Varying sleep times statistics") +{ + test_helpers::clear_ctrack(); + + // Test with varying sleep times: 5ms, 8ms, 10ms, 15ms, 20ms + std::vector sleep_times = {5, 8, 10, 15, 20}; + + for (int sleep_time : sleep_times) + { + varying_sleep_function(sleep_time); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto &stats = tables.details.rows[0]; + + // Verify basic properties + CHECK(stats.function_name == "varying_sleep_function"); + CHECK(stats.calls == 5); + CHECK(stats.threads == 1); + + // Expected mean: (5+8+10+15+20)/5 = 11.6ms + CHECK(test_helpers::within_tolerance(stats.center_mean, std::chrono::milliseconds(12), std::chrono::milliseconds(3))); // 20% tolerance for variance + + // Expected total: 5+8+10+15+20 = 58ms + CHECK(test_helpers::within_tolerance(stats.time_acc, std::chrono::milliseconds(58))); + + // Min should be close to 5ms, max close to 20ms + CHECK(test_helpers::within_tolerance(stats.center_min, std::chrono::milliseconds(5) ,std::chrono::milliseconds(2))); + CHECK(test_helpers::within_tolerance(stats.center_max, std::chrono::milliseconds(20), std::chrono::milliseconds(5))); + + // CV should be higher due to variance + CHECK(stats.cv > 0.2); // Should have significant coefficient of variation +} + +TEST_CASE("Nested function calls tracking") +{ + test_helpers::clear_ctrack(); + + // Execute nested function calls 5 times + for (int i = 0; i < 5; i++) + { + nested_parent_function(); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 2); // Parent and child functions + + // Find parent and child functions in results + const ctrack::detail_stats *parent_stats = nullptr; + const ctrack::detail_stats *child_stats = nullptr; + + for (const auto &stats : tables.details.rows) + { + if (stats.function_name == "nested_parent_function") + { + parent_stats = &stats; + } + else if (stats.function_name == "nested_child_function") + { + child_stats = &stats; + } + } + + REQUIRE(parent_stats != nullptr); + REQUIRE(child_stats != nullptr); + + // Verify parent function + CHECK(parent_stats->calls == 5); + CHECK(parent_stats->threads == 1); + // Parent function: 5ms own + child call time + CHECK(parent_stats->center_mean >= std::chrono::milliseconds(8)); // At least 8ms (5ms + 3ms) + + // Verify child function + CHECK(child_stats->calls == 5); + CHECK(child_stats->threads == 1); + CHECK(test_helpers::within_tolerance(child_stats->center_mean, std::chrono::milliseconds(5))); + + // Parent should take longer than child (includes child execution) + CHECK(parent_stats->center_mean > child_stats->center_mean); +} + +TEST_CASE("Recursive function calls tracking") +{ + test_helpers::clear_ctrack(); + + // Calculate factorial of 4 (should make 4 recursive calls: 4, 3, 2, 1) + int result = recursive_factorial(4); + CHECK(result == 24); // 4! = 24 + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto &stats = tables.details.rows[0]; + + // Verify basic properties + CHECK(stats.function_name == "recursive_factorial"); + CHECK(stats.calls == 4); // Called for n=4, 3, 2, 1 + CHECK(stats.threads == 1); + + CHECK(test_helpers::within_tolerance(stats.center_mean, std::chrono::milliseconds(12))); + + // Total time should be around 5+10+15+20 = 50ms (approx) + CHECK(test_helpers::within_tolerance(stats.time_acc, std::chrono::milliseconds(50))); + + // All calls should have similar timing + CHECK(stats.cv < 0.5); // Should have low variance +} + +TEST_CASE("Multiple different functions tracking") +{ + test_helpers::clear_ctrack(); + + // Call different functions + simple_function_5ms(); + simple_function_10ms(); + simple_function_20ms(); + zero_duration_function(); + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 4); + + // Verify we have all four functions + std::set function_names; + for (const auto &stats : tables.details.rows) + { + function_names.insert(stats.function_name); + + // All should have exactly 1 call + CHECK(stats.calls == 1); + CHECK(stats.threads == 1); + CHECK(stats.line > 0); + } + + CHECK(function_names.count("simple_function_5ms") == 1); + CHECK(function_names.count("simple_function_10ms") == 1); + CHECK(function_names.count("simple_function_20ms") == 1); + CHECK(function_names.count("zero_duration_function") == 1); +} + +TEST_CASE("Summary table validation") +{ + test_helpers::clear_ctrack(); + + // Execute multiple functions with known patterns + for (int i = 0; i < 10; i++) + { + simple_function_5ms(); + } + for (int i = 0; i < 5; i++) + { + simple_function_10ms(); + } + + auto tables = ctrack::result_get_tables(); + + // Verify summary table has same number of entries as detail table + CHECK(tables.summary.rows.size() == tables.details.rows.size()); + CHECK(tables.summary.rows.size() == 2); + + // Verify summary table entries + for (const auto &summary_row : tables.summary.rows) + { + CHECK(summary_row.line > 0); + CHECK(summary_row.calls > 0); + CHECK(summary_row.percent_ae_bracket >= 0.0); + CHECK(summary_row.percent_ae_bracket <= 100.0); + CHECK(summary_row.percent_ae_all >= 0.0); + CHECK(summary_row.percent_ae_all <= 100.0); + CHECK(summary_row.time_ae_all.count() >= 0); + CHECK(summary_row.time_a_all.count() >= 0); + + // Function name should be one of our test functions + CHECK((summary_row.function_name == "simple_function_5ms" || + summary_row.function_name == "simple_function_10ms")); + } +} + +TEST_CASE("Timing relationships validation") +{ + test_helpers::clear_ctrack(); + + // Execute function multiple times to get meaningful statistics + for (int i = 0; i < 100; i++) + { + simple_function_10ms(); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto &stats = tables.details.rows[0]; + + // Verify all timing relationships + CHECK(stats.fastest_min <= stats.fastest_mean); + CHECK(stats.fastest_mean <= stats.center_min); + CHECK(stats.center_min <= stats.center_mean); + CHECK(test_helpers::within_tolerance(stats.center_med, stats.center_mean, std::chrono::milliseconds(1))); + CHECK(stats.center_med <= stats.center_max); + CHECK(stats.center_max <= stats.slowest_mean); + CHECK(stats.slowest_mean <= stats.slowest_max); + + // Time accumulation relationships + CHECK(stats.center_time_a <= stats.time_acc); + CHECK(stats.center_time_ae <= stats.center_time_a); + + // Standard deviation should be non-negative + CHECK(stats.sd.count() >= 0); + + // Coefficient of variation should be non-negative + CHECK(stats.cv >= 0.0); +} + +TEST_CASE("Meta information validation") +{ + auto start_time = std::chrono::high_resolution_clock::now(); + test_helpers::clear_ctrack(); + + // Execute some functions + simple_function_5ms(); + simple_function_10ms(); + + auto tables = ctrack::result_get_tables(); + auto end_time = std::chrono::high_resolution_clock::now(); + + // Verify meta information + CHECK(tables.start_time <= tables.end_time); + CHECK(tables.start_time >= start_time); + CHECK(tables.end_time <= end_time); + + CHECK(tables.time_total.count() > 0); + CHECK(tables.time_ctracked.count() > 0); + CHECK(tables.time_ctracked <= tables.time_total); + + // Should have tracked approximately 15ms (5ms + 10ms) + CHECK(test_helpers::within_tolerance(tables.time_ctracked, std::chrono::milliseconds(15), std::chrono::milliseconds(5))); +} \ No newline at end of file diff --git a/test/test_edge_cases.cpp b/test/test_edge_cases.cpp new file mode 100644 index 0000000..de57c97 --- /dev/null +++ b/test/test_edge_cases.cpp @@ -0,0 +1,463 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest.h" +#include "test_helpers.hpp" +#include "ctrack.hpp" + +#include +#include +#include +#include +#include +#include + +TEST_CASE("Zero duration functions") { + test_helpers::clear_ctrack(); + + // Function with no actual work - just the CTRACK macro + auto zero_duration = []() { + CTRACK_NAME("zero_duration"); + // No sleep or work, just return immediately + }; + + const int call_count = 1000; + for (int i = 0; i < call_count; i++) { + zero_duration(); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto& stats = tables.details.rows[0]; + CHECK(stats.calls == call_count); + CHECK(stats.threads == 1); + CHECK(stats.function_name.find("zero_duration") != std::string::npos); + + // Even zero-duration should have some overhead (nanoseconds) + CHECK(stats.center_mean.count() >= 0); + CHECK(stats.center_mean.count() < 1000000); // Less than 1ms per call + CHECK(stats.time_acc.count() >= 0); + CHECK(stats.fastest_min.count() >= 0); + CHECK(stats.slowest_max.count() >= stats.fastest_min.count()); + CHECK(stats.cv >= 0.0); +} + +TEST_CASE("Single call scenarios") { + test_helpers::clear_ctrack(); + + // Test single call with no work + { + CTRACK_NAME("single_call_no_work"); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto& stats = tables.details.rows[0]; + CHECK(stats.calls == 1); + CHECK(stats.threads == 1); + CHECK(stats.function_name == "single_call_no_work"); + CHECK(stats.center_mean.count() >= 0); + CHECK(stats.fastest_min == stats.slowest_max); // Single call, min == max + CHECK(stats.center_min == stats.center_max); // Single call, min == max + + // CV should be 0 for single call (no variation) + CHECK(stats.cv == 0.0); +} + +TEST_CASE("Very high call counts") { + test_helpers::clear_ctrack(); + + const int high_count = 100000; + + auto high_frequency_func = []() { + CTRACK_NAME("high_frequency"); + // Minimal work to avoid compiler optimization + volatile int x = 42; + (void)x; + }; + + auto start_time = std::chrono::high_resolution_clock::now(); + + for (int i = 0; i < high_count; i++) { + high_frequency_func(); + } + + auto end_time = std::chrono::high_resolution_clock::now(); + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto& stats = tables.details.rows[0]; + CHECK(stats.calls == high_count); + CHECK(stats.time_acc.count() > 0); + CHECK(stats.center_mean.count() >= 0); + CHECK(stats.fastest_min.count() >= 0); + CHECK(stats.slowest_max.count() >= stats.fastest_min.count()); + + // Verify no integer overflow occurred + CHECK(stats.time_acc.count() > 0); + CHECK(stats.time_acc.count() < std::numeric_limits::max() / 2); + + // Summary should also be consistent + CHECK(tables.summary.rows.size() == 1); + CHECK(tables.summary.rows[0].calls == high_count); +} + +TEST_CASE("Empty tracking - no CTRACK calls") { + test_helpers::clear_ctrack(); + + // Do some work without any tracking + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + volatile int sum = 0; + for (int i = 0; i < 1000; i++) { + sum += i; + } + (void)sum; + + auto tables = ctrack::result_get_tables(); + + // Should have no tracked functions + CHECK(tables.summary.rows.empty()); + CHECK(tables.details.rows.empty()); + CHECK(tables.time_ctracked.count() == 0); + CHECK(tables.time_total.count() > 0); // Should still measure total time +} + +// Deep recursive function for extreme nesting +int deep_recursive_func(int depth, int max_depth) { + if (depth >= max_depth) { + CTRACK_NAME("recursive_base"); + return depth; + } + + CTRACK_NAME("recursive_call"); + return deep_recursive_func(depth + 1, max_depth); +} + +TEST_CASE("Extremely nested scenarios") { + test_helpers::clear_ctrack(); + + const int nesting_depth = 25; + int result = deep_recursive_func(0, nesting_depth); + + CHECK(result == nesting_depth); + + auto tables = ctrack::result_get_tables(); + + // Should have two function entries: recursive_call and recursive_base + REQUIRE(tables.details.rows.size() == 2); + + // Find the recursive call stats + const ctrack::detail_stats* recursive_stats = nullptr; + const ctrack::detail_stats* base_stats = nullptr; + + for (const auto& stats : tables.details.rows) { + if (stats.function_name == "recursive_call") { + recursive_stats = &stats; + } else if (stats.function_name == "recursive_base") { + base_stats = &stats; + } + } + + REQUIRE(recursive_stats != nullptr); + REQUIRE(base_stats != nullptr); + + // Recursive call should be called nesting_depth times + CHECK(recursive_stats->calls == nesting_depth); + // Base case should be called once + CHECK(base_stats->calls == 1); + + // All calls should be on the same thread + CHECK(recursive_stats->threads == 1); + CHECK(base_stats->threads == 1); + + // Verify timing relationships are maintained + CHECK(recursive_stats->time_acc.count() >= base_stats->time_acc.count()); +} + +TEST_CASE("Functions with same name but different locations") { + test_helpers::clear_ctrack(); + + // Lambda 1 + auto func1 = []() { + CTRACK_NAME("same_function_name"); + test_helpers::sleep_ms(5); + }; + + // Lambda 2 - different location, same name + auto func2 = []() { + CTRACK_NAME("same_function_name"); + test_helpers::sleep_ms(5); + }; + + // Call both functions multiple times + for (int i = 0; i < 10; i++) { + func1(); + func2(); + } + + auto tables = ctrack::result_get_tables(); + + // Should have separate entries for each location + // (Note: ctrack might aggregate by name, depending on implementation) + CHECK(tables.details.rows.size() >= 1); + CHECK(tables.details.rows.size() <= 2); + + // Total calls should be 20 + int total_calls = 0; + for (const auto& stats : tables.details.rows) { + CHECK(stats.function_name == "same_function_name"); + total_calls += stats.calls; + } + CHECK(total_calls == 20); +} + +TEST_CASE("Very long running functions") { + test_helpers::clear_ctrack(); + + auto long_running_func = []() { + CTRACK_NAME("long_runner"); + test_helpers::sleep_ms(500); // 500ms + }; + + const int call_count = 3; + for (int i = 0; i < call_count; i++) { + long_running_func(); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto& stats = tables.details.rows[0]; + CHECK(stats.calls == call_count); + CHECK(stats.function_name == "long_runner"); + + // Each call should be around 500ms + auto expected_per_call = std::chrono::nanoseconds(500 * 1000000); // 500ms in ns + CHECK(stats.center_mean.count() >= expected_per_call.count() * 0.8); + CHECK(stats.center_mean.count() <= expected_per_call.count() * 1.3); + + // Total time should be around 1.5 seconds + auto expected_total = std::chrono::nanoseconds(1500 * 1000000); // 1.5s in ns + CHECK(stats.time_acc.count() >= expected_total.count() * 0.8); + CHECK(stats.time_acc.count() <= expected_total.count() * 1.3); + + // Verify no overflow in timing calculations + CHECK(stats.time_acc.count() > 0); + CHECK(stats.center_mean.count() > 0); + +} + +TEST_CASE("Mix of very fast and very slow functions") { + test_helpers::clear_ctrack(); + + auto fast_func = []() { + CTRACK_NAME("fast_function"); + // Just volatile operations + volatile int x = 1; + x *= 2; + (void)x; + }; + + auto slow_func = []() { + CTRACK_NAME("slow_function"); + test_helpers::sleep_ms(100); + }; + + // Call fast function many times, slow function few times + const int fast_calls = 1000; + const int slow_calls = 5; + + for (int i = 0; i < fast_calls; i++) { + fast_func(); + } + + for (int i = 0; i < slow_calls; i++) { + slow_func(); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 2); + + const ctrack::detail_stats* fast_stats = nullptr; + const ctrack::detail_stats* slow_stats = nullptr; + + for (const auto& stats : tables.details.rows) { + if (stats.function_name == "fast_function") { + fast_stats = &stats; + } else if (stats.function_name == "slow_function") { + slow_stats = &stats; + } + } + + REQUIRE(fast_stats != nullptr); + REQUIRE(slow_stats != nullptr); + + CHECK(fast_stats->calls == fast_calls); + CHECK(slow_stats->calls == slow_calls); + + // Slow function should have much higher mean time + CHECK(slow_stats->center_mean.count() > fast_stats->center_mean.count() * 1000); + + // Both should have valid statistics + CHECK(fast_stats->cv >= 0.0); + CHECK(slow_stats->cv >= 0.0); + CHECK(fast_stats->time_acc.count() > 0); + CHECK(slow_stats->time_acc.count() > 0); +} + +TEST_CASE("Rapid successive calls - stress test") { + test_helpers::clear_ctrack(); + + const int rapid_count = 50000; + std::atomic counter{0}; + + auto rapid_func = [&counter]() { + CTRACK_NAME("rapid_calls"); + counter.fetch_add(1, std::memory_order_relaxed); + }; + + auto start = std::chrono::high_resolution_clock::now(); + + // Rapid fire calls + for (int i = 0; i < rapid_count; i++) { + rapid_func(); + } + + auto end = std::chrono::high_resolution_clock::now(); + auto total_time = std::chrono::duration_cast(end - start); + + CHECK(counter.load() == rapid_count); + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto& stats = tables.details.rows[0]; + CHECK(stats.calls == rapid_count); + CHECK(stats.function_name == "rapid_calls"); + + // Verify timing is reasonable (should be very fast per call) + CHECK(stats.center_mean.count() < 10000); // Less than 10 microseconds per call + CHECK(stats.time_acc.count() > 0); + CHECK(stats.time_acc.count() <= total_time.count() * 2); // Allow some overhead + + // Verify statistics are computed correctly even with rapid calls + CHECK(stats.fastest_min.count() >= 0); + CHECK(stats.slowest_max.count() >= stats.fastest_min.count()); + CHECK(stats.cv >= 0.0); + CHECK(stats.sd.count() >= 0); +} + +TEST_CASE("Boundary condition - maximum thread count") { + test_helpers::clear_ctrack(); + + const int thread_count = std::min(20, static_cast(std::thread::hardware_concurrency() * 2)); + std::vector> futures; + test_helpers::ThreadBarrier barrier(thread_count); + + auto threaded_func = [&barrier]() { + CTRACK_NAME("multithreaded_boundary"); + barrier.wait(); // Synchronize all threads + test_helpers::sleep_ms(5); // 5ms work + }; + + // Launch multiple threads + for (int i = 0; i < thread_count; i++) { + futures.push_back(std::async(std::launch::async, threaded_func)); + } + + // Wait for all threads + for (auto& future : futures) { + future.wait(); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto& stats = tables.details.rows[0]; + CHECK(stats.calls == thread_count); + CHECK(stats.function_name == "multithreaded_boundary"); + CHECK(stats.threads == thread_count); + + // Verify multithreaded timing makes sense + CHECK(stats.time_acc.count() > 0); + CHECK(stats.center_mean.count() >= 5000000); // At least 5ms per call + CHECK(stats.cv >= 0.0); +} + +TEST_CASE("Precision edge case - very small time differences") { + test_helpers::clear_ctrack(); + + auto micro_work_func = []() { + CTRACK_NAME("micro_work"); + // Minimal CPU work that should complete in nanoseconds + volatile int result = 0; + for (int i = 0; i < 10; i++) { + result += i * i; + } + (void)result; + }; + + const int call_count = 10000; + for (int i = 0; i < call_count; i++) { + micro_work_func(); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto& stats = tables.details.rows[0]; + CHECK(stats.calls == call_count); + + // Even micro work should be measurable + CHECK(stats.center_mean.count() >= 0); + CHECK(stats.time_acc.count() > 0); + CHECK(stats.fastest_min.count() >= 0); + CHECK(stats.slowest_max.count() >= stats.fastest_min.count()); + + // Standard deviation and CV should be computed correctly + CHECK(stats.sd.count() >= 0); + CHECK(stats.cv >= 0.0); + + // For very small times, ensure no division by zero or overflow + if (stats.center_mean.count() > 0) { + double calculated_cv = static_cast(stats.sd.count()) / static_cast(stats.center_mean.count()); + CHECK(test_helpers::within_tolerance(stats.cv, calculated_cv, 20.0)); + } +} + +TEST_CASE("Memory stress - tracking large number of unique functions") { + test_helpers::clear_ctrack(); + + // Create many unique function names + const int unique_functions = 100; + std::vector> functions; + + for (int i = 0; i < unique_functions; i++) { + std::string name = "unique_func_" + std::to_string(i); + functions.emplace_back([name]() { + CTRACK_NAME(name.c_str()); + test_helpers::sleep_ms(5); + }); + } + + // Call each function once + for (auto& func : functions) { + func(); + } + + auto tables = ctrack::result_get_tables(); + CHECK(tables.details.rows.size() == unique_functions); + CHECK(tables.summary.rows.size() == unique_functions); + + // Verify each function is tracked correctly + std::set found_names; + for (const auto& stats : tables.details.rows) { + CHECK(stats.calls == 1); + CHECK(stats.threads == 1); + CHECK(stats.function_name.find("unique_func_") == 0); + found_names.insert(stats.function_name); + } + + CHECK(found_names.size() == unique_functions); +} \ No newline at end of file diff --git a/test/test_helpers.hpp b/test/test_helpers.hpp new file mode 100644 index 0000000..3f70603 --- /dev/null +++ b/test/test_helpers.hpp @@ -0,0 +1,284 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ctrack.hpp" + +namespace test_helpers { + + inline void precise_busy_wait(std::chrono::nanoseconds duration) { + auto start = std::chrono::high_resolution_clock::now(); + while (std::chrono::high_resolution_clock::now() - start < duration) { + // Busy wait - burns CPU but very precise + } + } + +// Sleep for a specific number of milliseconds with reasonable precision +inline void sleep_ms(int milliseconds) { + std::chrono::milliseconds ms(milliseconds); + precise_busy_wait(ms); +} + +// Sleep for a specific number of microseconds +inline void sleep_us(int microseconds) { + std::chrono::microseconds us(microseconds); + precise_busy_wait(us); + +} + + + + + + + +// Simple tolerance check for nanoseconds (as int64_t) +// Relative tolerance with default 15%, minimum 1ms +inline bool within_tolerance_relative(int64_t actual_ns, int64_t expected_ns, double tolerance_percent = 20.0) { + int64_t tolerance = std::max( + static_cast(std::abs(expected_ns) * tolerance_percent / 100.0), + int64_t(1000000) // 1ms minimum + ); + return std::abs(actual_ns - expected_ns) <= tolerance; +} + +// Absolute tolerance in nanoseconds +inline bool within_tolerance_absolute(int64_t actual_ns, int64_t expected_ns, int64_t tolerance_ns) { + return std::abs(actual_ns - expected_ns) <= tolerance_ns; +} + + + +// Simple wrapper for chrono types - just call .count() +template +inline bool within_tolerance(std::chrono::duration actual, + std::chrono::duration expected) { + auto actual_ns = std::chrono::duration_cast(actual).count(); + auto expected_ns = std::chrono::duration_cast(expected).count(); + return within_tolerance_relative(actual_ns, expected_ns, 20.0); +} + +template +inline bool within_tolerance(std::chrono::duration actual, + std::chrono::duration expected, + double tolerance_percent) { + auto actual_ns = std::chrono::duration_cast(actual).count(); + auto expected_ns = std::chrono::duration_cast(expected).count(); + return within_tolerance_relative(actual_ns, expected_ns, tolerance_percent); +} + +template +inline bool within_tolerance(std::chrono::duration actual, + std::chrono::duration expected, + std::chrono::duration tolerance) { + auto actual_ns = std::chrono::duration_cast(actual).count(); + auto expected_ns = std::chrono::duration_cast(expected).count(); + auto tolerance_ns = std::chrono::duration_cast(tolerance).count(); + return within_tolerance_absolute(actual_ns, expected_ns, tolerance_ns); +} + +// For doubles +inline bool within_tolerance(double actual, double expected, double tolerance_percent = 20.0) { + if (expected == 0.0) { + return std::abs(actual) < 0.001; + } + double tolerance = std::abs(expected) * (tolerance_percent / 100.0); + return std::abs(actual - expected) <= tolerance; +} + +// Thread barrier for synchronizing multiple threads +class ThreadBarrier { +public: + explicit ThreadBarrier(size_t count) : threshold(count), count(count), generation(0) {} + + void wait() { + std::unique_lock lock(mutex); + auto gen = generation; + + if (--count == 0) { + generation++; + count = threshold; + cond.notify_all(); + } else { + cond.wait(lock, [this, gen] { return gen != generation; }); + } + } + +private: + std::mutex mutex; + std::condition_variable cond; + size_t threshold; + size_t count; + size_t generation; +}; + +// Calculate expected statistics for a series of sleep times +struct ExpectedStats { + std::chrono::nanoseconds min; + std::chrono::nanoseconds max; + std::chrono::nanoseconds mean; + std::chrono::nanoseconds median; + std::chrono::nanoseconds total; + double std_dev_ns; + double cv; + + ExpectedStats(const std::vector& sleep_times_ms) { + if (sleep_times_ms.empty()) { + min = max = mean = median = total = std::chrono::nanoseconds(0); + std_dev_ns = cv = 0.0; + return; + } + + // Convert to nanoseconds + std::vector times_ns; + for (int ms : sleep_times_ms) { + times_ns.push_back(static_cast(ms) * 1000000); + } + + // Calculate basic stats + auto minmax = std::minmax_element(times_ns.begin(), times_ns.end()); + min = std::chrono::nanoseconds(*minmax.first); + max = std::chrono::nanoseconds(*minmax.second); + + int64_t sum = std::accumulate(times_ns.begin(), times_ns.end(), int64_t(0)); + total = std::chrono::nanoseconds(sum); + mean = std::chrono::nanoseconds(sum / times_ns.size()); + + // Calculate median + std::vector sorted = times_ns; + std::sort(sorted.begin(), sorted.end()); + if (sorted.size() % 2 == 0) { + median = std::chrono::nanoseconds((sorted[sorted.size()/2 - 1] + sorted[sorted.size()/2]) / 2); + } else { + median = std::chrono::nanoseconds(sorted[sorted.size()/2]); + } + + // Calculate standard deviation + double mean_val = static_cast(mean.count()); + double variance = 0.0; + for (int64_t t : times_ns) { + double diff = static_cast(t) - mean_val; + variance += diff * diff; + } + variance /= times_ns.size(); + std_dev_ns = std::sqrt(variance); + + // Calculate CV + cv = (mean_val > 0) ? (std_dev_ns / mean_val) : 0.0; + } +}; + +// Validate summary row +inline bool validate_summary_row(const ctrack::summary_row& row, + const std::string& expected_function, + int expected_calls, + double tolerance_percent = 15.0) { + if (row.function_name != expected_function) return false; + if (row.calls != expected_calls) return false; + if (row.line <= 0) return false; + + // Percentages should be between 0 and 100 + if (row.percent_ae_bracket < 0 || row.percent_ae_bracket > 100) return false; + if (row.percent_ae_all < 0 || row.percent_ae_all > 100) return false; + + // Times should be non-negative + if (row.time_ae_all.count() < 0) return false; + if (row.time_a_all.count() < 0) return false; + + return true; +} + +// Validate detail stats +inline bool validate_detail_stats(const ctrack::detail_stats& stats, + const std::string& expected_function, + int expected_calls, + int expected_threads = 1, + double tolerance_percent = 15.0) { + if (stats.function_name != expected_function) return false; + if (stats.calls != expected_calls) return false; + if (stats.threads != expected_threads) return false; + if (stats.line <= 0) return false; + + // All times should be non-negative + if (stats.time_acc.count() < 0) return false; + if (stats.sd.count() < 0) return false; + + if (stats.center_min.count() < 0) return false; + if (stats.center_mean.count() < 0) return false; + if (stats.center_med.count() < 0) return false; + if (stats.center_time_a.count() < 0) return false; + if (stats.center_time_ae.count() < 0) return false; + if (stats.center_max.count() < 0) return false; + + + // CV should be non-negative + if (stats.cv < 0) return false; + + bool does_non_center_exist = (expected_calls * stats.fastest_range / 100 )>0; + + if (does_non_center_exist) { + + if (stats.fastest_min.count() < 0) return false; + if (stats.fastest_mean.count() < 0) return false; + if (stats.slowest_mean.count() < 0) return false; + if (stats.slowest_max.count() < 0) return false; + + // Logical relationships + if (stats.fastest_min > stats.fastest_mean) return false; + if (stats.fastest_mean > stats.center_mean) return false; + if (stats.center_mean > stats.slowest_mean) return false; + if (stats.center_min > stats.center_max) return false; + if (stats.slowest_mean > stats.slowest_max) return false; + + } + + + + return true; +} + +// Helper to clear ctrack data between tests +inline void clear_ctrack() { + // Get current results to clear the internal state + ctrack::result_get_tables(); +} + +// Test function with predictable timing +template +inline void test_function_with_sleep(int sleep_time_ms, const char(&name)[N]) { + CTRACK_NAME(name); // String literals have static storage duration + sleep_ms(sleep_time_ms); +} + +// Overload for empty/runtime strings +inline void test_function_with_sleep(int sleep_time_ms) { + CTRACK; + sleep_ms(sleep_time_ms); +} + +// Nested test functions +inline void nested_level_2(int sleep_time_ms) { + CTRACK; + sleep_ms(sleep_time_ms); +} + +inline void nested_level_1(int sleep_time_ms, int child_sleep_ms) { + CTRACK; + sleep_ms(sleep_time_ms); + nested_level_2(child_sleep_ms); +} + +inline void nested_root(int sleep_time_ms, int level1_sleep_ms, int level2_sleep_ms) { + CTRACK; + sleep_ms(sleep_time_ms); + nested_level_1(level1_sleep_ms, level2_sleep_ms); +} + +} // namespace test_helpers \ No newline at end of file diff --git a/test/test_multithreaded.cpp b/test/test_multithreaded.cpp new file mode 100644 index 0000000..f9e597c --- /dev/null +++ b/test/test_multithreaded.cpp @@ -0,0 +1,470 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest.h" +#include "test_helpers.hpp" +#include "ctrack.hpp" +#include +#include +#include +#include +#include +#include + +using namespace test_helpers; + +// Helper function for multi-threaded testing +void multithreaded_test_function(int sleep_time_ms) { + CTRACK; + test_helpers::sleep_ms(sleep_time_ms); +} + +// Named function for testing + +void named_test_function_1(int sleep_time_ms) { + CTRACK_NAME("named_test_function_1"); + test_helpers::sleep_ms(sleep_time_ms); +} + +void named_test_function_2(int sleep_time_ms) { + CTRACK_NAME("named_test_function_2"); + test_helpers::sleep_ms(sleep_time_ms); +} + +void named_test_function_3(int sleep_time_ms) { + CTRACK_NAME("named_test_function_3"); + test_helpers::sleep_ms(sleep_time_ms); +} + + + + +// Nested function for testing call hierarchy in threads +void nested_child(int sleep_time_ms) { + CTRACK; + test_helpers::sleep_ms(sleep_time_ms); +} + +void nested_parent(int child_sleep_ms, int parent_sleep_ms) { + CTRACK; + test_helpers::sleep_ms(parent_sleep_ms); + nested_child(child_sleep_ms); +} + +TEST_CASE("Multiple threads same function") { + clear_ctrack(); + const int num_threads = 4; + const int calls_per_thread = 5; + const int sleep_time_ms = 20; + + ThreadBarrier barrier(num_threads); + std::vector threads; + + // Start threads synchronized + for (int i = 0; i < num_threads; i++) { + threads.emplace_back([&barrier, calls_per_thread, sleep_time_ms]() { + barrier.wait(); // Synchronize start + for (int j = 0; j < calls_per_thread; j++) { + multithreaded_test_function(sleep_time_ms); + } + }); + } + + // Wait for all threads to complete + for (auto& t : threads) { + t.join(); + } + + // Validate results + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto& stats = tables.details.rows[0]; + CHECK(stats.function_name == "multithreaded_test_function"); + CHECK(stats.calls == num_threads * calls_per_thread); + CHECK(stats.threads == num_threads); + + // Validate that time_acc represents total accumulated time across all threads + auto expected_total_time = std::chrono::milliseconds(num_threads * calls_per_thread * sleep_time_ms); + CHECK(within_tolerance(stats.time_acc, expected_total_time, 20.0)); + + // Summary table should match + REQUIRE(tables.summary.rows.size() == 1); + const auto& summary = tables.summary.rows[0]; + CHECK(summary.function_name == "multithreaded_test_function"); + CHECK(summary.calls == num_threads * calls_per_thread); +} + +TEST_CASE("Thread count tracking with different thread counts") { + SUBCASE("2 threads") { + clear_ctrack(); + const int num_threads = 2; + const int sleep_time_ms = 10; + + ThreadBarrier barrier(num_threads); + std::vector threads; + + for (int i = 0; i < num_threads; i++) { + threads.emplace_back([&barrier, sleep_time_ms]() { + barrier.wait(); + multithreaded_test_function(sleep_time_ms); + }); + } + + for (auto& t : threads) { + t.join(); + } + + auto tables = ctrack::result_get_tables(); + const auto& stats = tables.details.rows[0]; + CHECK(stats.threads == 2); + CHECK(stats.calls == 2); + } + + SUBCASE("8 threads") { + clear_ctrack(); + const int num_threads = 8; + const int sleep_time_ms = 5; + + ThreadBarrier barrier(num_threads); + std::vector threads; + + for (int i = 0; i < num_threads; i++) { + threads.emplace_back([&barrier, sleep_time_ms]() { + barrier.wait(); + multithreaded_test_function(sleep_time_ms); + }); + } + + for (auto& t : threads) { + t.join(); + } + + auto tables = ctrack::result_get_tables(); + const auto& stats = tables.details.rows[0]; + CHECK(stats.threads == 8); + CHECK(stats.calls == 8); + } +} + +TEST_CASE("Per-thread event isolation") { + clear_ctrack(); + const int num_threads = 3; + const int sleep_time_ms = 8; + + std::vector threads; + std::atomic completed_threads{0}; + + // Each thread calls a uniquely named function + for (int i = 0; i < num_threads; i++) { + threads.emplace_back([i, sleep_time_ms, &completed_threads]() { + + if((i+1) % 2 == 0) { + named_test_function_1(sleep_time_ms); + } else if ((i+1) % 3 == 0) { + named_test_function_2(sleep_time_ms); + } else { + named_test_function_3(sleep_time_ms); + } + + completed_threads.fetch_add(1); + }); + } + + for (auto& t : threads) { + t.join(); + } + + CHECK(completed_threads.load() == num_threads); + + auto tables = ctrack::result_get_tables(); + CHECK(tables.details.rows.size() == num_threads); + + // Each function should show up with 1 call and 1 thread + for (const auto& stats : tables.details.rows) { + CHECK(stats.calls == 1); + CHECK(stats.threads == 1); + CHECK(stats.function_name.substr(0, 20) == "named_test_function_"); + + auto expected_time = std::chrono::milliseconds(sleep_time_ms); + CHECK(within_tolerance(stats.time_acc, expected_time, 20.0)); + } +} + +TEST_CASE("Concurrent access thread safety") { + clear_ctrack(); + const int num_threads = 6; + const int calls_per_thread = 20; + const int sleep_time_ms = 5; // Short sleep to stress test timing + + std::vector threads; + std::atomic total_calls{0}; + + for (int i = 0; i < num_threads; i++) { + threads.emplace_back([calls_per_thread, sleep_time_ms, &total_calls]() { + for (int j = 0; j < calls_per_thread; j++) { + multithreaded_test_function(sleep_time_ms); + total_calls.fetch_add(1); + } + }); + } + + for (auto& t : threads) { + t.join(); + } + + CHECK(total_calls.load() == num_threads * calls_per_thread); + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto& stats = tables.details.rows[0]; + CHECK(stats.calls == num_threads * calls_per_thread); + CHECK(stats.threads == num_threads); + + // Verify accumulated time is reasonable + auto min_expected_time = std::chrono::milliseconds(num_threads * calls_per_thread * sleep_time_ms); + CHECK(stats.time_acc >= min_expected_time * 0.8); // Allow some tolerance for timing variations +} + +TEST_CASE("Thread barrier synchronization scenarios") { + clear_ctrack(); + const int num_threads = 4; + const int sleep_time_ms = 100; + + ThreadBarrier start_barrier(num_threads); + ThreadBarrier end_barrier(num_threads); + std::vector threads; + + auto start_time = std::chrono::high_resolution_clock::now(); + + for (int i = 0; i < num_threads; i++) { + threads.emplace_back([&start_barrier, &end_barrier, sleep_time_ms]() { + start_barrier.wait(); // All threads start simultaneously + multithreaded_test_function(sleep_time_ms); + end_barrier.wait(); // All threads finish simultaneously + }); + } + + for (auto& t : threads) { + t.join(); + } + + auto end_time = std::chrono::high_resolution_clock::now(); + auto wall_clock_time = std::chrono::duration_cast(end_time - start_time); + + auto tables = ctrack::result_get_tables(); + const auto& stats = tables.details.rows[0]; + + CHECK(stats.calls == num_threads); + CHECK(stats.threads == num_threads); + + // Since threads run in parallel, the wall clock time should be approximately + // equal to the sleep time (not num_threads * sleep_time) + CHECK(within_tolerance(wall_clock_time, std::chrono::milliseconds(sleep_time_ms), 50.0)); + + // But accumulated time should be the sum of all thread times + auto expected_acc_time = std::chrono::milliseconds(num_threads * sleep_time_ms); + CHECK(within_tolerance(stats.time_acc, expected_acc_time, 20.0)); +} + +TEST_CASE("Mixed function calls across threads") { + clear_ctrack(); + const int num_threads = 4; + const int sleep_time_ms = 5; + + ThreadBarrier barrier(num_threads); + std::vector threads; + + // Each thread calls both functions + for (int i = 0; i < num_threads; i++) { + threads.emplace_back([&barrier, sleep_time_ms, i]() { + barrier.wait(); + + // Call multithreaded_test_function + multithreaded_test_function(sleep_time_ms); + + + if(( i+1) % 2 == 0) { + named_test_function_1(sleep_time_ms); + } else { + named_test_function_2(sleep_time_ms); + } + + }); + } + + for (auto& t : threads) { + t.join(); + } + + auto tables = ctrack::result_get_tables(); + CHECK(tables.details.rows.size() == 3); // multithreaded_test_function + 2 mixed_function variants + + // Find each function in the results + const ctrack::detail_stats* multithreaded_stats = nullptr; + const ctrack::detail_stats* mixed0_stats = nullptr; + const ctrack::detail_stats* mixed1_stats = nullptr; + + for (const auto& stats : tables.details.rows) { + if (stats.function_name == "multithreaded_test_function") { + multithreaded_stats = &stats; + } else if (stats.function_name == "named_test_function_1") { + mixed0_stats = &stats; + } else if (stats.function_name == "named_test_function_2") { + mixed1_stats = &stats; + } + } + + REQUIRE(multithreaded_stats != nullptr); + REQUIRE(mixed0_stats != nullptr); + REQUIRE(mixed1_stats != nullptr); + + // multithreaded_test_function called by all 4 threads + CHECK(multithreaded_stats->calls == 4); + CHECK(multithreaded_stats->threads == 4); + + // Each mixed function called by 2 threads (i % 2) + CHECK(mixed0_stats->calls == 2); + CHECK(mixed0_stats->threads == 2); + CHECK(mixed1_stats->calls == 2); + CHECK(mixed1_stats->threads == 2); +} + +TEST_CASE("Nested function calls in multithreaded environment") { + clear_ctrack(); + const int num_threads = 3; + const int parent_sleep_ms = 50; + const int child_sleep_ms = 50; + + ThreadBarrier barrier(num_threads); + std::vector threads; + + for (int i = 0; i < num_threads; i++) { + threads.emplace_back([&barrier, parent_sleep_ms, child_sleep_ms]() { + barrier.wait(); + nested_parent(child_sleep_ms, parent_sleep_ms); + }); + } + + for (auto& t : threads) { + t.join(); + } + + auto tables = ctrack::result_get_tables(); + CHECK(tables.details.rows.size() == 2); // nested_parent + nested_child + + // Find parent and child functions + const ctrack::detail_stats* parent_stats = nullptr; + const ctrack::detail_stats* child_stats = nullptr; + + for (const auto& stats : tables.details.rows) { + if (stats.function_name == "nested_parent") { + parent_stats = &stats; + } else if (stats.function_name == "nested_child") { + child_stats = &stats; + } + + } + + REQUIRE(parent_stats != nullptr); + REQUIRE(child_stats != nullptr); + + // Both functions called by all threads + CHECK(parent_stats->calls == num_threads); + CHECK(parent_stats->threads == num_threads); + CHECK(child_stats->calls == num_threads); + CHECK(child_stats->threads == num_threads); + + // Parent time should be greater than child time (includes child + own work) + CHECK(parent_stats->time_acc > child_stats->time_acc); + + // Validate timing expectations + auto expected_child_time = std::chrono::milliseconds(num_threads * child_sleep_ms); + auto expected_parent_total_time = std::chrono::milliseconds(num_threads * (parent_sleep_ms + child_sleep_ms)); + + CHECK(within_tolerance(child_stats->time_acc, expected_child_time, 25.0)); + CHECK(within_tolerance(parent_stats->time_acc, expected_parent_total_time, 25.0)); +} + +TEST_CASE("High contention stress test") { + clear_ctrack(); + const int num_threads = 10; + const int calls_per_thread = 50; + const int sleep_time_ms = 5; // Milliseconds for stable testing + + std::vector threads; + std::atomic completed_calls{0}; + + for (int i = 0; i < num_threads; i++) { + threads.emplace_back([calls_per_thread, sleep_time_ms, &completed_calls]() { + for (int j = 0; j < calls_per_thread; j++) { + { + CTRACK; + sleep_ms(sleep_time_ms); + } + completed_calls.fetch_add(1); + } + }); + } + + for (auto& t : threads) { + t.join(); + } + + CHECK(completed_calls.load() == num_threads * calls_per_thread); + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto& stats = tables.details.rows[0]; + CHECK(stats.calls == num_threads * calls_per_thread); + CHECK(stats.threads == num_threads); + + // Verify no data corruption occurred during high contention + CHECK(stats.time_acc.count() > 0); + CHECK(stats.fastest_min.count() > 0); + CHECK(stats.center_mean.count() > 0); + CHECK(stats.cv >= 0.0); +} + +TEST_CASE("Random timing variations across threads") { + clear_ctrack(); + const int num_threads = 5; + const int calls_per_thread = 100; + + ThreadBarrier barrier(num_threads); + std::vector threads; + std::random_device rd; + + for (int i = 0; i < num_threads; i++) { + threads.emplace_back([&barrier, calls_per_thread, &rd]() { + std::mt19937 gen(rd() + static_cast(std::hash{}(std::this_thread::get_id()))); + std::uniform_int_distribution<> dis(1, 10); // 1-10ms random sleep + + barrier.wait(); + + for (int j = 0; j < calls_per_thread; j++) { + int sleep_time = dis(gen); + multithreaded_test_function(sleep_time); + } + }); + } + + for (auto& t : threads) { + t.join(); + } + + auto tables = ctrack::result_get_tables(); + const auto& stats = tables.details.rows[0]; + + CHECK(stats.calls == num_threads * calls_per_thread); + CHECK(stats.threads == num_threads); + + // With random timing, we should see some variation + CHECK(stats.cv > 0.0); // Coefficient of variation should be > 0 + CHECK(stats.fastest_min < stats.slowest_max); // Should have range + + // Basic sanity checks + CHECK(stats.fastest_min <= stats.center_mean); + CHECK(stats.center_mean <= stats.slowest_max); +} + diff --git a/test/test_nested.cpp b/test/test_nested.cpp new file mode 100644 index 0000000..90bbe51 --- /dev/null +++ b/test/test_nested.cpp @@ -0,0 +1,466 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest.h" +#include "test_helpers.hpp" +#include "ctrack.hpp" + +#include +#include + +namespace { + +// Recursive factorial function for testing recursive scenarios +int recursive_factorial(int n) { + CTRACK; + if (n <= 1) { + test_helpers::sleep_ms(5); // Small work at base case + return 1; + } + test_helpers::sleep_ms(5); // Work at each level + return n * recursive_factorial(n - 1); +} + +// Recursive fibonacci for testing deep recursion with branching +int recursive_fibonacci(int n) { + CTRACK; + if (n <= 1) { + test_helpers::sleep_ms(5); + return n; + } + test_helpers::sleep_ms(5); + return recursive_fibonacci(n - 1) + recursive_fibonacci(n - 2); +} + +// Diamond pattern helper functions +void diamond_leaf_d(int sleep_ms) { + CTRACK; + test_helpers::sleep_ms(sleep_ms); +} + +void diamond_branch_b(int sleep_ms, int leaf_sleep_ms) { + CTRACK; + test_helpers::sleep_ms(sleep_ms); + diamond_leaf_d(leaf_sleep_ms); +} + +void diamond_branch_c(int sleep_ms, int leaf_sleep_ms) { + CTRACK; + test_helpers::sleep_ms(sleep_ms); + diamond_leaf_d(leaf_sleep_ms); +} + +void diamond_root_a(int sleep_ms, int branch_sleep_ms, int leaf_sleep_ms) { + CTRACK; + test_helpers::sleep_ms(sleep_ms); + diamond_branch_b(branch_sleep_ms, leaf_sleep_ms); + diamond_branch_c(branch_sleep_ms, leaf_sleep_ms); +} + +// Deep nesting functions +void deep_level_5(int sleep_ms) { + CTRACK; + test_helpers::sleep_ms(sleep_ms); +} + +void deep_level_4(int sleep_ms, int child_sleep_ms) { + CTRACK; + test_helpers::sleep_ms(sleep_ms); + deep_level_5(child_sleep_ms); +} + +void deep_level_3(int sleep_ms, int child_sleep_ms) { + CTRACK; + test_helpers::sleep_ms(sleep_ms); + deep_level_4(sleep_ms, child_sleep_ms); +} + +void deep_level_2(int sleep_ms, int child_sleep_ms) { + CTRACK; + test_helpers::sleep_ms(sleep_ms); + deep_level_3(sleep_ms, child_sleep_ms); +} + +void deep_level_1(int sleep_ms, int child_sleep_ms) { + CTRACK; + test_helpers::sleep_ms(sleep_ms); + deep_level_2(sleep_ms, child_sleep_ms); +} + +void deep_root(int sleep_ms, int child_sleep_ms) { + CTRACK; + test_helpers::sleep_ms(sleep_ms); + deep_level_1(sleep_ms, child_sleep_ms); +} + +// Multiple children functions +void multi_child_1(int sleep_ms) { + CTRACK; + test_helpers::sleep_ms(sleep_ms); +} + +void multi_child_2(int sleep_ms) { + CTRACK; + test_helpers::sleep_ms(sleep_ms); +} + +void multi_child_3(int sleep_ms) { + CTRACK; + test_helpers::sleep_ms(sleep_ms); +} + +void multi_parent(int parent_sleep_ms, int child_sleep_ms) { + CTRACK; + test_helpers::sleep_ms(parent_sleep_ms); + multi_child_1(child_sleep_ms); + multi_child_2(child_sleep_ms); + multi_child_3(child_sleep_ms); +} + +} // anonymous namespace + +TEST_CASE("Simple nested functions - two levels") { + test_helpers::clear_ctrack(); + + // Root: 10ms, Level1: 5ms, Level2: 2ms + // Expected: root total ~17ms, root exclusive ~10ms + test_helpers::nested_root(20, 5, 2); + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 3); + + // Find each function in results and validate + bool found_root = false, found_level1 = false, found_level2 = false; + + for (const auto& stats : tables.details.rows) { + if (stats.function_name.find("nested_root") != std::string::npos) { + found_root = true; + CHECK(stats.calls == 1); + // Root exclusive time should be around 20ms (its own sleep) + CHECK(test_helpers::within_tolerance(stats.center_time_ae, std::chrono::milliseconds(20))); + // Root total time should be around 27ms (20 + 5 + 2) + CHECK(test_helpers::within_tolerance(stats.center_time_a, std::chrono::milliseconds(27))); + // Exclusive should be less than total for parent functions + CHECK(stats.center_time_ae < stats.center_time_a); + } + else if (stats.function_name.find("nested_level_1") != std::string::npos) { + found_level1 = true; + CHECK(stats.calls == 1); + // Level1 exclusive time should be around 5ms + CHECK(test_helpers::within_tolerance(stats.center_time_ae, std::chrono::milliseconds(5))); + // Level1 total time should be around 7ms (5 + 2) + CHECK(test_helpers::within_tolerance(stats.center_time_a, std::chrono::milliseconds(7))); + CHECK(stats.center_time_ae < stats.center_time_a); + } + else if (stats.function_name.find("nested_level_2") != std::string::npos) { + found_level2 = true; + CHECK(stats.calls == 1); + // Level2 is leaf: exclusive should equal total (around 2ms) + CHECK(test_helpers::within_tolerance(stats.center_time_ae, std::chrono::milliseconds(2))); + CHECK(test_helpers::within_tolerance(stats.center_time_a, std::chrono::milliseconds(2))); + CHECK(test_helpers::within_tolerance(static_cast(stats.center_time_ae.count()), static_cast(stats.center_time_a.count()), 5.0)); + } + } + + CHECK(found_root); + CHECK(found_level1); + CHECK(found_level2); +} + +TEST_CASE("Deep nesting - 6 levels") { + test_helpers::clear_ctrack(); + + // Each level sleeps 3ms, total should be 6*3 = 18ms + deep_root(3, 3); + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 6); + + bool found_deep_root = false, found_deep_level_5 = false; + + for (const auto& stats : tables.details.rows) { + if (stats.function_name.find("deep_root") != std::string::npos) { + found_deep_root = true; + CHECK(stats.calls == 1); + // Root exclusive should be ~3ms, total should be ~18ms + CHECK(test_helpers::within_tolerance(stats.center_time_ae, std::chrono::milliseconds(3))); + CHECK(test_helpers::within_tolerance(stats.center_time_a, std::chrono::milliseconds(18))); + CHECK(stats.center_time_ae < stats.center_time_a); + } + else if (stats.function_name.find("deep_level_5") != std::string::npos) { + found_deep_level_5 = true; + CHECK(stats.calls == 1); + // Leaf function: exclusive should equal total (~3ms) + CHECK(test_helpers::within_tolerance(stats.center_time_ae, std::chrono::milliseconds(3))); + CHECK(test_helpers::within_tolerance(stats.center_time_a, std::chrono::milliseconds(3))); + CHECK(test_helpers::within_tolerance(static_cast(stats.center_time_ae.count()), static_cast(stats.center_time_a.count()), 5.0)); + } + } + + CHECK(found_deep_root); + CHECK(found_deep_level_5); +} + +TEST_CASE("Multiple children per parent") { + test_helpers::clear_ctrack(); + + // Parent: 5ms, each of 3 children: 3ms + // Expected: parent total ~14ms (5 + 3*3), parent exclusive ~5ms + multi_parent(5, 3); + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 4); + + bool found_parent = false; + int found_children = 0; + + for (const auto& stats : tables.details.rows) { + if (stats.function_name.find("multi_parent") != std::string::npos) { + found_parent = true; + CHECK(stats.calls == 1); + // Parent exclusive should be ~5ms, total should be ~14ms + CHECK(test_helpers::within_tolerance(stats.center_time_ae, std::chrono::milliseconds(5))); + CHECK(test_helpers::within_tolerance(stats.center_time_a, std::chrono::milliseconds(14))); + CHECK(stats.center_time_ae < stats.center_time_a); + } + else if (stats.function_name.find("multi_child_") != std::string::npos) { + found_children++; + CHECK(stats.calls == 1); + // Each child: exclusive should equal total (~3ms) + CHECK(test_helpers::within_tolerance(stats.center_time_ae, std::chrono::milliseconds(3))); + CHECK(test_helpers::within_tolerance(stats.center_time_a, std::chrono::milliseconds(3))); + CHECK(test_helpers::within_tolerance(static_cast(stats.center_time_ae.count()), static_cast(stats.center_time_a.count()), 5.0)); + } + } + + CHECK(found_parent); + CHECK(found_children == 3); +} + +TEST_CASE("Diamond pattern - A calls B and C, both call D") { + test_helpers::clear_ctrack(); + + // A: 2ms, B: 2ms, C: 2ms, D: 3ms each call + // Expected: A total ~12ms (2 + 2*2 + 2*3), D called twice + diamond_root_a(2, 2, 3); + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 4); + + bool found_a = false, found_b = false, found_c = false, found_d = false; + + for (const auto& stats : tables.details.rows) { + if (stats.function_name.find("diamond_root_a") != std::string::npos) { + found_a = true; + CHECK(stats.calls == 1); + // A exclusive ~2ms, total ~12ms (2 + 2*2 + 2*3) + CHECK(test_helpers::within_tolerance(stats.center_time_ae, std::chrono::milliseconds(2))); + CHECK(test_helpers::within_tolerance(stats.center_time_a, std::chrono::milliseconds(12))); + } + else if (stats.function_name.find("diamond_branch_b") != std::string::npos) { + found_b = true; + CHECK(stats.calls == 1); + // B exclusive ~2ms, total ~5ms (2 + 3) + CHECK(test_helpers::within_tolerance(stats.center_time_ae, std::chrono::milliseconds(2))); + CHECK(test_helpers::within_tolerance(stats.center_time_a, std::chrono::milliseconds(5))); + } + else if (stats.function_name.find("diamond_branch_c") != std::string::npos) { + found_c = true; + CHECK(stats.calls == 1); + // C exclusive ~2ms, total ~5ms (2 + 3) + CHECK(test_helpers::within_tolerance(stats.center_time_ae, std::chrono::milliseconds(2))); + CHECK(test_helpers::within_tolerance(stats.center_time_a, std::chrono::milliseconds(5))); + } + else if (stats.function_name.find("diamond_leaf_d") != std::string::npos) { + found_d = true; + CHECK(stats.calls == 2); // Called by both B and C + // D is leaf: exclusive should equal total 6(~3ms per call) + CHECK(test_helpers::within_tolerance(stats.center_time_ae, std::chrono::milliseconds(6))); + CHECK(test_helpers::within_tolerance(stats.center_time_a, std::chrono::milliseconds(6))); + } + } + + CHECK(found_a); + CHECK(found_b); + CHECK(found_c); + CHECK(found_d); +} + +TEST_CASE("Recursive factorial - linear recursion") { + test_helpers::clear_ctrack(); + + // Calculate factorial of 5: should create 5 calls (5, 4, 3, 2, 1) + int result = recursive_factorial(5); + CHECK(result == 120); + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); // All calls are to the same function + + const auto& stats = tables.details.rows[0]; + CHECK(stats.function_name.find("recursive_factorial") != std::string::npos); + CHECK(stats.calls == 5); + + // Each call sleeps 5ms, so total active time should be ~25ms + CHECK(test_helpers::within_tolerance(stats.time_a_all, std::chrono::milliseconds(25))); + + // For recursive functions, we expect some variation in individual call times + // due to the recursive overhead, but each should be roughly 5ms + overhead + CHECK(stats.center_time_ae > std::chrono::milliseconds(0)); + CHECK(stats.center_time_a > std::chrono::milliseconds(0)); +} + +TEST_CASE("Recursive fibonacci - branching recursion") { + test_helpers::clear_ctrack(); + + // Calculate fibonacci of 4: fib(4) = fib(3) + fib(2) + // This creates multiple calls with different recursion depths + int result = recursive_fibonacci(4); + CHECK(result == 3); // fib(4) = 3 + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); // All calls are to the same function + + const auto& stats = tables.details.rows[0]; + CHECK(stats.function_name.find("recursive_fibonacci") != std::string::npos); + + // Fibonacci(4) should make these calls: + // fib(4), fib(3), fib(2), fib(1), fib(0), fib(1), fib(2), fib(1), fib(0) + // That's 9 total calls + CHECK(stats.calls == 9); + + // Each call sleeps 5ms, so total active time should be ~45ms + CHECK(test_helpers::within_tolerance(stats.time_a_all, std::chrono::milliseconds(45))); +} + +TEST_CASE("Nested functions with multiple calls to same child") { + test_helpers::clear_ctrack(); + + // Call nested_level_2 multiple times from different contexts + test_helpers::nested_level_1(3, 2); // First call path + test_helpers::nested_level_2(2); // Direct call + + auto tables = ctrack::result_get_tables(); + + bool found_level1 = false, found_level2 = false; + + for (const auto& stats : tables.details.rows) { + if (stats.function_name.find("nested_level_1") != std::string::npos) { + found_level1 = true; + CHECK(stats.calls == 1); + // Level1: 3ms own + 2ms child = 5ms total, 3ms exclusive + CHECK(test_helpers::within_tolerance(stats.center_time_ae, std::chrono::milliseconds(3))); + CHECK(test_helpers::within_tolerance(stats.center_time_a, std::chrono::milliseconds(5))); + } + else if (stats.function_name.find("nested_level_2") != std::string::npos) { + found_level2 = true; + CHECK(stats.calls == 2); // Called twice + // Each call is 2ms, total active time is 4ms + CHECK(test_helpers::within_tolerance(stats.time_a_all, std::chrono::milliseconds(4))); + CHECK(test_helpers::within_tolerance(stats.center_time_ae, std::chrono::milliseconds(4))); + CHECK(test_helpers::within_tolerance(stats.center_time_a, std::chrono::milliseconds(4))); + } + } + + CHECK(found_level1); + CHECK(found_level2); +} + +TEST_CASE("Verify parent-child time relationships in complex nesting") { + test_helpers::clear_ctrack(); + + // Create a complex call tree: + // root(5) -> level1(3) -> level2(2) + // -> level1(4) -> level2(1) + + test_helpers::nested_root(5, 3, 2); + test_helpers::nested_level_1(4, 1); + + auto tables = ctrack::result_get_tables(); + + std::chrono::nanoseconds root_total{0}, root_exclusive{0}; + std::chrono::nanoseconds level1_total_active{0}, level1_exclusive_active{0}; + std::chrono::nanoseconds level2_total_active{0}, level2_exclusive_active{0}; + + for (const auto& stats : tables.details.rows) { + if (stats.function_name.find("nested_root") != std::string::npos) { + root_total = stats.time_a_all; + root_exclusive = stats.time_ae_all; + } + else if (stats.function_name.find("nested_level_1") != std::string::npos) { + level1_total_active = stats.time_a_all; + level1_exclusive_active = stats.time_ae_all; + + } + else if (stats.function_name.find("nested_level_2") != std::string::npos) { + level2_total_active = stats.time_a_all; + level2_exclusive_active = stats.time_ae_all; + + } + } + + // Verify that parent exclusive time + children total time ≈ parent total time + // Root exclusive (5ms) + Level1 calls total (3+2+4+1 = 10ms) ≈ Root total (15ms) + auto expected_root_total = root_exclusive + level1_total_active; + CHECK(test_helpers::within_tolerance(static_cast(root_total.count()+ std::chrono::duration_cast(std::chrono::milliseconds(4)).count() ), static_cast(expected_root_total.count()), 20.0)); + + // For leaf functions, exclusive time should equal total time + CHECK(test_helpers::within_tolerance(static_cast(level2_total_active.count()), static_cast(level2_exclusive_active.count()), 5.0)); +} + +TEST_CASE("Verify time_active_exclusive calculations are correct") { + test_helpers::clear_ctrack(); + + // Simple case: parent(10ms) -> child(5ms) + // Parent total should be ~15ms, parent exclusive should be ~10ms + test_helpers::nested_level_1(10, 5); + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 2); + + for (const auto& stats : tables.details.rows) { + if (stats.function_name.find("nested_level_1") != std::string::npos) { + // Parent function + CHECK(stats.calls == 1); + + // Exclusive time should be significantly less than total time + CHECK(stats.center_time_ae < stats.center_time_a); + + // The difference should be approximately the child's execution time + auto child_time_approx = stats.center_time_a - stats.center_time_ae; + CHECK(test_helpers::within_tolerance(child_time_approx, std::chrono::milliseconds(5))); + + // Exclusive time should be approximately the parent's own work + CHECK(test_helpers::within_tolerance(stats.center_time_ae, std::chrono::milliseconds(10))); + } + else if (stats.function_name.find("nested_level_2") != std::string::npos) { + // Child (leaf) function + CHECK(stats.calls == 1); + + // For leaf functions, exclusive should equal total + CHECK(test_helpers::within_tolerance(static_cast(stats.center_time_ae.count()), static_cast(stats.center_time_a.count()), 5.0)); + + // Should be approximately 5ms + CHECK(test_helpers::within_tolerance(stats.center_time_ae, std::chrono::milliseconds(5))); + } + } +} + +TEST_CASE("Large recursion depth handling") { + test_helpers::clear_ctrack(); + + // Test with a larger factorial to ensure the library handles deeper recursion + int result = recursive_factorial(8); + CHECK(result == 40320); + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + + const auto& stats = tables.details.rows[0]; + CHECK(stats.calls == 8); + + // Total active time should be ~40ms (8 calls × 5ms each) + CHECK(test_helpers::within_tolerance(stats.time_a_all, std::chrono::milliseconds(40))); + + // Verify that the statistics are reasonable + CHECK(stats.center_time_ae > std::chrono::milliseconds(0)); + CHECK(stats.center_time_a > std::chrono::milliseconds(0)); + CHECK(stats.cv >= 0.0); // Coefficient of variation should be non-negative +} \ No newline at end of file diff --git a/test/test_results.cpp b/test/test_results.cpp new file mode 100644 index 0000000..23c868e --- /dev/null +++ b/test/test_results.cpp @@ -0,0 +1,472 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest.h" +#include "test_helpers.hpp" +#include "ctrack.hpp" + +#include +#include +#include +#include + +TEST_CASE("Summary row validation - basic fields") { + test_helpers::clear_ctrack(); + + // Execute a simple tracked function + test_helpers::test_function_with_sleep(10, "test_function"); + + auto tables = ctrack::result_get_tables(); + + REQUIRE(tables.summary.rows.size() == 1); + + const auto& row = tables.summary.rows[0]; + + // Basic field validation + CHECK(row.function_name == "test_function"); + CHECK(row.calls == 1); + CHECK(row.line > 0); + CHECK_FALSE(row.filename.empty()); + + // Percentage fields should be between 0-100 + CHECK(row.percent_ae_bracket >= 0.0); + CHECK(row.percent_ae_bracket <= 100.0); + CHECK(row.percent_ae_all >= 0.0); + CHECK(row.percent_ae_all <= 100.0); + + // Time fields should be non-negative + CHECK(row.time_ae_all.count() >= 0); + CHECK(row.time_a_all.count() >= 0); + + // For a single function, it should account for 100% of tracked time + CHECK(test_helpers::within_tolerance(row.percent_ae_all, 100.0, 5.0)); + CHECK(test_helpers::within_tolerance(row.percent_ae_bracket, 100.0, 5.0)); +} + +TEST_CASE("Summary row validation - multiple functions with different timing") { + test_helpers::clear_ctrack(); + + // Create functions with different execution times + test_helpers::test_function_with_sleep(5, "fast_function"); + test_helpers::test_function_with_sleep(20, "slow_function"); + + auto tables = ctrack::result_get_tables(); + + REQUIRE(tables.summary.rows.size() == 2); + + // Find functions by name (order might vary) + const ctrack::summary_row* fast_row = nullptr; + const ctrack::summary_row* slow_row = nullptr; + + for (const auto& row : tables.summary.rows) { + if (row.function_name == "fast_function") { + fast_row = &row; + } else if (row.function_name == "slow_function") { + slow_row = &row; + } + } + + REQUIRE(fast_row != nullptr); + REQUIRE(slow_row != nullptr); + + // Validate both rows + CHECK(test_helpers::validate_summary_row(*fast_row, "fast_function", 1)); + CHECK(test_helpers::validate_summary_row(*slow_row, "slow_function", 1)); + + // Slow function should have higher percentage than fast function + CHECK(slow_row->percent_ae_all > fast_row->percent_ae_all); + CHECK(slow_row->time_ae_all > fast_row->time_ae_all); + + // Combined percentages should approximately equal 100% + double total_percent = fast_row->percent_ae_all + slow_row->percent_ae_all; + CHECK(test_helpers::within_tolerance(total_percent, 100.0, 5.0)); +} + +TEST_CASE("Detail stats validation - comprehensive field check") { + test_helpers::clear_ctrack(); + + int calls = 100; + // Execute function multiple times for meaningful statistics + for (int i = 0; i < calls; i++) { + test_helpers::test_function_with_sleep(10, "detail_test_function"); + } + + auto tables = ctrack::result_get_tables(); + + REQUIRE(tables.details.rows.size() == 1); + + const auto& stats = tables.details.rows[0]; + + // Basic info fields + CHECK(stats.function_name == "detail_test_function"); + CHECK(stats.calls == calls); + CHECK(stats.threads == 1); + CHECK(stats.line > 0); + CHECK_FALSE(stats.filename.empty()); + + // All time fields should be non-negative + CHECK(stats.time_acc.count() >= 0); + CHECK(stats.sd.count() >= 0); + CHECK(stats.fastest_min.count() >= 0); + CHECK(stats.fastest_mean.count() >= 0); + CHECK(stats.center_min.count() >= 0); + CHECK(stats.center_mean.count() >= 0); + CHECK(stats.center_med.count() >= 0); + CHECK(stats.center_time_a.count() >= 0); + CHECK(stats.center_time_ae.count() >= 0); + CHECK(stats.center_max.count() >= 0); + CHECK(stats.slowest_mean.count() >= 0); + CHECK(stats.slowest_max.count() >= 0); + + // CV should be non-negative + CHECK(stats.cv >= 0); + + // Logical time relationships + CHECK(stats.fastest_min <= stats.fastest_mean); + CHECK(stats.fastest_mean <= stats.center_mean); + CHECK(stats.center_mean <= stats.slowest_mean); + CHECK(stats.center_min <= stats.center_max); + CHECK(stats.slowest_mean <= stats.slowest_max); + + // Range fields + CHECK(stats.fastest_range >= 0); + CHECK(stats.slowest_range >= 0); + CHECK(stats.fastest_range <= 100); + CHECK(stats.slowest_range <= 100); + + // Use validation helper + CHECK(test_helpers::validate_detail_stats(stats, "detail_test_function", calls, 1)); +} + +TEST_CASE("Result meta information validation") { + test_helpers::clear_ctrack(); + + auto start = std::chrono::high_resolution_clock::now(); + test_helpers::test_function_with_sleep(10, "meta_test"); + test_helpers::test_function_with_sleep(15, "meta_test2"); + auto end = std::chrono::high_resolution_clock::now(); + + auto tables = ctrack::result_get_tables(); + + // Meta information should be valid + CHECK(tables.time_total.count() > 0); + CHECK(tables.time_ctracked.count() > 0); + CHECK(tables.time_ctracked <= tables.time_total); + CHECK(tables.start_time <= tables.end_time); + + // Time range should be reasonable + auto expected_duration = std::chrono::duration_cast(end - start); + CHECK(tables.time_total <= expected_duration * 2); // Allow some overhead + + // Settings should be preserved + CHECK(tables.settings.non_center_percent == 1); // Default value + CHECK(tables.settings.min_percent_active_exclusive == 0.0); // Default value + CHECK(tables.settings.percent_exclude_fastest_active_exclusive == 0.0); // Default value +} + +TEST_CASE("Result settings filtering - min_percent_active_exclusive") { + test_helpers::clear_ctrack(); + + // Create functions with significantly different execution times + // Small function: ~25ms total + for (int i = 0; i < 5; i++) { + test_helpers::test_function_with_sleep(5, "small_function"); + } + + // Large function: ~100ms total + for (int i = 0; i < 10; i++) { + test_helpers::test_function_with_sleep(10, "large_function"); + } + + // Test with no filtering (default) + { + ctrack::ctrack_result_settings settings; + settings.min_percent_active_exclusive = 0.0; + + auto tables = ctrack::result_get_tables(settings); + + // Should include both functions + CHECK(tables.summary.rows.size() == 2); + CHECK(tables.details.rows.size() == 2); + } + + test_helpers::clear_ctrack(); + + // Recreate the same pattern + for (int i = 0; i < 5; i++) { + test_helpers::test_function_with_sleep(5, "small_function"); + } + for (int i = 0; i < 10; i++) { + test_helpers::test_function_with_sleep(10, "large_function"); + } + + // Test with filtering that should exclude small function + { + ctrack::ctrack_result_settings settings; + settings.min_percent_active_exclusive = 25.0; // Should filter out small contributors + + auto tables = ctrack::result_get_tables(settings); + + // Should only include large_function + CHECK(tables.summary.rows.size() == 1); + CHECK(tables.details.rows.size() == 1); + + if (!tables.summary.rows.empty()) { + CHECK(tables.summary.rows[0].function_name == "large_function"); + } + if (!tables.details.rows.empty()) { + CHECK(tables.details.rows[0].function_name == "large_function"); + } + } +} + +TEST_CASE("Result settings - percent_exclude_fastest_active_exclusive") { + test_helpers::clear_ctrack(); + int calls = 100; + // Execute function multiple times to have fastest/slowest distribution + for (int i = 0; i < calls; i++) { + // Vary sleep time slightly to create distribution + int sleep_time = 8 + (i % 5); // 8-12ms range + test_helpers::test_function_with_sleep(sleep_time, "variable_function"); + } + + // Test with different exclusion percentages + ctrack::ctrack_result_settings settings; + settings.percent_exclude_fastest_active_exclusive = 10.0; // Exclude fastest 10% + + auto tables = ctrack::result_get_tables(settings); + + REQUIRE(tables.details.rows.size() == 1); + + const auto& stats = tables.details.rows[0]; + CHECK(stats.function_name == "variable_function"); + CHECK(stats.calls == calls); + + // Settings should be preserved + CHECK(tables.settings.percent_exclude_fastest_active_exclusive == 10.0); + + // Validate the stats structure + CHECK(test_helpers::validate_detail_stats(stats, "variable_function", calls, 1)); +} + +TEST_CASE("Result settings - non_center_percent variations") { + test_helpers::clear_ctrack(); + + // Execute function multiple times + for (int i = 0; i < 50; i++) { + test_helpers::test_function_with_sleep(5, "center_test"); + } + + // Test different non_center_percent values + std::vector test_values = {1, 5, 10}; + + for (unsigned int center_percent : test_values) { + test_helpers::clear_ctrack(); + + // Recreate the same pattern + for (int i = 0; i < 50; i++) { + test_helpers::test_function_with_sleep(5, "center_test"); + } + + ctrack::ctrack_result_settings settings; + settings.non_center_percent = center_percent; + + auto tables = ctrack::result_get_tables(settings); + + REQUIRE(tables.details.rows.size() == 1); + + const auto& stats = tables.details.rows[0]; + + // Settings should be preserved + CHECK(tables.settings.non_center_percent == center_percent); + + // Range fields should reflect the percentage + CHECK(stats.fastest_range == center_percent); + CHECK(stats.slowest_range == 100-center_percent); + + // Validate structure + CHECK(test_helpers::validate_detail_stats(stats, "center_test", 50, 1)); + } +} + +TEST_CASE("Summary table sorting and ordering") { + test_helpers::clear_ctrack(); + + // Create functions with different execution times and call counts + test_helpers::test_function_with_sleep(5, "function_a"); + + for (int i = 0; i < 3; i++) { + test_helpers::test_function_with_sleep(10, "function_b"); + } + + test_helpers::test_function_with_sleep(30, "function_c"); + + auto tables = ctrack::result_get_tables(); + + REQUIRE(tables.summary.rows.size() == 3); + + // Rows should be ordered by total time (descending) + // function_c should be first (30ms), then function_b (30ms total), then function_a (5ms) + for (size_t i = 0; i < tables.summary.rows.size() - 1; i++) { + CHECK(tables.summary.rows[i].time_ae_all >= tables.summary.rows[i + 1].time_ae_all); + } + + // Verify all rows are valid + for (const auto& row : tables.summary.rows) { + CHECK(row.calls > 0); + CHECK(row.line > 0); + CHECK_FALSE(row.function_name.empty()); + CHECK_FALSE(row.filename.empty()); + CHECK(row.percent_ae_all >= 0.0); + CHECK(row.percent_ae_all <= 100.0); + CHECK(row.time_ae_all.count() >= 0); + CHECK(row.time_a_all.count() >= 0); + } +} + +TEST_CASE("Empty results - no tracked functions") { + test_helpers::clear_ctrack(); + + // Don't execute any tracked functions + auto tables = ctrack::result_get_tables(); + + // Should have empty tables + CHECK(tables.summary.rows.empty()); + CHECK(tables.details.rows.empty()); + + // Meta information should still be valid + CHECK(tables.time_total.count() >= 0); + CHECK(tables.time_ctracked.count() >= 0); + CHECK(tables.start_time <= tables.end_time); + + // Settings should have default values + CHECK(tables.settings.non_center_percent == 1); + CHECK(tables.settings.min_percent_active_exclusive == 0.0); + CHECK(tables.settings.percent_exclude_fastest_active_exclusive == 0.0); +} + +TEST_CASE("Complex settings combination") { + test_helpers::clear_ctrack(); + + // Create a complex scenario with multiple functions + for (int i = 0; i < 10; i++) { + test_helpers::test_function_with_sleep(1, "tiny_function"); + } + + for (int i = 0; i < 20; i++) { + test_helpers::test_function_with_sleep(5, "small_function"); + } + + for (int i = 0; i < 10; i++) { + test_helpers::test_function_with_sleep(20, "large_function"); + } + + // Apply complex filtering + ctrack::ctrack_result_settings settings; + settings.non_center_percent = 5; + settings.min_percent_active_exclusive = 15.0; // Should filter out tiny_function + settings.percent_exclude_fastest_active_exclusive = 5.0; + + auto tables = ctrack::result_get_tables(settings); + + // Should filter out tiny_function due to min_percent_active_exclusive + CHECK(tables.summary.rows.size() <= 2); + CHECK(tables.details.rows.size() <= 2); + + // Settings should be preserved + CHECK(tables.settings.non_center_percent == 5); + CHECK(tables.settings.min_percent_active_exclusive == 15.0); + CHECK(tables.settings.percent_exclude_fastest_active_exclusive == 5.0); + + // Verify all remaining functions meet the criteria + for (const auto& row : tables.summary.rows) { + CHECK(row.function_name != "tiny_function"); // Should be filtered out + CHECK(test_helpers::validate_summary_row(row, row.function_name, row.calls)); + } + + for (const auto& stats : tables.details.rows) { + CHECK(stats.function_name != "tiny_function"); // Should be filtered out + CHECK(test_helpers::validate_detail_stats(stats, stats.function_name, stats.calls, stats.threads)); + + // Range percentages should match settings + CHECK(stats.fastest_range == 5); + CHECK(stats.slowest_range == 95); + } +} + +TEST_CASE("Multiple calls same function - statistical validation") { + test_helpers::clear_ctrack(); + + // Execute the same function with varying sleep times + std::vector sleep_times = {5, 8, 10, 12, 15, 18, 20, 22, 25, 30}; + + for (int sleep_time : sleep_times) { + test_helpers::test_function_with_sleep(sleep_time, "stats_function"); + } + + auto tables = ctrack::result_get_tables(); + + REQUIRE(tables.summary.rows.size() == 1); + REQUIRE(tables.details.rows.size() == 1); + + const auto& summary = tables.summary.rows[0]; + const auto& details = tables.details.rows[0]; + + // Summary validation + CHECK(summary.function_name == "stats_function"); + CHECK(summary.calls == 10); + CHECK(test_helpers::validate_summary_row(summary, "stats_function", 10)); + + // Details validation + CHECK(details.function_name == "stats_function"); + CHECK(details.calls == 10); + CHECK(details.threads == 1); + CHECK(test_helpers::validate_detail_stats(details, "stats_function", 10, 1)); + + // Statistical properties + CHECK(details.cv >= 0.0); // Coefficient of variation should be positive for varied data + CHECK(details.sd.count() > 0); // Standard deviation should be positive for varied data + + // Time ordering should be maintained + + CHECK(details.center_min <= details.center_mean); + CHECK(details.center_mean <= details.center_max); + +} + +TEST_CASE("Result consistency across multiple calls") { + test_helpers::clear_ctrack(); + + // Execute some functions + test_helpers::test_function_with_sleep(10, "consistent_test"); + test_helpers::test_function_with_sleep(15, "consistent_test2"); + + // Get results multiple times with same settings + ctrack::ctrack_result_settings settings; + settings.non_center_percent = 2; + + auto tables1 = ctrack::result_get_tables(settings); + + test_helpers::clear_ctrack(); + + // Recreate same pattern + test_helpers::test_function_with_sleep(10, "consistent_test"); + test_helpers::test_function_with_sleep(15, "consistent_test2"); + + auto tables2 = ctrack::result_get_tables(settings); + + // Results should be consistent + CHECK(tables1.summary.rows.size() == tables2.summary.rows.size()); + CHECK(tables1.details.rows.size() == tables2.details.rows.size()); + + // Settings should match + CHECK(tables1.settings.non_center_percent == tables2.settings.non_center_percent); + CHECK(tables1.settings.min_percent_active_exclusive == tables2.settings.min_percent_active_exclusive); + CHECK(tables1.settings.percent_exclude_fastest_active_exclusive == tables2.settings.percent_exclude_fastest_active_exclusive); + + // Function names and call counts should match + if (tables1.summary.rows.size() == tables2.summary.rows.size()) { + for (size_t i = 0; i < tables1.summary.rows.size(); i++) { + CHECK(tables1.summary.rows[i].function_name == tables2.summary.rows[i].function_name); + CHECK(tables1.summary.rows[i].calls == tables2.summary.rows[i].calls); + } + } +} \ No newline at end of file diff --git a/test/test_statistics.cpp b/test/test_statistics.cpp new file mode 100644 index 0000000..8771227 --- /dev/null +++ b/test/test_statistics.cpp @@ -0,0 +1,559 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest.h" +#include "test_helpers.hpp" +#include "ctrack.hpp" +#include +#include +#include +#include + +// Helper function to test statistical calculations with known timing patterns +void test_function_with_sleep(int sleep_ms) { + CTRACK; + test_helpers::sleep_ms(sleep_ms); +} + +TEST_CASE("High variance timing statistics") { + test_helpers::clear_ctrack(); + + // Test with high variance sleep times from 5ms to 50ms + std::vector sleep_times = {5, 8, 10, 20, 50, 10, 5, 30, 15, 6}; + for (int ms : sleep_times) { + test_function_with_sleep(ms); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + const auto& stats = tables.details.rows[0]; + + test_helpers::ExpectedStats expected(sleep_times); + + // Basic validation + CHECK(stats.calls == static_cast(sleep_times.size())); + CHECK(stats.function_name == "test_function_with_sleep"); + CHECK(stats.threads == 1); + + // Timing validation with generous tolerance for sleep timing variability + CHECK(test_helpers::within_tolerance(stats.center_min, expected.min, 30.0)); + CHECK(test_helpers::within_tolerance(stats.center_max, expected.max, 30.0)); + CHECK(test_helpers::within_tolerance(stats.center_mean, expected.mean, 25.0)); + + // Statistical measures validation + CHECK(test_helpers::within_tolerance(static_cast(stats.sd.count()), expected.std_dev_ns, 35.0)); + CHECK(test_helpers::within_tolerance(stats.cv, expected.cv, 35.0)); + + // Verify CV = sd/mean relationship + double calculated_cv = static_cast(stats.sd.count()) / static_cast(stats.center_mean.count()); + CHECK(test_helpers::within_tolerance(stats.cv, calculated_cv, 5.0)); + + // Logical relationships + CHECK(stats.center_min <= stats.center_mean); + CHECK(stats.center_mean <= stats.center_max); + CHECK(stats.sd.count() >= 0); + CHECK(stats.cv >= 0.0); +} + +TEST_CASE("Low variance timing statistics") { + test_helpers::clear_ctrack(); + + // Test with low variance - all times similar (10ms +/- 1ms) + std::vector sleep_times = {9, 10, 11, 10, 9, 10, 11, 10, 9, 10}; + for (int ms : sleep_times) { + test_function_with_sleep(ms); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + const auto& stats = tables.details.rows[0]; + + test_helpers::ExpectedStats expected(sleep_times); + + // Basic validation + CHECK(stats.calls == static_cast(sleep_times.size())); + + // Low variance should result in low CV + CHECK(stats.cv < 0.5); // CV should be relatively low for similar times + + // Standard deviation should be relatively small + double mean_val = static_cast(stats.center_mean.count()); + double sd_val = static_cast(stats.sd.count()); + CHECK(sd_val / mean_val < 0.5); // SD should be less than 50% of mean for low variance + + // Verify statistical calculations + CHECK(test_helpers::within_tolerance(stats.center_mean, expected.mean, 20.0)); + CHECK(test_helpers::within_tolerance(static_cast(stats.sd.count()), expected.std_dev_ns, 30.0)); + CHECK(test_helpers::within_tolerance(stats.cv, expected.cv, 30.0)); +} + +TEST_CASE("Zero variance - identical timing") { + test_helpers::clear_ctrack(); + + // All identical sleep times + std::vector sleep_times = {10, 10, 10, 10, 10}; + for (int ms : sleep_times) { + test_function_with_sleep(ms); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + const auto& stats = tables.details.rows[0]; + + // With identical times, min, mean, and max should be very close + CHECK(test_helpers::within_tolerance(stats.center_min, stats.center_mean, 10.0)); + CHECK(test_helpers::within_tolerance(stats.center_mean, stats.center_max, 10.0)); + + // CV should be very low (approaching zero) for identical times + CHECK(stats.cv < 0.2); // Very low coefficient of variation + + // Standard deviation should be very low for identical times + double mean_val = static_cast(stats.center_mean.count()); + double sd_val = static_cast(stats.sd.count()); + CHECK(sd_val / mean_val < 0.2); // Very low relative standard deviation +} + +TEST_CASE("Bimodal distribution timing") { + test_helpers::clear_ctrack(); + + // Two distinct clusters: fast (5ms) and slow (25ms) + std::vector sleep_times = {5, 5, 5, 5, 25, 25, 25, 25}; + for (int ms : sleep_times) { + test_function_with_sleep(ms); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + const auto& stats = tables.details.rows[0]; + + test_helpers::ExpectedStats expected(sleep_times); + + // Bimodal distributions typically have high variance + CHECK(stats.cv > 0.3); // Should have relatively high CV due to bimodal nature + + // Min and max should span the two clusters + CHECK(test_helpers::within_tolerance(stats.center_min, std::chrono::nanoseconds(5 * 1000000), 25.0)); + CHECK(test_helpers::within_tolerance(stats.center_max, std::chrono::nanoseconds(25 * 1000000), 25.0)); + + // Mean should be between the two clusters + CHECK(stats.center_mean > std::chrono::nanoseconds(10 * 1000000)); // Greater than fast cluster + CHECK(stats.center_mean < std::chrono::nanoseconds(20 * 1000000)); // Less than slow cluster + + // Verify statistical calculations + CHECK(test_helpers::within_tolerance(stats.center_mean, expected.mean, 25.0)); + CHECK(test_helpers::within_tolerance(static_cast(stats.sd.count()), expected.std_dev_ns, 35.0)); + CHECK(test_helpers::within_tolerance(stats.cv, expected.cv, 35.0)); +} + +TEST_CASE("Percentile calculations with different settings") { + + + // Use a wide range of sleep times for percentile testing + std::vector sleep_times = {50, 70, 100, 120, 150, 200, 250, 300, 450, 600}; + + + // Test with default settings (5% exclusion on each end) + { + test_helpers::clear_ctrack(); + for (int ms : sleep_times) { + test_function_with_sleep(ms); + } + + ctrack::ctrack_result_settings settings; + settings.non_center_percent = 5; + auto tables = ctrack::result_get_tables(settings); + REQUIRE(tables.details.rows.size() == 1); + const auto& stats = tables.details.rows[0]; + + // With 5% on each side center is still all events + CHECK(stats.center_min < std::chrono::nanoseconds(70 * 1000000)); + CHECK(stats.center_max > std::chrono::nanoseconds(600* 1000000)); + + // Percentile ranges should reflect the settings + CHECK(stats.fastest_range == 5); + CHECK(stats.slowest_range == 95); + } + + // Test with 10% exclusion + { + test_helpers::clear_ctrack(); + for (int ms : sleep_times) { + test_function_with_sleep(ms); + } + + ctrack::ctrack_result_settings settings; + settings.non_center_percent = 10; + auto tables = ctrack::result_get_tables(settings); + REQUIRE(tables.details.rows.size() == 1); + const auto& stats = tables.details.rows[0]; + + // With 10% exclusion on 10 samples, should exclude more outliers + CHECK(stats.fastest_range == 10); + CHECK(stats.slowest_range == 90); + + // Center range should be even more constrained + CHECK(stats.center_min >= std::chrono::nanoseconds(60 * 1000000)); // Should exclude 5ms + CHECK(stats.center_max <= std::chrono::nanoseconds(600 * 1000000)); // Should exclude 60ms + } + + // Test with 1% exclusion (minimal outlier removal) + { + test_helpers::clear_ctrack(); + for (int ms : sleep_times) { + test_function_with_sleep(ms); + } + + ctrack::ctrack_result_settings settings; + settings.non_center_percent = 1; + auto tables = ctrack::result_get_tables(settings); + REQUIRE(tables.details.rows.size() == 1); + const auto& stats = tables.details.rows[0]; + + CHECK(stats.fastest_range == 1); + CHECK(stats.slowest_range == 99); + + // With 1% exclusion, center should be very close to full range + CHECK(test_helpers::within_tolerance(stats.center_min, std::chrono::nanoseconds(50 * 1000000), 30.0)); + CHECK(test_helpers::within_tolerance(stats.center_max, std::chrono::nanoseconds(600 * 1000000), 30.0)); + } +} + +TEST_CASE("Outlier handling verification") { + test_helpers::clear_ctrack(); + + // Create dataset with clear outliers + std::vector sleep_times = {100, 10, 10, 10, 10, 10, 10, 10, 10, 5}; // 100ms and 5ms are outliers + for (int ms : sleep_times) { + test_function_with_sleep(ms); + } + + // Test with 10% exclusion to remove outliers + ctrack::ctrack_result_settings settings; + settings.non_center_percent = 10; + auto tables = ctrack::result_get_tables(settings); + REQUIRE(tables.details.rows.size() == 1); + const auto& stats = tables.details.rows[0]; + + // Center statistics should exclude the extreme outliers + CHECK(stats.center_min >= std::chrono::nanoseconds(5 * 1000000)); // Should exclude 5ms + CHECK(stats.center_max < std::chrono::nanoseconds(50 * 1000000)); // Should exclude 100ms + + // Center mean should be close to 10ms (the dominant value) + CHECK(test_helpers::within_tolerance(stats.center_mean, std::chrono::nanoseconds(10 * 1000000), 25.0)); + + + // Verify that fastest and slowest stats capture the outliers + CHECK(stats.fastest_min <= std::chrono::nanoseconds(6 * 1000000)); // Should include 5ms + CHECK(stats.fastest_min >= std::chrono::nanoseconds(5 * 1000000)); // Should include 5ms + CHECK(stats.slowest_max >= std::chrono::nanoseconds(50 * 1000000)); // Should include 100ms +} + +TEST_CASE("Statistical consistency across multiple calls") { + test_helpers::clear_ctrack(); + + // Test with systematic pattern + std::vector sleep_times = {5, 10, 15, 20, 25}; + + // Call each timing multiple times to increase sample size + for (int repeat = 0; repeat < 4; ++repeat) { + for (int ms : sleep_times) { + test_function_with_sleep(ms); + } + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + const auto& stats = tables.details.rows[0]; + + // Should have 20 total calls (4 repeats * 5 different times) + CHECK(stats.calls == 20); + + // Statistical measures should be consistent with the pattern + test_helpers::ExpectedStats expected({5, 10, 15, 20, 25}); // Use single instance for expected + + // Mean should be around 15ms (middle value) + CHECK(test_helpers::within_tolerance(stats.center_mean, std::chrono::nanoseconds(15 * 1000000), 25.0)); + + // CV should reflect the systematic spread + CHECK(stats.cv > 0.2); // Should have reasonable variance + CHECK(stats.cv < 0.8); // But not excessive + + // Verify statistical calculations remain consistent + double calculated_cv = static_cast(stats.sd.count()) / static_cast(stats.center_mean.count()); + CHECK(test_helpers::within_tolerance(stats.cv, calculated_cv, 5.0)); +} + +TEST_CASE("Edge case: single execution") { + test_helpers::clear_ctrack(); + + // Single execution + test_function_with_sleep(10); + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + const auto& stats = tables.details.rows[0]; + + CHECK(stats.calls == 1); + + // With single execution, min == mean == max + CHECK(stats.center_min == stats.center_mean); + CHECK(stats.center_mean == stats.center_max); + + // Standard deviation should be zero or very close to zero + CHECK(stats.sd.count() < 1000000); // Less than 1ms (should be 0 or very small) + + // CV should be zero or very close to zero for single execution + CHECK(stats.cv < 0.01); // Very low CV for single execution + +} + +TEST_CASE("Statistical validation with extreme values") { + test_helpers::clear_ctrack(); + + // Test with extreme timing differences + std::vector sleep_times = {5, 6, 100, 5, 6}; // Mix of very fast and one very slow + for (int ms : sleep_times) { + test_function_with_sleep(ms); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + const auto& stats = tables.details.rows[0]; + + test_helpers::ExpectedStats expected(sleep_times); + + // Should have very high CV due to extreme difference + CHECK(stats.cv > 1.0); // CV > 100% indicates high variability + + // Standard deviation should be large relative to mean + double mean_val = static_cast(stats.center_mean.count()); + double sd_val = static_cast(stats.sd.count()); + CHECK(sd_val > mean_val * 0.5); // SD should be significant portion of mean + + // Verify calculations with generous tolerance due to extreme values + CHECK(test_helpers::within_tolerance(stats.center_mean, expected.mean, 40.0)); + CHECK(test_helpers::within_tolerance(static_cast(stats.sd.count()), expected.std_dev_ns, 50.0)); + CHECK(test_helpers::within_tolerance(stats.cv, expected.cv, 50.0)); + + // CV consistency check + double calculated_cv = sd_val / mean_val; + CHECK(test_helpers::within_tolerance(stats.cv, calculated_cv, 10.0)); +} + +TEST_CASE("Percentile range validation") { + test_helpers::clear_ctrack(); + + // Create dataset with 20 samples for clear percentile testing + std::vector sleep_times; + for (int i = 1; i <= 20; ++i) { + sleep_times.push_back(i + 4); // 5ms to 24ms + } + + for (int ms : sleep_times) { + test_function_with_sleep(ms); + } + + // Test 5% exclusion (should exclude 1 sample from each end with 20 samples) + ctrack::ctrack_result_settings settings; + settings.non_center_percent = 5; + auto tables = ctrack::result_get_tables(settings); + REQUIRE(tables.details.rows.size() == 1); + const auto& stats = tables.details.rows[0]; + + // With 5% exclusion on 20 samples, should exclude fastest (1ms) and slowest (24ms) + CHECK(stats.center_min > std::chrono::nanoseconds(1 * 1000000)); + CHECK(stats.center_max < std::chrono::nanoseconds(24 * 1000000)); + + // Center mean should be around 14.5ms + CHECK(test_helpers::within_tolerance(stats.center_mean, std::chrono::nanoseconds(14500000), 20.0)); + + // Fastest and slowest should capture the extremes + CHECK(test_helpers::within_tolerance(stats.fastest_min, std::chrono::nanoseconds(5 * 1000000), 25.0)); + CHECK(test_helpers::within_tolerance(stats.slowest_max, std::chrono::nanoseconds(24 * 1000000), 25.0)); +} + +TEST_CASE("Complex bimodal distribution with statistical validation") { + test_helpers::clear_ctrack(); + + // Create a more complex bimodal distribution + // Fast cluster: 3-7ms, Slow cluster: 30-40ms + std::vector sleep_times = { + 3, 4, 5, 6, 7, // Fast cluster + 30, 32, 35, 38, 40, // Slow cluster + 4, 5, 6, // More fast samples + 31, 36, 39 // More slow samples + }; + + for (int ms : sleep_times) { + test_function_with_sleep(ms); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + const auto& stats = tables.details.rows[0]; + + test_helpers::ExpectedStats expected(sleep_times); + + // Bimodal should have high CV due to two distinct clusters + CHECK(stats.cv > 0.4); // High coefficient of variation + + // Mean should be between the clusters but closer to center + CHECK(stats.center_mean > std::chrono::nanoseconds(15 * 1000000)); + CHECK(stats.center_mean < std::chrono::nanoseconds(25 * 1000000)); + + // Standard deviation should be significant + double mean_val = static_cast(stats.center_mean.count()); + double sd_val = static_cast(stats.sd.count()); + CHECK(sd_val > mean_val * 0.3); // SD should be substantial portion of mean + + // Verify statistical accuracy with generous tolerance for bimodal + CHECK(test_helpers::within_tolerance(stats.center_mean, expected.mean, 30.0)); + CHECK(test_helpers::within_tolerance(static_cast(stats.sd.count()), expected.std_dev_ns, 40.0)); + CHECK(test_helpers::within_tolerance(stats.cv, expected.cv, 40.0)); + + // CV formula verification + double calculated_cv = sd_val / mean_val; + CHECK(test_helpers::within_tolerance(stats.cv, calculated_cv, 5.0)); +} + +TEST_CASE("Extreme outlier impact on percentile exclusion") { + + + // Dataset with extreme outliers to test percentile robustness + std::vector sleep_times = { + 10, 10, 10, 10, 10, // Normal cluster + 10, 10, 10, 10, 10, // More normal + 1, // Extreme fast outlier + 500 // Extreme slow outlier + }; + + + // Test different exclusion levels + std::vector exclusion_levels = {1.0, 5.0, 10.0, 20.0}; + + for (double exclusion : exclusion_levels) { + test_helpers::clear_ctrack(); + for (int ms : sleep_times) { + test_function_with_sleep(ms); + } + + ctrack::ctrack_result_settings settings; + settings.non_center_percent = static_cast(exclusion); + auto tables = ctrack::result_get_tables(settings); + REQUIRE(tables.details.rows.size() == 1); + const auto& stats = tables.details.rows[0]; + + // Higher exclusion should result in center stats closer to 10ms + if (exclusion >= 10.0) { + // Should exclude outliers effectively + CHECK(test_helpers::within_tolerance(stats.center_mean, std::chrono::nanoseconds(10 * 1000000), 15.0)); + } + + // Verify percentile ranges are set correctly + CHECK(stats.fastest_range == static_cast(exclusion)); + CHECK(stats.slowest_range == static_cast(100-exclusion)); + + // Logical consistency checks + CHECK(stats.center_min <= stats.center_mean); + CHECK(stats.center_mean <= stats.center_max); + + } +} + +TEST_CASE("Progressive variance analysis") { + test_helpers::clear_ctrack(); + + // Test with progressively increasing variance + std::vector> variance_tests = { + {10, 10, 10, 10, 10}, // No variance + {9, 10, 10, 10, 11}, // Very low variance + {8, 9, 10, 11, 12}, // Low variance + {5, 8, 10, 12, 15}, // Medium variance + {2, 6, 10, 14, 18}, // High variance + {1, 3, 10, 17, 20} // Very high variance + }; + + std::vector expected_cv_progression; + + for (size_t test_idx = 0; test_idx < variance_tests.size(); ++test_idx) { + test_helpers::clear_ctrack(); + + const auto& sleep_times = variance_tests[test_idx]; + for (int ms : sleep_times) { + test_function_with_sleep(ms); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + const auto& stats = tables.details.rows[0]; + + test_helpers::ExpectedStats expected(sleep_times); + + // Verify statistical calculations + CHECK(test_helpers::within_tolerance(stats.center_mean, expected.mean, 25.0)); + CHECK(test_helpers::within_tolerance_absolute(stats.sd.count(), static_cast( expected.std_dev_ns), 10*1000000)); + CHECK(std::abs(stats.cv- expected.cv) < 0.05); + + // Store CV for progression analysis + expected_cv_progression.push_back(stats.cv); + + // CV should increase with variance (except for first case which might be ~0) + if (test_idx > 0) { + // Each test should have higher or equal CV than the previous + // (allowing some tolerance for measurement variance) + CHECK(stats.cv >= expected_cv_progression[test_idx - 1] - 0.1); + } + + // Verify CV formula consistency + double mean_val = static_cast(stats.center_mean.count()); + double sd_val = static_cast(stats.sd.count()); + if (mean_val > 0) { + double calculated_cv = sd_val / mean_val; + CHECK(test_helpers::within_tolerance(stats.cv, calculated_cv, 5.0)); + } + } + + // Overall progression check: CV should generally increase + CHECK(expected_cv_progression.back() > expected_cv_progression.front()); +} + +TEST_CASE("Statistical stability with large sample sizes") { + test_helpers::clear_ctrack(); + + // Test statistical stability with 100 samples + std::vector base_pattern = {8, 10, 12}; // Simple pattern + std::vector large_sample; + + // Repeat pattern to create 99 samples (33 of each) + for (int repeat = 0; repeat < 33; ++repeat) { + for (int ms : base_pattern) { + large_sample.push_back(ms); + } + } + + for (int ms : large_sample) { + test_function_with_sleep(ms); + } + + auto tables = ctrack::result_get_tables(); + REQUIRE(tables.details.rows.size() == 1); + const auto& stats = tables.details.rows[0]; + + CHECK(stats.calls == 99); + + // With large sample, statistics should be very stable + // Mean should be exactly 10ms (perfect average of 8, 10, 12) + CHECK(test_helpers::within_tolerance(stats.center_mean, std::chrono::nanoseconds(10 * 1000000), 15.0)); + + // CV should be consistent with the pattern + test_helpers::ExpectedStats expected(base_pattern); + CHECK(test_helpers::within_tolerance(stats.cv, expected.cv, 20.0)); + + // Standard deviation should be stable + double mean_val = static_cast(stats.center_mean.count()); + double sd_val = static_cast(stats.sd.count()); + double calculated_cv = sd_val / mean_val; + CHECK(test_helpers::within_tolerance(stats.cv, calculated_cv, 3.0)); // Very tight tolerance for large sample + + // Min and max should match the pattern + CHECK(test_helpers::within_tolerance(stats.center_min, std::chrono::nanoseconds(8 * 1000000), 20.0)); + CHECK(test_helpers::within_tolerance(stats.center_max, std::chrono::nanoseconds(12 * 1000000), 20.0)); +} \ No newline at end of file