diff --git a/.clang-tidy b/.clang-tidy index ecd6c9c96..ac2a0b155 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -25,6 +25,13 @@ Checks: > -cert-dcl37-c, -cert-dcl51-cpp, -cert-dcl16-c, + -cert-dcl03-c, + # cert-dcl03-c suggests replacing assert() with static_assert. Disabled + # because DASHER_ASSERT (now mapped to assert() in Debug builds per + # Tier 1 #2) is used for runtime invariants that cannot be evaluated + # at compile time (e.g. DASHER_ASSERT(false) for "should never happen" + # branches, DASHER_ASSERT(p != nullptr) for runtime pointers). The + # check fires on every such site. clang-analyzer-*, -clang-analyzer-core.NullDereference, -clang-analyzer-core.CallAndMessage, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3673771d..ea92c31d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,9 +108,18 @@ jobs: - name: Check for warnings run: | - # Filter out compiler warnings ([-Wxxx]) and command-line noise; - # only fail on actual clang-tidy findings ([check-name] pattern) - TIDY_WARNINGS=$(grep "warning:" clang-tidy-output.txt | grep -v '\[-W' | grep -v 'command-line option' || true) + # Filter out compiler warnings and command-line noise; only fail + # on actual clang-tidy findings ([check-name] pattern). + # Both forms of compiler warning are excluded: + # [-Wfoo] — direct from the compiler + # [clang-diagnostic-foo] — same warnings, surfaced via clang-tidy + # The latter is what clang-tidy emits when it wraps a -W warning + # into its own [check-name] reporting format. + TIDY_WARNINGS=$(grep "warning:" clang-tidy-output.txt \ + | grep -v '\[-W' \ + | grep -v '\[clang-diagnostic-' \ + | grep -v 'command-line option' \ + || true) if [ -n "$TIDY_WARNINGS" ]; then echo "::error::clang-tidy found issues" echo "$TIDY_WARNINGS" diff --git a/.gitignore b/.gitignore index 9aad6d440..bcaf3b2cf 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ # Build directories build/ +build-*/ build-sanitize/ Build/ out/ @@ -42,6 +43,13 @@ Testing/ # Build logs build_log.txt +*.log.txt + +# Local helper scripts and scratch files (prefixed with underscore) +_*.ps1 +_*.bat +_*.sh +_*.py # IDE .autotools @@ -70,6 +78,12 @@ Data/dasher_settings.xml *.settings.xml dasher_settings.xml dasher.log + +# Training files written by the engine to CWD when contexts are destroyed. +# This is a real bug (Tier 1 item: library should not write to CWD) — for +# now we ignore the leaked files so they don't pollute git status. +training_*.txt + build-san/ build-tidy/ build-san/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 4904a6631..caf6e4127 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,12 @@ add_compile_options( add_compile_options( $<$:/W4> $<$:/permissive-> + # C4100 (unre-ferenced formal parameter) — disabled globally. The legacy + # codebase has hundreds of inherited virtual method overrides where + # parameters are intentionally unused in specific subclasses. This is a + # style issue, not a bug. The bug-finding warnings (C4701 uninitialized, + # C4244 narrowing) remain enabled. + $<$:/wd4100> ) ############################### @@ -133,14 +139,6 @@ list(REMOVE_ITEM SOURCE_FILES "${CMAKE_CURRENT_LIST_DIR}/src/DasherCore/Common/Unicode/original_harness.c" ) -# Still in the library but not actively used anymore. Also not supported on all platforms -list(REMOVE_ITEM HEADER_FILES "${CMAKE_CURRENT_LIST_DIR}/src/DasherCore/CannaConversionHelper.h") -list(REMOVE_ITEM HEADER_FILES "${CMAKE_CURRENT_LIST_DIR}/src/DasherCore/SocketInputBase.h") -list(REMOVE_ITEM HEADER_FILES "${CMAKE_CURRENT_LIST_DIR}/src/DasherCore/SocketInput.h") -list(REMOVE_ITEM SOURCE_FILES "${CMAKE_CURRENT_LIST_DIR}/src/DasherCore/CannaConversionHelper.cpp") -list(REMOVE_ITEM SOURCE_FILES "${CMAKE_CURRENT_LIST_DIR}/src/DasherCore/SocketInputBase.cpp") -list(REMOVE_ITEM SOURCE_FILES "${CMAKE_CURRENT_LIST_DIR}/src/DasherCore/SocketInput.cpp") - ADD_library(DasherCore STATIC ${HEADER_FILES} ${SOURCE_FILES} @@ -203,22 +201,40 @@ if(BUILD_CAPI) set(TEST_DATA_DIR "${CMAKE_CURRENT_LIST_DIR}") endif() + # doctest is the test framework. Vendored as a single header in Thirdparty/. + set(DOCTEST_INCLUDE_DIR "${CMAKE_CURRENT_LIST_DIR}/Thirdparty/doctest") + + # Default per-test timeout. Tests routinely take 15-30s because each + # dasher_create() loads every alphabet XML file from Data/. 300s + # gives headroom on slow filesystems (WSL+9P, Windows CI) and for + # Debug builds with asserts active, while still catching real hangs. + # (CI can override via `ctest --timeout` on the command line.) + set(DASHER_TEST_TIMEOUT 300 CACHE STRING "Per-test timeout in seconds") + # Helper function for adding a test executable function(dasher_add_test name source) add_executable(${name} ${CMAKE_CURRENT_LIST_DIR}/tests/${source}) - target_include_directories(${name} PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src/ ${CMAKE_CURRENT_LIST_DIR}/tests/) + target_include_directories(${name} PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/src/ + ${CMAKE_CURRENT_LIST_DIR}/tests/ + ${DOCTEST_INCLUDE_DIR}) target_link_libraries(${name} PRIVATE dasher) target_compile_definitions(${name} PRIVATE TEST_DATA_DIR="${TEST_DATA_DIR}") add_test(NAME ${name} COMMAND ${name}) + set_tests_properties(${name} PROPERTIES TIMEOUT ${DASHER_TEST_TIMEOUT}) endfunction() # Variant for tests that need internal DasherCore symbols (e.g. ConvertUTF) function(dasher_add_test_internal name source) add_executable(${name} ${CMAKE_CURRENT_LIST_DIR}/tests/${source}) - target_include_directories(${name} PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src/ ${CMAKE_CURRENT_LIST_DIR}/tests/) + target_include_directories(${name} PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/src/ + ${CMAKE_CURRENT_LIST_DIR}/tests/ + ${DOCTEST_INCLUDE_DIR}) target_link_libraries(${name} PRIVATE DasherCore pugixml) target_compile_definitions(${name} PRIVATE TEST_DATA_DIR="${TEST_DATA_DIR}") add_test(NAME ${name} COMMAND ${name}) + set_tests_properties(${name} PROPERTIES TIMEOUT ${DASHER_TEST_TIMEOUT}) endfunction() dasher_add_test(dasher_capi_tests test_capi.cpp) @@ -243,6 +259,28 @@ if(BUILD_CAPI) dasher_add_test(dasher_training_tests test_training.cpp) dasher_add_test(dasher_node_tree_tests test_node_tree.cpp) + # Phase B (characterization) — closes coverage gaps noted in the review. + # - buffer_lifetime: the C API "valid until next call" contract + # - lm_correctness: training, LP_UNIFORM/LP_LM_*/BP_LM_ADAPTIVE effects + # - view_geometry: LP_ORIENTATION, BP_NONLINEAR_Y, LP_GEOMETRY + # - input_filters: all 14 registered filters (12 previously untested) + # - xml_error_paths: malformed alphabet/color/settings XML + dasher_add_test(dasher_capi_buffer_lifetime_tests test_capi_buffer_lifetime.cpp) + dasher_add_test(dasher_lm_correctness_tests test_lm_correctness.cpp) + dasher_add_test(dasher_view_geometry_tests test_view_geometry.cpp) + dasher_add_test(dasher_input_filter_tests test_input_filters.cpp) + dasher_add_test(dasher_xml_error_path_tests test_xml_error_paths.cpp) + + # Phase C (deeper coverage) — end-to-end + perf + property tests. + # - control_actions_navigation: fires callback via actual node nav + # - spell_word: drives mouse via probabilities to enter children + # - benchmarks: dasher_frame p99 latency baseline (catches regressions) + # - property_invariants: normalization holds across random training + dasher_add_test(dasher_control_actions_navigation_tests test_control_actions_navigation.cpp) + dasher_add_test(dasher_spell_word_tests test_spell_word.cpp) + dasher_add_test(dasher_benchmark_tests test_benchmarks.cpp) + dasher_add_test(dasher_property_invariant_tests test_property_invariants.cpp) + # Control action system tests — needs internal DasherCore classes (ActionRegistry) # AND C API functions, so we compile CAPI.cpp directly and link DasherCore add_executable(dasher_control_action_tests @@ -250,9 +288,11 @@ if(BUILD_CAPI) ${CMAKE_CURRENT_LIST_DIR}/src/CAPI.cpp) target_include_directories(dasher_control_action_tests PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src/ - ${CMAKE_CURRENT_LIST_DIR}/tests/) + ${CMAKE_CURRENT_LIST_DIR}/tests/ + ${DOCTEST_INCLUDE_DIR}) target_link_libraries(dasher_control_action_tests PRIVATE DasherCore pugixml) target_compile_definitions(dasher_control_action_tests PRIVATE TEST_DATA_DIR="${TEST_DATA_DIR}") add_test(NAME dasher_control_action_tests COMMAND dasher_control_action_tests) + set_tests_properties(dasher_control_action_tests PROPERTIES TIMEOUT ${DASHER_TEST_TIMEOUT}) endif() endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3fdf1de75..537e434a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -130,6 +130,33 @@ Write readable code first. Optimize only after profiling proves a bottleneck. - Prefer pass-by-`const`-reference over copy for large types - Don't inline complex logic — let the compiler decide +### Rule 7: Naming Convention for New Code + +DasherCore has 25 years of accumulated naming styles. We do NOT rename +existing code (that would be a massive churn PR with zero functional +benefit). Instead, all **new** code — new classes, new functions, new +files — should follow these conventions: + +| Element | Convention | Example | Notes | +|---------|-----------|---------|-------| +| Classes | `PascalCase` | `class AlphabetManager` | Not `CAlphabetManager` (the C-prefix is legacy MFC style) | +| Methods | `PascalCase` | `void GetProbs()` | Not `getProbs()` or `get_probs()` | +| Member variables | `snake_case_` (trailing underscore) | `int symbol_count_;` | Not `m_iSymbolCount` (legacy Hungarian) | +| Free functions | `snake_case` | `int get_root_child_count()` | C API functions use `dasher_` prefix | +| Constants/enums | `PascalCase` or `UPPER_SNAKE` | `NORMALIZATION`, `EDIT_OUTPUT` | Match surrounding code | +| Files | `PascalCase.h` / `PascalCase.cpp` | `AlphabetManager.h` | Not `alphabet_manager.h` | +| Namespaces | `PascalCase` | `namespace Dasher` | | + +**Existing conventions you'll encounter (do not change these):** +- `CFoo` — MFC-style class prefix (~70 classes). The dominant legacy style. +- `m_iFoo`, `m_bFoo` — Hungarian-notation member prefix. Common in legacy. +- `Get_node_under_crosshair` — snake_case methods (rare, in DasherModel). +- `dasher_foo_bar()` — C API functions in `dasher.h` (frozen, do not change). + +**Rule of thumb:** When you touch a file, match the file's existing style +for that edit. When you create a new file, use the conventions above. +When in doubt, ask in the PR. + --- ## Testing diff --git a/Scripts/generate_parameters.py b/Scripts/generate_parameters.py index f7fca7667..2e3f35e38 100644 --- a/Scripts/generate_parameters.py +++ b/Scripts/generate_parameters.py @@ -13,10 +13,12 @@ import os import sys import argparse +import shutil +import subprocess AUTOGEN_HEADER = """\ // ============================================================================= -// AUTOGENERATED FILE — DO NOT EDIT DIRECTLY +// AUTOGENERATED FILE - DO NOT EDIT DIRECTLY // Generated by: python3 Scripts/generate_parameters.py // Source: settings_manifest.json // ============================================================================= @@ -204,6 +206,17 @@ def generate(manifest_path, output_path): with open(output_path, 'w') as f: f.write(content) + # Run clang-format on the generated file so it matches the project's + # .clang-format style. The template above intentionally uses compact + # tab-indented formatting (easier to maintain in Python); clang-format + # normalizes it to the project standard. If clang-format is not + # installed, the raw output is still valid C++ - just not formatted. + clang_format = shutil.which("clang-format") + if clang_format: + subprocess.run([clang_format, "-i", output_path], check=True) + else: + print("WARNING: clang-format not found; output is unformatted.", file=sys.stderr) + print(f"Generated {output_path} from {manifest_path}") print(f" {len(manifest['parameters'])} parameters") diff --git a/Strings/strings_en.json b/Strings/strings_en.json index 8a2c40843..1833cdc6c 100644 --- a/Strings/strings_en.json +++ b/Strings/strings_en.json @@ -104,8 +104,6 @@ "LP_GEOMETRY.enum.Squish and Log": "Squish and Log", "LP_LM_WORD_ALPHA.label": "Word Alpha", "LP_LM_WORD_ALPHA.description": "Alpha value for word-based model.", - "LP_USER_LOG_LEVEL_MASK.label": "User Log Level", - "LP_USER_LOG_LEVEL_MASK.description": "Controls level of user logging, 0 = none, 1 = short, 2 = detailed, 3 = both.", "LP_ZOOMSTEPS.label": "Zoom Steps", "LP_ZOOMSTEPS.description": "Defines how many discrete steps are needed to zoom from the outer edge to the center.", "LP_B.label": "B-Parameter", @@ -130,16 +128,6 @@ "LP_NONLINEAR_X.description": "Nonlinear compression of X-axis (0 = none, higher = more extreme).", "LP_AUTOSPEED_SENSITIVITY.label": "Auto-Speed Sensitivity", "LP_AUTOSPEED_SENSITIVITY.description": "Sensitivity of automatic speed control (percent).", - "LP_SOCKET_PORT.label": "Socket Port", - "LP_SOCKET_PORT.description": "UDP/TCP socket to use for network socket input.", - "LP_SOCKET_INPUT_X_MIN.label": "Socket X Min", - "LP_SOCKET_INPUT_X_MIN.description": "Bottom of range of X values expected from network input.", - "LP_SOCKET_INPUT_X_MAX.label": "Socket X Max", - "LP_SOCKET_INPUT_X_MAX.description": "Top of range of X values expected from network input.", - "LP_SOCKET_INPUT_Y_MIN.label": "Socket Y Min", - "LP_SOCKET_INPUT_Y_MIN.description": "Bottom of range of Y values expected from network input.", - "LP_SOCKET_INPUT_Y_MAX.label": "Socket Y Max", - "LP_SOCKET_INPUT_Y_MAX.description": "Top of range of Y values expected from network input.", "LP_CIRCLE_PERCENT.label": "Circle Start Size", "LP_CIRCLE_PERCENT.description": "Percentage of nominal vertical range to use for radius of start circle.", "LP_TWO_BUTTON_OFFSET.label": "Two Button Offset", @@ -166,12 +154,6 @@ "LP_STATIC1B_TIME.description": "The time in milliseconds for static mode to scan from top to bottom.", "LP_STATIC1B_ZOOM.label": "Static Zoom Step", "LP_STATIC1B_ZOOM.description": "The amount of zoom applied per step in static mode.", - "LP_DEMO_SPRING.label": "Demo Springiness", - "LP_DEMO_SPRING.description": "A physics parameter for the automated cursor's 'springiness' in demo mode.", - "LP_DEMO_NOISE_MEM.label": "Demo Noise Memory", - "LP_DEMO_NOISE_MEM.description": "A physics parameter controlling the 'memory' or consistency of random movements in demo mode.", - "LP_DEMO_NOISE_MAG.label": "Demo Noise Magnitude", - "LP_DEMO_NOISE_MAG.description": "Controls the magnitude or intensity of random movements in demo mode.", "LP_MAXZOOM.label": "Zoom Sensitivity / Max Zoom", "LP_MAXZOOM.description": "Controls zoom mechanics in click mode.", "LP_DYNAMIC_SPEED_INC.label": "Auto-Speed Increment", diff --git a/Thirdparty/doctest/doctest.h b/Thirdparty/doctest/doctest.h new file mode 100644 index 000000000..5c754cde0 --- /dev/null +++ b/Thirdparty/doctest/doctest.h @@ -0,0 +1,7106 @@ +// ====================================================================== 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 11 + +// 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 + 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<<(L&& operand) { + 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_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() - 1 - 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() - 1 - 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; + } + } +#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 "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_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/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 000000000..60a10f621 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,304 @@ +# DasherCore Architecture + +This document describes the internal architecture of DasherCore — the +engine, not the frontends. It targets developers who need to understand +the code flow before making changes. For the public C API contract, see +`src/dasher.h`. For the cleanup plan, see `TIER2-3-PLAN.md`. + + +## Component overview + +``` + dasher.h (public C API) + │ + ┌─────────┴──────────┐ + │ CAPI.cpp │ C → C++ adapter layer + │ (dasher_ctx) │ Owns one Interface + one Screen + └─────────┬──────────┘ + │ + ┌───────────────┼───────────────┐ + │ │ │ + ┌────────┴──────┐ ┌──────┴───────┐ ┌─────┴──────┐ + │ CDasherInter- │ │ CommandScreen │ │ PointerInput│ C-API-owned + │ faceBase │ │ (CDasherScreen)│ │ (CDasherInput)│ implementations + │ (god class) │ └──────────────┘ └────────────┘ + └──────┬────────┘ + │ owns / dispatches to + ┌────────┼────────┬────────────┬─────────────┐ + │ │ │ │ │ +┌──┴──┐ ┌───┴────┐ ┌──┴──────┐ ┌───┴────┐ ┌─────┴──────┐ +│Model│ │View │ │Input │ │Module │ │SettingsStore│ +│ │ │(Dasher │ │Filter │ │Manager │ │(XML-backed) │ +│ │ │View) │ │ │ │ │ │ │ +└──┬──┘ └────────┘ └─────────┘ └───┬────┘ └─────────────┘ + │ │ + │ creates nodes via │ registers: + │ │ - Input filters (14) +┌──┴──────────────┐ │ - Language models (5) +│NodeCreation │ │ - Input devices +│Manager (NCM) │ │ - Colour palettes +│ ├AlphabetMgr │◄───────────────┘ +│ ├ConvMgr │ +│ ├ControlMgr │ +│ └Trainer │ +└─────────────────┘ +``` + +### Key types + +| Type | Role | Defined in | +|---|---|---| +| `dasher_ctx` | Opaque C handle. Owns one `Interface`, one `CommandScreen`, one `PointerInput`. | `CAPI.cpp:335` | +| `CDasherInterfaceBase` | The "god class" — 21 responsibilities (lifecycle, rendering, settings, edit buffer, training, etc.). Target for Tier 3 split. | `DasherInterfaceBase.h` | +| `CDasherModel` | The node tree + scheduling logic. Holds the root node and drives zooming. | `DasherModel.h` | +| `CDasherView` | Coordinate transforms + rendering. Subclassed by `CDasherViewSquare` (the only concrete view). | `DasherView.h` | +| `CDasherScreen` | Abstract output surface. C API provides `CommandScreen` which records draw commands instead of drawing pixels. | `DasherScreen.h` | +| `CDasherInput` | Abstract input device. C API provides `PointerInput` (mouse/touch). | `DasherInput.h` | +| `CInputFilter` | Input interpretation strategy (14 registered: Normal Control, Click Mode, One/Button/Two-Button/Two-Push Dynamic, etc.). | `InputFilter.h` | +| `CNodeCreationManager` | Creates and manages the node tree. Owns the AlphabetManager, ConversionManager, ControlManager, and Trainer. | `NodeCreationManager.h` | +| `CLanguageModel` | Probability model for predicting next symbol. 5 registered: PPM, Word, Mixture, CTW, Mandarin (PPMPY). | `LanguageModel.h` | +| `CSettingsStore` | Parameter storage. Backed by `XmlSettingsStore` for persistence. | `SettingsStore.h` | + + +## Sequence: Startup + +Triggered by `dasher_set_screen_size()` — the first call with valid +dimensions causes Realize, which is the heavy initialization. + +``` +dasher_set_screen_size(ctx, w, h) + │ + ├─ First call: create CommandScreen(w, h) + ├─ intf->ChangeScreen(screen) + ├─ intf->Realize(now) + │ │ + │ ├─ CAlphIO: scan Data/alphabets/alphabet.*.xml + │ ├─ CColorIO: scan Data/colours/color.*.xml + │ ├─ ChangeView() → create CDasherViewSquare + │ ├─ ChangeColors() → load active palette + │ │ + │ ├─ CreateModules() + │ │ └─ Register 14 input filters (CDefaultFilter, CPressFilter, + │ │ CClickFilter, COneButtonDynamicFilter, ...) + │ │ + │ ├─ ChangeAlphabet() + │ │ ├─ CreateNCManager() + │ │ │ ├─ Create CAlphabetManager (loads alphabet structure) + │ │ │ ├─ Create CLanguageModel (PPM by default) + │ │ │ ├─ Train on bundled training_*.txt files + │ │ │ └─ Build initial root node + │ │ └─ SetOffset(0, true) → build root node tree + │ │ + │ ├─ CreateInput() → create PointerInput + │ └─ CreateInputFilter() → activate "Normal Control" filter + │ + ├─ ctx->realized = true + └─ Force SP_INPUT_FILTER = "Normal Control" +``` + +After this, the engine is ready to render frames. The C API client +should call `dasher_frame()` on each display refresh. + + +## Sequence: Mouse input → model movement + +The C API stores pointer position on the `PointerInput` object. The +actual model movement happens inside the next `dasher_frame()` call, +not at input time. + +``` +dasher_mouse_move(ctx, x, y) + └─ ctx->input->SetPosition(x, y) [stored, not processed yet] + +dasher_mouse_down(ctx) + ├─ intf->SetBoolParameter(BP_START_MOUSE, true) + └─ intf->KeyDown(NOW, Primary_Input) + └─ m_pInputFilter->KeyDown(...) [filter decides: start/stop] + +dasher_mouse_up(ctx) + └─ intf->KeyUp(NOW, Primary_Input) + └─ m_pInputFilter->KeyUp(...) [filter decides: pause] +``` + +Inside the frame (see next section), the active input filter's +`Timer()` method reads the current pointer position from +`PointerInput` and calls `CDasherModel::ScheduleZoom()` or +`OneStepTowards()` to enqueue model movement. + +Different input filters interpret the same input differently: +- **Normal Control (CDefaultFilter):** continuous — pointer position + determines zoom direction and speed +- **Click Mode (CClickFilter):** discrete — clicks zoom into a target zone +- **One/Two-Button Dynamic:** button presses steer up/down +- **Two-Push Dynamic:** timing of pushes determines direction + + +## Sequence: Frame render + +Called every display refresh (~60 FPS). This is the hot path. + +``` +dasher_frame(ctx, time_ms, &cmds, &cmd_count, &strs, &str_count) + │ + ├─ screen->BeginFrame() [clear command buffer] + │ + ├─ intf->NewFrame(time_ms, true) + │ │ + │ ├─ 1. INPUT FILTER TIMER + │ │ m_pInputFilter->Timer(time, view, input, model, &policy) + │ │ └─ reads pointer pos → model->ScheduleZoom(x, y, time) + │ │ + │ ├─ 2. MODEL STEP + │ │ model->NextScheduledStep() + │ │ └─ executes queued zoom: shifts root, creates/deletes nodes + │ │ + │ ├─ 3. RENDER + │ │ Redraw(time, forceRedraw, policy) + │ │ ├─ model->RenderToView(view, policy) + │ │ │ └─ view traverses visible nodes, emits draw commands + │ │ ├─ gameModule->DecorateView() [if game mode active] + │ │ └─ inputFilter->DecorateView() [mouse line, guides, etc.] + │ │ + │ ├─ 4. POLICY APPLY + │ │ policy.apply() + │ │ └─ expands/contracts child nodes based on visibility + │ │ + │ └─ 5. FINISH + │ screen->Display() [no-op for CommandScreen] + │ + ├─ screen->BuildStringPtrs() + │ + └─ Return command buffer + string pointers to caller + (valid until next dasher_frame call) +``` + +### Draw command format + +Each frame produces an array of `int` commands. Commands are +opcode-driven, 6 ints each (except text commands which are variable): + +``` +[opcode] [arg1] [arg2] [arg3] [arg4] [arg5] + +Opcode 0: Clear screen [argb color] [alpha] [0] [0] [0] +Opcode 1: Circle [center_x] [center_y] [radius] [fill_argb] [outline_argb] +Opcode 2: Rectangle (outline-only) [x1] [y1] [x2] [y2] [color] +Opcode 3: Rectangle (filled) [x1] [y1] [x2] [y2] [fill_color] +Opcode 4: Polygon [count] [points_offset] [fill] [outline] [width] +Opcode 5: Text [string_index] [x] [y] [font_size] [color] +Opcode 6: Polyline [width] [color] [0] [0] [0] +``` + +The frontend interprets these commands and draws to its native canvas. +This abstraction is what makes DasherCore cross-platform — it never +touches pixels directly. + + +## Sequence: Training + +Two training paths exist: + +### Import training text (explicit, via C API) + +``` +dasher_import_training_text(ctx, "the cat sat on the mat") + │ + ├─ Write text to {userDir}/.dasher_training_tmp.txt + └─ intf->ImportTrainingText(tmpfile_path) + └─ m_pNCManager->ImportTrainingText(path) + └─ CTrainer::ParseFile(path, learn=true) + └─ For each character/symbol in the text: + ├─ LM->LearnSymbol(symbol) + └─ Node tree updated with new counts +``` + +### Adaptive learning (implicit, during normal use) + +When `BP_LM_ADAPTIVE` is true (default), every symbol the user outputs +triggers `CSymbolNode::TrainSymbol()`: + +``` +User navigates into a symbol node + └─ CDasherNode::Output() + └─ CSymbolNode::Output() + ├─ Append text to edit buffer + └─ TrainSymbol() + ├─ If context changed: flush old context to training file + ├─ LM->LearnSymbol(current_symbol) + └─ Update context state +``` + +The "flush old context to training file" step is what caused the +CWD leak fixed in Tier 1 #5. It now writes to `{userDir}/` via +`FileUtils::WriteUserDataFile()`. + + +## Parameter system + +Parameters are defined in `settings_manifest.json` (the canonical +source). A Python codegen (`Scripts/generate_parameters.py`) generates +`Parameters.h` (enum) and `Parameters.cpp` (default values + metadata). + +Three parameter kinds: +- **BP_** (Bool Parameters): `BP_LM_ADAPTIVE`, `BP_CONTROL_MODE`, etc. +- **LP_** (Long Parameters): `LP_MAX_BITRATE`, `LP_ORIENTATION`, etc. +- **SP_** (String Parameters): `SP_ALPHABET_ID`, `SP_COLOUR_ID`, etc. + +Parameters are accessed via `GetBoolParameter()`, `GetLongParameter()`, +`GetStringParameter()` on `CSettingsStore`. Changes fire the +`OnParameterChanged` event, which `CDasherInterfaceBase` listens to +and dispatches (e.g., alphabet change → `ChangeAlphabet()`, colour +change → `ChangeColors()`). + +Localization strings live in `Strings/strings_*.json` (one per locale), +keyed by the parameter's `enumKeyName + ".label"` / `".description"`. + + +## Language model hierarchy + +``` +CLanguageModel (abstract) + ├─ CPPMLanguageModel ← base PPM (correct normalization) + │ └─ CPPMPYLanguageModel ← Mandarin pinyin (adds routing) + │ └─ CRoutingPPMLanguageModel ← routing-aware PPM + ├─ CWordLanguageModel ← word-level (uses a PPM spelling model underneath) + ├─ CDictLanguageModel ← dictionary-based + ├─ CMixtureLanguageModel ← blends two LMs (typically Word + PPM) + └─ CCTWLanguageModel ← context tree weighting + +Registered in LMRegistry (singleton), selected via LP_LANGUAGE_MODEL_ID. +``` + +The `GetProbs()` contract: returns a cumulative probability array where +`probs[0] == 0` (sentinel) and `probs[N-1]` should equal the norm +(65536). PPM and CTW normalize correctly. Word and Mixture have known +leaks (see `test_property_invariants.cpp` and Tier 2 #2.5). + + +## Threading model + +DasherCore is **single-threaded**. The C API is not thread-safe — +`dasher_ctx` must be accessed from one thread at a time. Frontends +that need multi-threading (e.g., Android IME) must marshal all +DasherCore calls to a single thread. + +The `NewFrame()` reentrancy guard (`static bool bReentered`) is the +only threading protection, and it's a safety net, not a design feature. + + +## Test architecture + +31 test executables, all using doctest (vendored in `Thirdparty/doctest/`). +Shared helpers in `tests/test_common.h`: + +- `create_isolated_context()` / `ScopedContext` — per-test temp dir + context +- `run_frames()` — canonical frame-stepping helper (60 FPS default) +- `ScopedTempDir` — RAII temp directory cleanup + +Three tiers of tests: +1. **Unit tests** (Phase A): alphabet map, coordinates, color math, draw commands, UTF-8 +2. **Characterization tests** (Phase B): LM correctness, view geometry, input filters, XML error paths, buffer lifetime +3. **Deep coverage** (Phase C): end-to-end spelling, control-mode navigation, benchmarks, property invariants + +Build: `cmake --preset debug && cmake --build build --parallel` +Test: `ctest --test-dir build --timeout 300` diff --git a/settings_manifest.json b/settings_manifest.json index 023214da2..974d9d451 100644 --- a/settings_manifest.json +++ b/settings_manifest.json @@ -727,24 +727,6 @@ "group": "Language", "subgroup": "Advanced" }, - { - "key": "LP_USER_LOG_LEVEL_MASK", - "storageName": "UserLogLevelMask", - "type": "long", - "default": 0, - "label": "User Log Level", - "description": "Controls level of user logging, 0 = none, 1 = short, 2 = detailed, 3 = both.", - "uiType": "Enum", - "tier": "expert", - "group": "Output", - "subgroup": "Logging", - "enumValues": [ - {"label": "None", "cppExpr": "0"}, - {"label": "Short", "cppExpr": "1"}, - {"label": "Detailed", "cppExpr": "2"}, - {"label": "Both", "cppExpr": "3"} - ] - }, { "key": "LP_ZOOMSTEPS", "storageName": "Zoomsteps", @@ -943,86 +925,6 @@ "group": "Input", "subgroup": "CDefaultFilter" }, - { - "key": "LP_SOCKET_PORT", - "storageName": "SocketPort", - "type": "long", - "default": 20320, - "label": "Socket Port", - "description": "UDP/TCP socket to use for network socket input.", - "uiType": "Step", - "min": 1024, - "max": 65535, - "divisor": 1, - "step": 1, - "tier": "expert", - "group": "Input", - "subgroup": "CSocket" - }, - { - "key": "LP_SOCKET_INPUT_X_MIN", - "storageName": "SocketInputXMinTimes1000", - "type": "long", - "default": 0, - "label": "Socket X Min", - "description": "Bottom of range of X values expected from network input.", - "uiType": "Step", - "min": -10000, - "max": 10000, - "divisor": 1, - "step": 100, - "tier": "expert", - "group": "Input", - "subgroup": "CSocket" - }, - { - "key": "LP_SOCKET_INPUT_X_MAX", - "storageName": "SocketInputXMaxTimes1000", - "type": "long", - "default": 1000, - "label": "Socket X Max", - "description": "Top of range of X values expected from network input.", - "uiType": "Step", - "min": -10000, - "max": 10000, - "divisor": 1, - "step": 100, - "tier": "expert", - "group": "Input", - "subgroup": "CSocket" - }, - { - "key": "LP_SOCKET_INPUT_Y_MIN", - "storageName": "SocketInputYMinTimes1000", - "type": "long", - "default": 0, - "label": "Socket Y Min", - "description": "Bottom of range of Y values expected from network input.", - "uiType": "Step", - "min": -10000, - "max": 10000, - "divisor": 1, - "step": 100, - "tier": "expert", - "group": "Input", - "subgroup": "CSocket" - }, - { - "key": "LP_SOCKET_INPUT_Y_MAX", - "storageName": "SocketInputYMaxTimes1000", - "type": "long", - "default": 1000, - "label": "Socket Y Max", - "description": "Top of range of Y values expected from network input.", - "uiType": "Step", - "min": -10000, - "max": 10000, - "divisor": 1, - "step": 100, - "tier": "expert", - "group": "Input", - "subgroup": "CSocket" - }, { "key": "LP_CIRCLE_PERCENT", "storageName": "CirclePercent", @@ -1232,54 +1134,6 @@ "subgroup": "CStaticFilter", "group": "Input" }, - { - "key": "LP_DEMO_SPRING", - "storageName": "DemoSpring", - "type": "long", - "default": 100, - "label": "Demo Springiness", - "description": "A physics parameter for the automated cursor's 'springiness' in demo mode.", - "uiType": "Step", - "min": 0, - "max": 1000, - "divisor": 1, - "step": 1, - "tier": "expert", - "subgroup": "CDemoFilter", - "group": "Input" - }, - { - "key": "LP_DEMO_NOISE_MEM", - "storageName": "DemoNoiseMem", - "type": "long", - "default": 100, - "label": "Demo Noise Memory", - "description": "A physics parameter controlling the 'memory' or consistency of random movements in demo mode.", - "uiType": "Step", - "min": 0, - "max": 1000, - "divisor": 1, - "step": 1, - "tier": "expert", - "subgroup": "CDemoFilter", - "group": "Input" - }, - { - "key": "LP_DEMO_NOISE_MAG", - "storageName": "DemoNoiseMag", - "type": "long", - "default": 325, - "label": "Demo Noise Magnitude", - "description": "Controls the magnitude or intensity of random movements in demo mode.", - "uiType": "Step", - "min": 0, - "max": 1000, - "divisor": 1, - "step": 1, - "tier": "expert", - "subgroup": "CDemoFilter", - "group": "Input" - }, { "key": "LP_MAXZOOM", "storageName": "ClickMaxZoom", diff --git a/src/CAPI.cpp b/src/CAPI.cpp index c2c98eac0..92d28dff8 100644 --- a/src/CAPI.cpp +++ b/src/CAPI.cpp @@ -348,6 +348,13 @@ struct dasher_ctx { std::string userDir; std::string stringBuf; + // Buffers backing const char* returns from various getters. These + // MUST live in dasher_ctx (not file-scope static) so that two + // contexts don't trample each other's returned pointers — a real + // cross-context bug noted in the codebase review (Tier 1 #4). + std::vector stringValues; // dasher_get_palette_name / alphabet_name / parameter_string_values + std::string gameTextBuf; // dasher_game_get_target_text + // Appearance model state (RFC 0007). Lives at the C API layer — appearance // is a shell/canvas concern, not a DasherCore engine parameter. Persisted to // /appearance_settings.xml. The active palette (SP_COLOUR_ID) is @@ -369,6 +376,12 @@ struct dasher_ctx { dasher_parameter_callback paramCb = nullptr; void* paramCbUserData = nullptr; + // Diagnostic log callback (replaces the former CFileLogger/CBasicLog/UserLog + // systems). When null, log messages are silently discarded. + dasher_log_callback logCb = nullptr; + void* logCbUserData = nullptr; + int logCbMinLevel = 0; + struct CustomActionEntry { std::string name; dasher_action_callback callback; @@ -391,9 +404,20 @@ struct dasher_ctx { } void Message(const std::string& strText, bool bInterrupt) override { + // Route user-facing messages to the message callback (for UI display) if (m_owner->messageCb && !strText.empty()) m_owner->messageCb(bInterrupt ? 1 : 0, strText.c_str(), m_owner->messageCbUserData); if (!m_owner->messageCb) CDashIntfScreenMsgs::Message(strText, bInterrupt); + + // Also route to the log callback for diagnostic logging. + // Modal/interrupt messages are WARN level; async are INFO. + // This ensures frontends that registered dasher_set_log_callback + // receive engine messages even if they didn't register the + // message callback separately. + if (m_owner->logCb && !strText.empty()) { + int level = bInterrupt ? 2 /*WARN*/ : 1 /*INFO*/; + if (level >= m_owner->logCbMinLevel) m_owner->logCb(level, strText.c_str(), m_owner->logCbUserData); + } } unsigned int ctrlOffsetAfterMove(unsigned int offsetBefore, bool bForwards, @@ -656,7 +680,12 @@ DASHER_API dasher_ctx* dasher_create(const char* data_dir, const char* user_dir, ctx->userDir = user_dir ? std::string(user_dir) : dir; std::string writableDir = user_dir ? std::string(user_dir) : dir; + // Keep the bundled data directory (read-only corpora) and the + // user-writable directory (logs, training deltas, settings) + // distinct so we never leak library files into CWD. Closes the + // dasher.log and training_english_GB.txt CWD leaks (Tier 1 #5). Dasher::FileUtils::SetDataDirectory(dir); + Dasher::FileUtils::SetUserDataDirectory(writableDir); std::string settingsPath = writableDir; #ifdef _WIN32 @@ -979,7 +1008,6 @@ static std::string s_paramInfoGroup; static std::string s_paramInfoSubgroup; static std::vector s_enumStrings; static std::vector> s_enumEntries; -static std::vector s_stringValues; static void ensureParamKeys() { if (!s_paramKeys.empty()) return; @@ -1078,16 +1106,17 @@ DASHER_API int dasher_get_parameter_enum_value(int key, int index) { DASHER_API int dasher_get_parameter_string_values(dasher_ctx* ctx, int key, const char** out_names, int max_out) { if (!out_names || max_out <= 0) return 0; - s_stringValues.clear(); + if (!ctx) return 0; + ctx->stringValues.clear(); - if (ctx && ctx->intf) { - s_stringValues = ctx->intf->GetPermittedValues(static_cast(key)); + if (ctx->intf) { + ctx->stringValues = ctx->intf->GetPermittedValues(static_cast(key)); } - int count = static_cast(s_stringValues.size()); + int count = static_cast(ctx->stringValues.size()); if (count > max_out) count = max_out; for (int i = 0; i < count; i++) { - out_names[i] = s_stringValues[i].c_str(); + out_names[i] = ctx->stringValues[i].c_str(); } return count; } @@ -1104,8 +1133,8 @@ DASHER_API const char* dasher_get_palette_name(dasher_ctx* ctx, int index) { if (!ctx || !ctx->intf) return ""; auto names = ctx->intf->GetPermittedValues(Dasher::SP_COLOUR_ID); if (index < 0 || index >= static_cast(names.size())) return ""; - s_stringValues = std::move(names); - return s_stringValues[index].c_str(); + ctx->stringValues = std::move(names); + return ctx->stringValues[index].c_str(); } DASHER_API const char* dasher_get_current_palette(dasher_ctx* ctx) { @@ -1259,8 +1288,8 @@ DASHER_API const char* dasher_get_alphabet_name(dasher_ctx* ctx, int index) { if (!ctx || !ctx->intf) return ""; auto names = ctx->intf->GetPermittedValues(Dasher::SP_ALPHABET_ID); if (index < 0 || index >= static_cast(names.size())) return ""; - s_stringValues = std::move(names); - return s_stringValues[index].c_str(); + ctx->stringValues = std::move(names); + return ctx->stringValues[index].c_str(); } // ── Game Mode ─────────────────────────────────────────────────────────────── @@ -1288,8 +1317,6 @@ DASHER_API void dasher_game_set_canvas_text(dasher_ctx* ctx, int enabled) { if (gm) gm->SetCanvasTextEnabled(enabled != 0); } -static std::string s_gameTextBuf; - static std::string symbolsToText(const Dasher::CAlphInfo* alph, const std::vector& syms, int count) { std::string result; for (int i = 0; i < count && i < (int)syms.size(); i++) { @@ -1303,8 +1330,8 @@ DASHER_API const char* dasher_game_get_target_text(dasher_ctx* ctx) { auto* gm = ctx->intf->GetGameModule(); if (!gm) return ""; const auto& syms = gm->GetTargetSymbols(); - s_gameTextBuf = symbolsToText(gm->GetAlphabet(), syms, (int)syms.size()); - return s_gameTextBuf.c_str(); + ctx->gameTextBuf = symbolsToText(gm->GetAlphabet(), syms, (int)syms.size()); + return ctx->gameTextBuf.c_str(); } DASHER_API int dasher_game_get_correct_count(dasher_ctx* ctx) { @@ -1325,8 +1352,8 @@ DASHER_API const char* dasher_game_get_wrong_text(dasher_ctx* ctx) { if (!ctx || !ctx->intf) return ""; auto* gm = ctx->intf->GetGameModule(); if (!gm) return ""; - s_gameTextBuf = gm->GetWrongText(); - return s_gameTextBuf.c_str(); + ctx->gameTextBuf = gm->GetWrongText(); + return ctx->gameTextBuf.c_str(); } // ── Persistence ─────────────────────────────────────────────────────────── @@ -1471,6 +1498,13 @@ DASHER_API void dasher_set_message_callback(dasher_ctx* ctx, dasher_message_call ctx->messageCbUserData = user_data; } +DASHER_API void dasher_set_log_callback(dasher_ctx* ctx, dasher_log_callback callback, void* user_data, int min_level) { + if (!ctx) return; + ctx->logCb = callback; + ctx->logCbUserData = user_data; + ctx->logCbMinLevel = min_level; +} + DASHER_API void dasher_set_speak_callback(dasher_ctx* ctx, dasher_speak_callback callback, void* user_data) { if (!ctx) return; ctx->speakCb = callback; diff --git a/src/DasherCore/Alphabet/AlphIO.cpp b/src/DasherCore/Alphabet/AlphIO.cpp index 9be25aee2..02e2935ce 100644 --- a/src/DasherCore/Alphabet/AlphIO.cpp +++ b/src/DasherCore/Alphabet/AlphIO.cpp @@ -39,11 +39,6 @@ SGroupInfo* CAlphIO::ParseGroupRecursive(pugi::xml_node& group_node, CAlphInfo* pNewGroup->strName = group_node.attribute("name").as_string(""); pNewGroup->strLabel = group_node.attribute("label").as_string(""); pNewGroup->colorGroup = group_node.attribute("colorInfoName").as_string(""); - // v5 groups don't have colorInfoName — they use "b" for color index. - // Without a colorGroup name, GetNodeColor returns undefinedColor which - // becomes transparent (Alpha=0), making nodes invisible. - // Default to "lowercase" which exists in all standard palettes. - if (pNewGroup->colorGroup.empty()) pNewGroup->colorGroup = "lowercase"; pNewGroup->pNext = previous_sibling; pNewGroup->pChild = nullptr; @@ -54,8 +49,8 @@ SGroupInfo* CAlphIO::ParseGroupRecursive(pugi::xml_node& group_node, CAlphInfo* new_ancestors.push_back(pNewGroup); SGroupInfo* previous_subgroup_sibling = nullptr; for (auto node : group_node.children()) { - // symbol (v6 "node" or v5 "s") - if (std::strcmp(node.name(), "node") == 0 || std::strcmp(node.name(), "s") == 0) { + // symbol + if (std::strcmp(node.name(), "node") == 0) { CurrentAlphabet->m_vCharacters.resize(CurrentAlphabet->m_vCharacters.size() + 1); // new char CurrentAlphabet->m_vCharacterDoActions.resize(CurrentAlphabet->m_vCharacterDoActions.size() + 1); // new Do Actions @@ -93,30 +88,17 @@ SGroupInfo* CAlphIO::ParseGroupRecursive(pugi::xml_node& group_node, CAlphInfo* return pNewGroup; } -bool Dasher::CAlphIO::Parse(pugi::xml_document& document, const std::string strDesc, bool bUser) { - pugi::xml_node root = document.document_element(); +bool Dasher::CAlphIO::Parse(pugi::xml_document& document, const std::string, bool bUser) { + pugi::xml_node alphabet = document.document_element(); - // v5 format: root containing children - if (std::strcmp(root.name(), "alphabets") == 0) { - bool any = false; - for (pugi::xml_node alpha : root.children("alphabet")) { - if (ParseSingle(alpha, strDesc, bUser)) any = true; - } - return any; - } + if (std::strcmp(alphabet.name(), "alphabet") != 0) return false; // a non node - // v6 format: root (single alphabet per file) - if (std::strcmp(root.name(), "alphabet") != 0) return false; - return ParseSingle(root, strDesc, bUser); -} - -bool Dasher::CAlphIO::ParseSingle(pugi::xml_node alphabet, const std::string, bool bUser) { CAlphInfo* CurrentAlphabet = new CAlphInfo(); CurrentAlphabet->AlphID = alphabet.attribute("name").as_string(); CurrentAlphabet->TrainingFile = alphabet.attribute("trainingFilename").as_string(); CurrentAlphabet->PreferredColors = alphabet.attribute("colorsName").as_string(); - // Orientation — always read v6 attribute first (present on every v6 alphabet) + // orientation const std::string orientation_type = alphabet.attribute("orientation").as_string("LR"); if (orientation_type == "RL") { CurrentAlphabet->Orientation = Options::RightToLeft; @@ -128,26 +110,6 @@ bool Dasher::CAlphIO::ParseSingle(pugi::xml_node alphabet, const std::string, bo CurrentAlphabet->Orientation = Options::LeftToRight; } - // Handle v5 metadata child elements (override if v5 format is present) - for (pugi::xml_node meta : alphabet.children()) { - const char* name = meta.name(); - if (std::strcmp(name, "train") == 0 && CurrentAlphabet->TrainingFile.empty()) - CurrentAlphabet->TrainingFile = meta.text().as_string(); - else if (std::strcmp(name, "palette") == 0 && CurrentAlphabet->PreferredColors.empty()) - CurrentAlphabet->PreferredColors = meta.text().as_string(); - else if (std::strcmp(name, "orientation") == 0) { - std::string otype = meta.attribute("type").as_string("LR"); - if (otype == "RL") - CurrentAlphabet->Orientation = Options::RightToLeft; - else if (otype == "TB") - CurrentAlphabet->Orientation = Options::TopToBottom; - else if (otype == "BT") - CurrentAlphabet->Orientation = Options::BottomToTop; - else - CurrentAlphabet->Orientation = Options::LeftToRight; - } - } - // conversion mode const std::string conversion_mode = alphabet.attribute("conversionMode").as_string("none"); if (conversion_mode == "mandarin") { @@ -279,12 +241,8 @@ void CAlphIO::ReadCharAttributes(pugi::xml_node xml_node, CAlphInfo::character& if (xml_node.type() == pugi::node_null) return; - // v6 uses "label"/"text" attributes; v5 uses "d"/"t" attributes alphabet_character.Display = xml_node.attribute("label").as_string(); - if (alphabet_character.Display.empty()) alphabet_character.Display = xml_node.attribute("d").as_string(); - alphabet_character.Text = xml_node.attribute("text").as_string(); - if (alphabet_character.Text.empty()) - alphabet_character.Text = xml_node.attribute("t").as_string(alphabet_character.Display.c_str()); + alphabet_character.Text = xml_node.attribute("text").as_string(alphabet_character.Display.c_str()); for (auto potentialActions : xml_node.children()) { const char* actionName = potentialActions.name(); @@ -423,13 +381,6 @@ void CAlphIO::ReadCharAttributes(pugi::xml_node xml_node, CAlphInfo::character& alphabet_character.ColorGroupOffset = parentGroup->iNumChildNodes; alphabet_character.fixedProbability = xml_node.attribute("fixedProbability").as_float(-1); alphabet_character.speedFactor = xml_node.attribute("speedFactor").as_float(-1); - - // v5 compatibility: if no action children were found (v5 elements have no - // action children), create default text output/delete actions from the text. - if (DoActions.empty() && !alphabet_character.Text.empty()) { - DoActions.push_back(new TextOutputAction(alphabet_character.Text)); - UndoActions.push_back(new TextDeleteAction(alphabet_character.Text)); - } } // Reverses the internal linked list for the given SGroupInfo diff --git a/src/DasherCore/Alphabet/AlphIO.h b/src/DasherCore/Alphabet/AlphIO.h index e361fc98b..9bf93489f 100644 --- a/src/DasherCore/Alphabet/AlphIO.h +++ b/src/DasherCore/Alphabet/AlphIO.h @@ -47,7 +47,6 @@ class Dasher::CAlphIO : public AbstractXMLParser { virtual ~CAlphIO(); virtual bool Parse(pugi::xml_document& document, const std::string filePath, bool bUser) override; - bool ParseSingle(pugi::xml_node alphabet, const std::string filePath, bool bUser); void GetAlphabets(std::vector* AlphabetList) const; const CAlphInfo* GetInfo(const std::string& AlphID) const; diff --git a/src/DasherCore/BasicLog.cpp b/src/DasherCore/BasicLog.cpp deleted file mode 100644 index 6c6c7a26e..000000000 --- a/src/DasherCore/BasicLog.cpp +++ /dev/null @@ -1,84 +0,0 @@ -#include "BasicLog.h" - -#include "DasherInterfaceBase.h" - -#include -#include -#include -#include -#include - -#include "FileUtils.h" - -using namespace Dasher; - -CBasicLog::CBasicLog(CSettingsStore* pSettingsStore, CDasherInterfaceBase* pIntf) - : CUserLogBase(pIntf), m_pSettingsStore(pSettingsStore), m_iKeyCount(0), m_iInitialRate(0), m_dBits(0) { - m_iSymbolCount = 0; - m_bStarted = false; -} - -CBasicLog::~CBasicLog() { - EndTrial(); -} - -void CBasicLog::StartWriting() { - if (!m_bStarted) { - StartTrial(); - m_bStarted = true; - } -} - -void CBasicLog::StopWriting(float dNats) { - m_dBits += dNats / log(2.0); -} - -void CBasicLog::AddSymbols(Dasher::VECTOR_SYMBOL_PROB* pVectorNewSymbolProbs, eUserLogEventType iEvent) { - m_iSymbolCount += static_cast(pVectorNewSymbolProbs->size()); -} - -void CBasicLog::DeleteSymbols(int iNumToDelete, eUserLogEventType iEvent) { - m_iSymbolCount -= iNumToDelete; -} - -void CBasicLog::NewTrial() { - EndTrial(); -} - -void CBasicLog::KeyDown(Dasher::Keys::VirtualKey Key, int iType, int iEffect) { - ++m_iKeyCount; -} - -void CBasicLog::StartTrial() { - m_iSymbolCount = 0; - m_iKeyCount = 0; - m_dBits = 0.0; - m_strStartDate = GetDateStamp(); - m_iInitialRate = m_pSettingsStore->GetLongParameter(LP_MAX_BITRATE); -} - -void CBasicLog::EndTrial() { - if (!m_bStarted) return; - - std::string strFileName(FileUtils::GetFullFilenamePath("dasher_basic.log")); - - std::ofstream oFile; - oFile.open(strFileName.c_str(), std::ios::out | std::ios::app); - - oFile << "\"" << m_strStartDate << "\":\"" << GetDateStamp() << "\":" << m_iSymbolCount << ":" << m_dBits << ":" - << m_iKeyCount << ":" << m_iInitialRate / 100.0 << ":" - << m_pSettingsStore->GetLongParameter(LP_MAX_BITRATE) / 100.0 << ":\"" - << m_pSettingsStore->GetStringParameter(SP_INPUT_FILTER) << "\":\"" - << m_pSettingsStore->GetStringParameter(SP_ALPHABET_ID) << "\"" << std::endl; - - oFile.close(); - - m_bStarted = false; -} - -std::string CBasicLog::GetDateStamp() { - auto datestamp = std::chrono::system_clock::to_time_t( - std::chrono::system_clock::now()); // This is a very long format, should probably be replaced using put_time - // where its used. - return std::ctime(&datestamp); -} diff --git a/src/DasherCore/BasicLog.h b/src/DasherCore/BasicLog.h deleted file mode 100644 index 2a304727a..000000000 --- a/src/DasherCore/BasicLog.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include "UserLogBase.h" -#include "SettingsStore.h" - -/// \ingroup Logging -/// @{ -class CBasicLog : public CUserLogBase { - public: - CBasicLog(Dasher::CSettingsStore* pSettingsStore, Dasher::CDasherInterfaceBase* pIntf); - ~CBasicLog(); - - virtual void AddParam(const std::string& strName, const std::string& strValue, int iOptionMask = 0) {}; - virtual void AddParam(const std::string& strName, double dValue, int iOptionMask = 0) {}; - virtual void AddParam(const std::string& strName, int iValue, int iOptionMask = 0) {}; - virtual void StartWriting(); - virtual void StopWriting(float dNats); - virtual void StopWriting() {}; - virtual void AddSymbols(Dasher::VECTOR_SYMBOL_PROB* pVectorNewSymbolProbs, - eUserLogEventType iEvent = userLogEventMouse); - virtual void DeleteSymbols(int iNumToDelete, eUserLogEventType iEvent = userLogEventMouse); - virtual void NewTrial(); - virtual void AddWindowSize(int iTop, int iLeft, int iBottom, int iRight) {}; - virtual void AddCanvasSize(int iTop, int iLeft, int iBottom, int iRight) {}; - virtual void AddMouseLocation(int iX, int iY, float dNats) {}; - virtual void AddMouseLocationNormalized(int iX, int iY, bool bStoreIntegerRep, float dNats) {}; - virtual void OutputFile() {}; - virtual void InitIsDone() {}; - virtual void SetOuputFilename(const std::string& strFilename = "") {}; - virtual int GetLogLevelMask() { return 0; }; - virtual void KeyDown(Dasher::Keys::VirtualKey Key, int iType, int iEffect); - - protected: - Dasher::CSettingsStore* m_pSettingsStore; - - private: - void StartTrial(); - void EndTrial(); - static std::string GetDateStamp(); - - bool m_bStarted; - int m_iSymbolCount; - int m_iKeyCount; - int m_iInitialRate; - double m_dBits; - std::string m_strStartDate; -}; -/// @} diff --git a/src/DasherCore/CannaConversionHelper.cpp b/src/DasherCore/CannaConversionHelper.cpp deleted file mode 100644 index d02374a8a..000000000 --- a/src/DasherCore/CannaConversionHelper.cpp +++ /dev/null @@ -1,332 +0,0 @@ -#ifdef HAVE_CONFIG_H -#include -#endif - -#include "CannaConversionHelper.h" - -#include -#include -#include -#include "SCENode.h" - -#define BUFSIZE 10240 - -#include //For testing 23 June 2005 - -using namespace Dasher; - -CCannaConversionHelper::CCannaConversionHelper(CNodeCreationManager* pNCManager, const CAlphInfo* pAlphabet, int Type, - int Order) - : CConversionManager(pNCManager, pAlphabet) { - - int ret; - char* buf; - int dicnum; - - iType = Type; // 0 = uniform, 1 = 1/(n+1),2 = 1/(n+1)/n - iOrder = Order; // 0 = canna, 1 = Unicode , 2 = Shift_JIS - - icon = iconv_open("SJIS", "UTF8"); - - IsInit = 0; - - /* Initialize */ - ret = RkInitialize(""); - if (ret < 0) { - return; - } - buf = (char*)malloc(sizeof(char) * BUFSIZE); - if (buf == NULL) return; - - /* Create Context ID */ - context_id = RkCreateContext(); - - /* Load Dictionaries */ - dicnum = RkGetDicList(context_id, buf, BUFSIZE); // Find all useable dictionaries - char* p = buf; - for (int i = 0; i < dicnum; i++) { - ret = RkMountDic(context_id, p, 0); // Mount a dictionary - if (ret) std::cout << "Error loading:" << p << std::endl; - - p += (strlen(p) + 1); // Move to next dictionary name - } - - free(buf); - IsInit = 1; - - // std::cout << "Init Canna OK." << endl; -} - -CCannaConversionHelper::~CCannaConversionHelper() { - RkCloseContext(context_id); // Close working context - // std::cout << "Finalizing Canna OK." << endl; - - /* exit */ - RkFinalize(); - iconv_close(icon); -} - -bool CCannaConversionHelper::Convert(const std::string& strSource, SCENode** pRoot) { - - if (strSource.size() == 0) return false; - - int nbun; - char* pQuery = (char*)strSource.c_str(); - - unsigned char* buf = (unsigned char*)malloc(sizeof(unsigned char) * BUFSIZE); - unsigned char* str_utf8 = (unsigned char*)malloc(sizeof(unsigned char) * BUFSIZE); - - char* inbuf = (char*)pQuery; - char* outbuf = (char*)buf; - size_t inbytesleft = strSource.length(); - size_t outbytesleft = BUFSIZE; - - // Use EUC for Canna - // - // NOTE: As far as I can tell, this requires NFC rather than NFD - // normalisation. http://www.cl.cam.ac.uk/~mgk25/unicode.html - // - iconv_t cd = iconv_open("EUC-JP", "UTF8"); - iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft); - *outbuf = '\0'; - inbuf = (char*)buf; - iconv_close(cd); - - /* Divide given string into phrases */ - nbun = RkBgnBun(context_id, // context ID - inbuf, // given string - strlen(inbuf), // length of given string - (RK_XFER << RK_XFERBITS) | RK_KFER); // mode - - if (nbun == -1) { - // Crude error detection - I don't know enough Japanese to figure out how to do this properly :-( - - std::cerr << "Error - Canna conversion failed, possibly could not connect to server." << std::endl; - } - - SCENode* pDummyRoot(new SCENode); - // pDummyRoot->pChild = NULL; - - /* Convert each phrase into Kanji */ - cd = iconv_open("UTF8", "EUC-JP"); - for (int i = nbun - 1; i >= 0; --i) { - SCENode* pTail = pDummyRoot->GetChild(); - - if (pTail) pTail->Ref(); - - pDummyRoot->SetChild(NULL); - - RkGoTo(context_id, i); // Move to a specific phrase - int len = RkGetKanjiList(context_id, buf, BUFSIZE); // Get a list of Kanji candidates - - // Use UTF-8 for Dasher - char* p = (char*)buf; - - std::vector vCandidates; - - for (int j = 0; j < len; ++j) { - inbuf = p; - // std::cout << "Canna:" << j << "[" << inbuf << "] "; - outbuf = (char*)str_utf8; - inbytesleft = strlen((char*)inbuf); - outbytesleft = BUFSIZE; - // for( int k=0; k<20; k++ ){ - // std::cout << (int) inbuf[k] << " "; - // } - // std::cout << inbytesleft << " ->"; - iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft); - *outbuf = '\0'; - - if (strlen((char*)str_utf8)) vCandidates.push_back((char*)str_utf8); - - // std::cout << "[" << str_utf8 << "] " << outbytesleft << std::endl; - p += (strlen(p) + 1); - } - - for (std::vector::reverse_iterator it(vCandidates.rbegin()); it != vCandidates.rend(); ++it) { - ProcessCandidate(*it, pDummyRoot, pTail); - } - - if (pTail) pTail->Unref(); - } - - RkEndBun(context_id, 0); // Close phrase division - - iconv_close(cd); - free(buf); - free(str_utf8); - - *pRoot = pDummyRoot->GetChild(); - - (*pRoot)->Ref(); - pDummyRoot->Unref(); - - return true; -} - -void CCannaConversionHelper::ProcessCandidate(std::string strCandidate, SCENode* pRoot, SCENode* pTail) { - - SCENode* pCurrentNode(pRoot); - - int iIdx(0); - - // TODO: Need phrase-based conversion - while (iIdx < strCandidate.size()) { - - int iLength; - - // TODO: Really dodgy UTF-8 parser - find a library routine to do this - if ((static_cast(strCandidate[iIdx]) & 0x80) == 0) - iLength = 1; - else if ((static_cast(strCandidate[iIdx]) & 0xE0) == 0xC0) - iLength = 2; - else if ((static_cast(strCandidate[iIdx]) & 0xF0) == 0xE0) - iLength = 3; - else if ((static_cast(strCandidate[iIdx]) & 0xF8) == 0xF0) - iLength = 4; - else if ((static_cast(strCandidate[iIdx]) & 0xFC) == 0xF8) - iLength = 5; - else - iLength = 6; - - std::string strSymbol(strCandidate.substr(iIdx, iLength)); - - iIdx += iLength; - - SCENode* pCurrentChild(pCurrentNode->GetChild()); // TODO: Initialise - - while (pCurrentChild) { - if (strSymbol == pCurrentChild->pszConversion) break; - pCurrentChild = pCurrentChild->GetNext(); - } - - if (!pCurrentChild) { // Need a new child - pCurrentChild = new SCENode; - if (pCurrentNode->GetChild()) pCurrentChild->SetNext(pCurrentNode->GetChild()); - if (pTail && (iIdx >= strCandidate.size())) pCurrentChild->SetChild(pTail); - - pCurrentChild->pszConversion = new char[strSymbol.size() + 1]; - strcpy(pCurrentChild->pszConversion, strSymbol.c_str()); - - pCurrentNode->SetChild(pCurrentChild); - pCurrentChild->Unref(); - } - - pCurrentNode = pCurrentChild; - } -} - -void CCannaConversionHelper::AssignSizes(SCENode** pStart, Dasher::CLanguageModel::Context context, long normalization, - int uniform, int iNChildren) { - - SCENode* pNode(*pStart); - - if (this->iType == 0) { - int iRemaining = iNChildren; - int iLeft = normalization; - int iCheck(0); - while (pNode) { - pNode->NodeSize = iLeft / iRemaining; - iLeft -= pNode->NodeSize; - iCheck += pNode->NodeSize; - --iRemaining; - pNode = pNode->GetNext(); - } - } - - else if (this->iType == 1) { - int iN = 1; - double iK = 0; - for (int k = 1; k <= iNChildren; k++) - iK += 1 / (1 + (double)k); - while (pNode) { - pNode->NodeSize = normalization / (iK * (1 + iN)); - pNode = pNode->GetNext(); - ++iN; - } - } else if (this->iType == 2) { - int iN = 1; - double iK = 0; - for (int k = 1; k <= iNChildren; k++) - iK += 1 / ((1 + (double)k) * (double)k); - while (pNode) { - pNode->NodeSize = normalization / (iK * (1 + iN) * iN); - pNode = pNode->GetNext(); - ++iN; - } - } - - else { - DASHER_ASSERT(false); - } - - int iCode_a, iCode_b; - SCENode* pTmp_a; - SCENode* pTmp_b; - - (*pStart)->Ref(); - - // Change the order of Kanji candidates// - if (this->iOrder > 1) { - for (int i = 1; i <= iNChildren; i++) { - pNode = *pStart; - if (pNode->GetNext()) { - SCENode* pNext = pNode->GetNext(); - SCENode* pNext2 = pNext->GetNext(); - if (this->iOrder == 1) { - iCode_a = this->iUTF8Decode(pNode->pszConversion); - iCode_b = this->iUTF8Decode(pNext->pszConversion); - } else if (this->iOrder == 2) { - iCode_a = this->iShiftJISDecode(pNode->pszConversion); - iCode_b = this->iShiftJISDecode(pNext->pszConversion); - } - - if (iCode_a > iCode_b) { - - if (pNext2) pNext2->Ref(); - - pNext->Ref(); - pNext->SetNext(*pStart); - (*pStart)->SetNext(pNext2); - - (*pStart)->Unref(); - - if (pNext2) pNext2->Unref(); - - *pStart = pNext; - - pNode = *pStart; - } - } - - while (pNode) { - if (pNode->GetNext() && (pNode->GetNext())->GetNext()) { - pTmp_a = pNode->GetNext(); - pTmp_b = pTmp_a->GetNext(); - - if (this->iOrder == 1) { - iCode_a = this->iUTF8Decode(pTmp_a->pszConversion); - iCode_b = this->iUTF8Decode(pTmp_b->pszConversion); - } else if (this->iOrder == 2) { - iCode_a = this->iShiftJISDecode(pTmp_a->pszConversion); - iCode_b = this->iShiftJISDecode(pTmp_b->pszConversion); - } - - if (iCode_a > iCode_b) { - SCENode* pTmp = pTmp_b->GetNext(); - if (pTmp) pTmp->Ref(); - pTmp_a->Ref(); - - pNode->SetNext(pTmp_b); - pTmp_b->SetNext(pTmp_a); - pTmp_a->SetNext(pTmp); - - pTmp_a->Unref(); - if (pTmp) pTmp->Unref(); - } - } - pNode = pNode->GetNext(); - } - } - } -} diff --git a/src/DasherCore/CannaConversionHelper.h b/src/DasherCore/CannaConversionHelper.h deleted file mode 100644 index 2230a34a9..000000000 --- a/src/DasherCore/CannaConversionHelper.h +++ /dev/null @@ -1,143 +0,0 @@ -#pragma once - -#define BUFSIZE 10240 - -#include "ConversionManager.h" -#include - -/// \ingroup Model -/// @{ -class CCannaConversionHelper : public Dasher::CConversionManager { - public: - CCannaConversionHelper(CNodeCreationManager* pNCManager, const Dasher::CAlphInfo* pAlphabet, int Type, int Order); - ~CCannaConversionHelper(); - - virtual bool Convert(const std::string& strSource, SCENode** pRoot); - - using CConversionManager::AssignSizes; - virtual void AssignSizes(SCENode** pStart, Dasher::CLanguageModel::Context context, long normalization, int uniform, - int iNChildren); - - virtual Dasher::CLanguageModel* GetLanguageModel() { return NULL; }; - - /* virtual bool GetPhraseList(int HZIndex, SCENode ** psOutput, int CMid) { */ - /* return false; */ - /* }; */ - - /* virtual void BuildDataBase() { */ - /* }; */ - - virtual void ClearData(int CMid) {}; - - /* virtual std::vector > > > > * GetDP(int CMid) { */ - /* return NULL; */ - /* };//get data pointer */ - - private: - void ProcessCandidate(std::string strCandidate, SCENode* pRoot, SCENode* pTail); - - unsigned int iUTF8Decode(const std::string& strChar) { - int iLength = strChar.size(); - - unsigned int iCode = 0; - - switch (iLength) { - case 0: - // Invalid - return 0; - iCode = 0; - break; - case 1: - iCode = static_cast(strChar[0]) & 127; - break; - case 2: - iCode = static_cast(strChar[1]) & 63 + ((static_cast(strChar[0]) & 31) << 6); - break; - case 3: - iCode = static_cast(strChar[2]) & - 63 + ((static_cast(strChar[1]) & 63) << 6) + ((static_cast(strChar[0]) & 15) << 12); - break; - case 4: - // Invalid (as far as we're concerned) - return 0; - iCode = 0; - break; - default: - break; - } - - return iCode; - } - - // Kazue - -#define JMS1(c) \ - (((((unsigned char)(c)) >= 0x81) && (((unsigned char)(c)) <= 0x9F)) || \ - ((((unsigned char)(c)) >= 0xE0) && (((unsigned char)(c)) <= 0xFC))) -#define JMS2(c) ((((unsigned char)(c)) != 0x7F) && (((unsigned char)(c)) >= 0x40) && (((unsigned char)(c)) <= 0xFC)) - - int isJMS(const char* str, int nPos) { - int i; - int state; // { 0, 1, 2 } = { 1byte, 1st of 2byte, 2nd of 2byte } - - state = 0; - for (i = 0; str[i] != '\0'; i++) { - if ((state == 0) && (JMS1(str[i]))) - state = 1; // 0 -> 1 // NOLINT(bugprone-branch-clone) - else if ((state == 1) && (JMS2(str[i]))) - state = 2; // 1 -> 2 - else if ((state == 2) && (JMS1(str[i]))) - state = 1; // 2 -> 1 - else - state = 0; // 2 -> 0, others - - if (i == nPos) return state; - } - return 0; - } // isJMS - -#define MAKEJMS(lead, trail) ((unsigned short)(((unsigned char)(trail) & 0xff) | (((unsigned char)(lead) & 0xff) << 8))) - - int jmscode(const char* p) { - if (JMS1(p[0]) && ('\0' != p[1])) return MAKEJMS(p[0], p[1]); - - return (unsigned char)p[0]; - } // jmscode - - unsigned int iShiftJISDecode(const std::string& strChar) { - - if (strChar.size() == 0) return false; - - char* pQuery = const_cast(strChar.c_str()); - - unsigned char* buf = static_cast(malloc(sizeof(unsigned char) * BUFSIZE)); - - char* inbuf = reinterpret_cast(pQuery); - char* outbuf = reinterpret_cast(buf); - size_t inbytesleft = strChar.length(); - size_t outbytesleft = BUFSIZE; - - // iconv_t cd = iconv_open("SJIS", "UTF8"); - // iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft); - iconv(this->icon, &inbuf, &inbytesleft, &outbuf, &outbytesleft); - - const std::string strSJIS = reinterpret_cast(buf); - - *outbuf = '\0'; - inbuf = reinterpret_cast(buf); - // iconv_close(cd); - - int iCode = 0; - - if (strSJIS.size() == 2) iCode = jmscode(reinterpret_cast(buf)); - - return iCode; - } - - int context_id; - bool IsInit; - - int iType; // 0 = uniform, 1 = 1/(n+1),2 = 1/(n+1)/n - int iOrder; // 0 = canna, 1 = Unicode , 2 = Shift_JIS - - iconv_t icon; -}; -/// @} diff --git a/src/DasherCore/CircleStartHandler.cpp b/src/DasherCore/CircleStartHandler.cpp index a3d43f631..55f5cbaf7 100644 --- a/src/DasherCore/CircleStartHandler.cpp +++ b/src/DasherCore/CircleStartHandler.cpp @@ -18,6 +18,7 @@ // along with Dasher; if not, write to the Free Software // Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +#include #include "CircleStartHandler.h" #include "DefaultFilter.h" #include "DasherInterfaceBase.h" diff --git a/src/DasherCore/Common/AppSettingsData.h b/src/DasherCore/Common/AppSettingsData.h deleted file mode 100644 index c93b1ab5a..000000000 --- a/src/DasherCore/Common/AppSettingsData.h +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once - -#include "DasherCore/Parameters.h" - -// This file is an include file purely for the purposes of -// cross-platform consistency. IT WILL NOT LINK IF INCLUDED IN MORE -// THAN ONE PLACE! You probably want to obtain values via the -// application settings object instead. - -// The following tables contain details of the application specific -// settings. Fields are: -// -// Key: See corresponding enum in AppSettingsHeader.h - entries *must* -// be in the same order as in the enum -// -// Registry Name: Short text, without spaces. Used as the registry key -// and for other 'machine readable' purposes. Please make the name -// indicative of the purpose of the setting. -// -// Persistence: Whether this should be persistent or set at the begining of each setting. -// -// Default value: Hopefully pretty obvious -// -// Human-readable Name: Used for help text etc. Please make sure this -// is useful - -Dasher::Settings::bp_table app_boolparamtable[] = { - {APP_BP_TIME_STAMP, "TimeStampNewFiles", Persistence::PERSISTENT, true, "TimeStampNewFiles"}, - {APP_BP_CONFIRM_UNSAVED, "ConfirmUnsavedFiles", Persistence::PERSISTENT, true, "ConfirmUnsavedFiles"}, - {APP_BP_SHOW_TOOLBAR, "ViewToolbar", Persistence::PERSISTENT, true, "ViewToolbar"}, - {APP_BP_SHOW_STATUSBAR, "ViewStatusbar", Persistence::PERSISTENT, true, "ViewStatusbar"}, - {APP_BP_MIRROR_LAYOUT, "MirrorLayout", Persistence::PERSISTENT, false, "MirrorLayout"}, - {APP_BP_FULL_SCREEN, "FullScreen", Persistence::PERSISTENT, false, "FullScreen"}, - {APP_BP_RESET_ON_FOCUS_CHANGE, "ResetOnFocusChange", Persistence::PERSISTENT, true, "ResetOnFocusChange"}, -}; - -Dasher::Settings::lp_table app_longparamtable[] = { - {APP_LP_FILE_ENCODING, "FileEncodingFormat", Persistence::PERSISTENT, -1, "FileEncodingFormat"}, - {APP_LP_EDIT_FONT_SIZE, "EditFontSize", Persistence::PERSISTENT, 0, "EditFontSize"}, - {APP_LP_EDIT_SIZE, "EditSize", Persistence::PERSISTENT, 75, "The size of the edit window"}, - {APP_LP_SCREEN_WIDTH, "ScreenWidth", Persistence::PERSISTENT, 400, "ScreenWidth"}, - {APP_LP_SCREEN_HEIGHT, "ScreenHeight", Persistence::PERSISTENT, 500, "ScreenHeight"}, - {APP_LP_STYLE, "AppStyle", Persistence::PERSISTENT, 0, "Application style"}, - {APP_LP_X, "XPosition", Persistence::PERSISTENT, 100, "X location of window"}, - {APP_LP_Y, "YPosition", Persistence::PERSISTENT, 100, "Y location of window"}, -}; - -Dasher::Settings::sp_table app_stringparamtable[] = { - {APP_SP_EDIT_FONT, "EditFont", Persistence::PERSISTENT, "Sans 10", "EditFont"}, - {APP_SP_TOOLBAR_ID, "ToolbarID", Persistence::PERSISTENT, "", "ToolbarID"}, -}; diff --git a/src/DasherCore/Common/MSVC_Unannoy.h b/src/DasherCore/Common/MSVC_Unannoy.h deleted file mode 100644 index 95a65f8db..000000000 --- a/src/DasherCore/Common/MSVC_Unannoy.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -// Sorry about this frequently included header non-VC++ users. -// It shouldn't do any harm. - -// Warning 4786 is so annoying if you have VC++ 6. -// It produces *pages* of complaints if you use certain STL headers. -// The warning just means that VC++ is unable to produce certain debug -// information - there is nothing wrong with the code. IAM 08/2002 -#ifdef _MSC_VER -#pragma warning(disable : 4786) -#pragma warning(disable : 4018) - -#pragma warning(disable : 4996) // warning C4996: 'fopen' was declared deprecated -#endif diff --git a/src/DasherCore/Common/OpenGLScreen.h b/src/DasherCore/Common/OpenGLScreen.h deleted file mode 100644 index 23cb9a2b3..000000000 --- a/src/DasherCore/Common/OpenGLScreen.h +++ /dev/null @@ -1,86 +0,0 @@ -// -// AlphabetLetter.h -// Dasher -// -// Created by Alan Lawrence on 20/03/2009. -// Copyright 2009 Cavendish Laboratory. All rights reserved. -// - -#pragma once - -#if TARGET_OS_IPHONE -#import -#else -#import -#import -#endif - -#import "DasherScreen.h" -#include - -namespace Dasher { -class OpenGLScreen : public CLabelListScreen { - protected: - class AlphabetLetter : public Label { - public: // to OpenGLScreen and subclasses - all read by DrawString - NSString* str; - GLuint texture; - GLfloat texcoords[8]; - CGSize sz; // at base font size, or wrapped size if appropriate - AlphabetLetter(OpenGLScreen* pScreen, const std::string& strText, unsigned int iWrapSize); - ~AlphabetLetter(); - void PrepareTexture(); - }; - - public: - OpenGLScreen(screenint iWidth, screenint iHeight, GLshort backingWidth, GLshort backingHeight, GLfloat tc_x, - GLfloat tc_y, GLuint* textures); - ~OpenGLScreen(); - - /// Note, subclasses should override to additionally have buffers ready, etc. - void Display(); - void SetColourScheme(const CColourIO::ColourInfo* pColourScheme); - void Polyline(point* Points, int iNum, int iWidth, int iColour); - void Polygon(point* points, int iNum, int iFillColour, int iOutlineColour, int iWidth); - void DrawRectangle(int x1, int y1, int x2, int y2, int iFillColorIndex, int iOutlineColour, int iThickness); - void DrawCircle(screenint iCX, screenint iCY, screenint iR, int iFillColour, int iLineColour, int iLineWidth); - - bool MultiSizeFonts() { return true; } - AlphabetLetter* MakeLabel(const std::string& strText, unsigned int iWrapSize = 0); - std::pair TextSize(CDasherScreen::Label* label, unsigned int iFontSize); - void DrawString(CDasherScreen::Label* label, screenint x, screenint y, unsigned int iFontSize, int iColour); - // leave virtual: SendMarker, Display - protected: - void resize(screenint iWidth, screenint iHeight, GLshort backingWidth, GLshort backingHeight, GLfloat tc_x, - GLfloat tc_y); - void RegenerateLabels(); - /// Render a string onto a CoreGraphics context, using the context's current colour etc. - /// \param iFontSize font size to use - /// \param bWrap if true, constrain to screen width and wrap across multiple lines (if necessary); - /// if false, render on a single line. - virtual void RenderStringOntoCGContext(NSString* string, CGContextRef context, unsigned int iFontSize, - bool bWrap) = 0; - /// Get the pixel dimensions of a string when rendered in a specified font size - /// \param bWrap if true, string should be wrapped to the screen width, possibly - /// over multiple lines (=> returned height will reflect this); if false, - /// keep on one line (even if that makes it wider than the screen) - virtual CGSize TextSize(NSString* str, unsigned int iFontSize, bool bWrap) = 0; - - private: - typedef struct { - float r, g, b; - } colour_t; - - // colours in use, we cache these as floats for feeding to OpenGL. - colour_t* colourTable; - - /// Caches for circleCallbackWithCentrePoint:... (see therein) - float circ_rad; - GLshort* circ_coords; - int circPoints; - - GLshort rectcoords[8]; - GLfloat texcoords[8]; - GLuint* textures; -}; -} // namespace Dasher \ No newline at end of file diff --git a/src/DasherCore/Common/Platform/stdminmax.h b/src/DasherCore/Common/Platform/stdminmax.h deleted file mode 100644 index a7d7c2bf8..000000000 --- a/src/DasherCore/Common/Platform/stdminmax.h +++ /dev/null @@ -1,24 +0,0 @@ -// stdminmax.h -// -///////////////////////////////////////////////////////////////////////////// -// -// Copyright (c) 2005 David Ward -// -///////////////////////////////////////////////////////////////////////////// - -#pragma once - -#if _MSC_VER < 1300 - -namespace std { -template -inline T min(T t1, T t2) { // NOLINT(cert-dcl58-cpp) - return std::_cpp_min(t1, t2); -} -template -inline T max(T t1, T t2) { // NOLINT(cert-dcl58-cpp) - return std::_cpp_max(t1, t2); -} - -}; // namespace std -#endif diff --git a/src/DasherCore/Common/SBTree.cpp b/src/DasherCore/Common/SBTree.cpp deleted file mode 100644 index d062030d9..000000000 --- a/src/DasherCore/Common/SBTree.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SBTree.cpp - * Dasher - * - * Created by Alan Lawrence on 17/04/2009. - * Copyright 2009 Cavendish Laboratory. All rights reserved. - * - */ - -#include "SBTree.h" - -SBTree::SBTree(int iValue) { - m_iValue = iValue; - m_pLeft = nullptr; - m_pRight = nullptr; - m_iCount = 1; -} - -SBTree::~SBTree() { - if (m_pLeft) delete m_pLeft; - - if (m_pRight) delete m_pRight; -} - -void SBTree::Add(int iValue) { - ++m_iCount; - - if (iValue > m_iValue) { - if (m_pRight) - m_pRight->Add(iValue); - else - m_pRight = new SBTree(iValue); - } else { - if (m_pLeft) - m_pLeft->Add(iValue); - else - m_pLeft = new SBTree(iValue); - } -} - -SBTree* SBTree::Delete(int iValue) { - // Hmm... deleting is awkward in binary trees - - if (iValue == m_iValue) { - if (!m_pLeft) { - SBTree* pOldRight = m_pRight; - m_pRight = nullptr; - delete this; - return pOldRight; - } else { - SBTree* pOldLeft = m_pLeft; - pOldLeft->SetRightMost(m_pRight); - m_pLeft = nullptr; - m_pRight = nullptr; - delete this; - return pOldLeft; - } - } else if (iValue > m_iValue) { - --m_iCount; - m_pRight = m_pRight->Delete(iValue); - } else { - --m_iCount; - m_pLeft = m_pLeft->Delete(iValue); - } - - return this; -} - -void SBTree::SetRightMost(SBTree* pNewTree) { - if (pNewTree) m_iCount += pNewTree->GetCount(); - - if (m_pRight) - m_pRight->SetRightMost(pNewTree); - else - m_pRight = pNewTree; -} - -int SBTree::GetOffset(int iOffset) { - if (m_pLeft && (m_pLeft->GetCount() > iOffset)) - return m_pLeft->GetOffset(iOffset); - else if ((m_pLeft && (m_pLeft->GetCount() == iOffset)) || (!m_pLeft && (iOffset == 0))) - return m_iValue; - else if (m_pLeft) - return m_pRight->GetOffset(iOffset - m_pLeft->GetCount() - 1); - else - return m_pRight->GetOffset(iOffset - 1); -} \ No newline at end of file diff --git a/src/DasherCore/Common/SBTree.h b/src/DasherCore/Common/SBTree.h deleted file mode 100644 index 9c7e325d3..000000000 --- a/src/DasherCore/Common/SBTree.h +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SBTree.h - * Dasher - * - * Created by Alan Lawrence on 17/04/2009. - * Copyright 2009 Cavendish Laboratory. All rights reserved. - * - */ - -#pragma once - -class SBTree { - public: - SBTree(int iValue); - ~SBTree(); - - void Add(int iValue); - SBTree* Delete(int iValue); - - int GetCount() { return m_iCount; }; - - int GetOffset(int iOffset); - - private: - void SetRightMost(SBTree* pNewTree); - int m_iValue; - SBTree* m_pLeft; - SBTree* m_pRight; - int m_iCount; -}; diff --git a/src/DasherCore/Common/Trace.cpp b/src/DasherCore/Common/Trace.cpp deleted file mode 100644 index 8c64ea7c1..000000000 --- a/src/DasherCore/Common/Trace.cpp +++ /dev/null @@ -1,24 +0,0 @@ -// Trace.cpp -// -// Copyright (c) 2005 David Ward - -#include "Trace.h" -#include -#include - -// Track memory leaks on Windows to the line that new'd the memory -#ifdef _WIN32 -#ifdef _DEBUG -#define DEBUG_NEW new (_NORMAL_BLOCK, THIS_FILE, __LINE__) -#define new DEBUG_NEW -#undef THIS_FILE -static char THIS_FILE[] = __FILE__; -#endif -#endif - -// Customize behaviour of Trace here - -// Send Trace to stdout -void DasherTraceOutputImpl(const char* pszFormat, va_list vargs) { - vfprintf(stdout, pszFormat, vargs); -} diff --git a/src/DasherCore/Common/Trace.h b/src/DasherCore/Common/Trace.h deleted file mode 100644 index 05e7af215..000000000 --- a/src/DasherCore/Common/Trace.h +++ /dev/null @@ -1,49 +0,0 @@ -// Trace.h -// -// Copyright (c) 2005 David Ward - -#pragma once - -// Trace is a mechanism for printf-like debugging that can be switched on/off -// at compile time - -// To use Trace, define DASHER_TRACE in your build files, or uncomment the -// following line -// #define DASHER_TRACE - -// Use the DASHER_TRACEOUTPUT macro to format a message to trace -// Syntax is identicaly to printf: -// -// int i=6; -// DASHER_TRACEOUTPUT("Hello World %d", i); - -// The behaviour of DASHER_TRACEOUTPUT can be customized by changing -// DasherTraceOutputImpl in Trace.cpp - -// Note that if DASHER_TRACE is not defined, trace code should be completely -// removed by the compiler - -#include - -void DasherTraceOutput(const char* pszFormat, ...); -void DasherTraceOutputImpl(const char* pszFormat, va_list vargs); - -inline void DasherTraceOutput(const char* pszFormat, ...) { - va_list v; - va_start(v, pszFormat); - DasherTraceOutputImpl(pszFormat, v); - va_end(v); -} - -// Define main Trace macro - -#ifdef DASHER_TRACE - -// Active -#define DASHER_TRACEOUTPUT DasherTraceOutput -#else - -// Inactive - function should never get called -#define DASHER_TRACEOUTPUT 1 ? (void)0 : DasherTraceOutput - -#endif // DASHER_TRACE diff --git a/src/DasherCore/Common/myassert.h b/src/DasherCore/Common/myassert.h index a4a5d3fd7..b019ba714 100644 --- a/src/DasherCore/Common/myassert.h +++ b/src/DasherCore/Common/myassert.h @@ -24,10 +24,23 @@ #include #endif -// The DASHER_ASSERT macro causes execution to break into the debugger in DEBUG mode -// In non-debug debug builds - no check is done +// The DASHER_ASSERT macro fires in debug builds (i.e. when NDEBUG is not +// defined — the standard C/C++ convention that CMake's Debug preset +// follows). In release builds it compiles to nothing. +// +// Historically this gate used "#ifdef DEBUG", which the project never +// defined anywhere — so every DASHER_ASSERT was silently ((void)true) +// in every build, masking real bugs (e.g. the visibleRegion/vr typo in +// DasherView.cpp that compiled only because the expression was never +// parsed). Switching to NDEBUG matches semantics and makes +// asserts active in Debug CI runs. + +#ifdef NDEBUG + +#define DASHER_ASSERT(expr) ((void)0) + +#else // !NDEBUG — debug build -#ifdef DEBUG #ifdef _WIN32 #include @@ -41,12 +54,7 @@ #endif // _WIN32 -#else - -// Non-debug version (assertions disabled) -#define DASHER_ASSERT(expr) ((void)true) - -#endif // DEBUG +#endif // NDEBUG ///////////////////////////////////////////////////////////////////////////// diff --git a/src/DasherCore/Common/mydebug.h b/src/DasherCore/Common/mydebug.h deleted file mode 100644 index 87a85d1f1..000000000 --- a/src/DasherCore/Common/mydebug.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -#define DASHER_DEBUG(x) g_message(x) diff --git a/src/DasherCore/Common/round.h b/src/DasherCore/Common/round.h deleted file mode 100644 index 0ad3a2c8b..000000000 --- a/src/DasherCore/Common/round.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -extern "C" { -double round(double); -} diff --git a/src/DasherCore/ControlManager.h b/src/DasherCore/ControlManager.h index fcf430f1e..6bf8194f0 100644 --- a/src/DasherCore/ControlManager.h +++ b/src/DasherCore/ControlManager.h @@ -6,6 +6,38 @@ // generic, extensible action framework. Both alphabet symbol nodes and control // nodes create and execute actions through the same ActionRegistry. // +// ┌─ Architecture ───────────────────────────────────────────────────────┐ +// │ │ +// │ ActionRegistry ◄── factories registered by name (XML-driven) │ +// │ │ │ +// │ ▼ │ +// │ ControlAction (abstract base) │ +// │ ├── StopAction, PauseAction │ +// │ ├── MoveAction, DeleteAction │ +// │ ├── TextActionBase → SpeakAction, CopyAction │ +// │ ├── TextOutputAction, TextDeleteAction │ +// │ ├── FixedSpeakAction, SpeakCancelAction │ +// │ ├── ChangeSettingAction │ +// │ ├── KeyboardAction │ +// │ ├── SocketOutputAction (parsed from alphabet XML) │ +// │ ├── ATSPIAction (parsed from alphabet XML) │ +// │ └── CustomAction (registered by frontends via C API) │ +// │ │ │ +// │ ▼ │ +// │ NodeTemplate ── holds a list of actions + successor templates │ +// │ │ │ +// │ ▼ │ +// │ CContNode ── a CDasherNode that wraps a NodeTemplate │ +// │ │ │ +// │ ▼ │ +// │ CControlManager ── owns the root template, parses control.xml, │ +// │ manages the action registry │ +// │ │ +// │ When the user navigates into a CContNode, its NodeTemplate's │ +// │ actions are executed (Do()). If a successor is nullptr, it's an │ +// │ escape — a bridge back to the alphabet node tree. │ +// └──────────────────────────────────────────────────────────────────────┘ +// // Copyright (c) 2007-2024 The Dasher Team // // This file is part of Dasher. Dasher is free software; you can redistribute @@ -253,7 +285,10 @@ class KeyboardAction : public ControlAction { [[maybe_unused]] std::vector> m_keycodes; }; -/// Output to a socket. +/// Output to a socket. Parsed from alphabet XML `` elements. +/// The execute() method is currently a no-op (socket output was removed from +/// DasherCore), but the class is still instantiated by AlphIO for backward +/// compatibility with v4/v5 alphabet files. class SocketOutputAction : public ControlAction { public: SocketOutputAction(std::string socketName, std::string action, bool addNewLine) @@ -262,12 +297,14 @@ class SocketOutputAction : public ControlAction { void execute(CDasherInterfaceBase* intf) override; private: - [[maybe_unused]] std::string m_socketName; - [[maybe_unused]] std::string m_action; - [[maybe_unused]] bool m_addNewLine; + std::string m_socketName; + std::string m_action; + bool m_addNewLine; }; -/// ATSPI accessibility action. +/// ATSPI accessibility action. Parsed from alphabet XML `` elements. +/// Like SocketOutputAction, execute() is currently a no-op but the class is +/// instantiated by AlphIO for backward compatibility. class ATSPIAction : public ControlAction { public: ATSPIAction(std::string action) : m_action(std::move(action)) {} diff --git a/src/DasherCore/ConversionManager.cpp b/src/DasherCore/ConversionManager.cpp index 4fd89d3dd..5ff4f2e5c 100644 --- a/src/DasherCore/ConversionManager.cpp +++ b/src/DasherCore/ConversionManager.cpp @@ -214,7 +214,6 @@ void CConversionManager::CConvNode::PopulateChildren() { // Finally loop through and create the children for (std::vector::const_iterator it = vChildren.begin(); it != vChildren.end(); it++) { - // std::cout << "Current scec: " << pCurrentSCEChild << std::endl; SCENode* pCurrentSCEChild(*it); DASHER_ASSERT(pCurrentSCEChild != NULL); unsigned int iLbnd(iCum); @@ -226,8 +225,6 @@ void CConversionManager::CConvNode::PopulateChildren() { // TODO: Parameters here are placeholders - need to figure out // what's right - // std::cout << "#" << pCurrentSCEChild->pszConversion << "#" << std::endl; - CConvNode* pNewNode = mgr()->makeNode(offset() + 1, mgr()->GetLabel(pCurrentSCEChild->pszConversion)); // TODO: Reimplement ---- diff --git a/src/DasherCore/ConversionManager.h b/src/DasherCore/ConversionManager.h index 730ea11ef..7f34e92a4 100644 --- a/src/DasherCore/ConversionManager.h +++ b/src/DasherCore/ConversionManager.h @@ -62,8 +62,6 @@ class CDasherInterfaceBase; /// management process. /// class CConversionManager : public CNodeManager { - friend class CConvertingAlphMgr; - protected: class CConvNode; // fwd decl public: diff --git a/src/DasherCore/ConvertingAlphMgr.cpp b/src/DasherCore/ConvertingAlphMgr.cpp deleted file mode 100644 index fe995acbd..000000000 --- a/src/DasherCore/ConvertingAlphMgr.cpp +++ /dev/null @@ -1,49 +0,0 @@ -/* - * ConvertingAlphMgr.cpp - * Dasher - * - * Created by Alan Lawrence on 06/08/2010. - * Copyright 2010 Cavendish Laboratory. All rights reserved. - * - */ - -#include "ConvertingAlphMgr.h" -#include "NodeCreationManager.h" - -using namespace Dasher; - -CConvertingAlphMgr::CConvertingAlphMgr(CSettingsStore* pSettingsStore, CDasherInterfaceBase* pInterface, - CNodeCreationManager* pNCManager, CConversionManager* pConvMgr, - const CAlphInfo* pAlphabet) - : CAlphabetManager(pSettingsStore, pInterface, pNCManager, pAlphabet), m_pConvMgr(pConvMgr) {} - -void CConvertingAlphMgr::MakeLabels(CDasherScreen* pScreen) { - CAlphabetManager::MakeLabels(pScreen); - m_pConvMgr->ChangeScreen(pScreen); -} - -CConvertingAlphMgr::~CConvertingAlphMgr() {} - -CDasherNode* CConvertingAlphMgr::CreateSymbolNode(CAlphNode* pParent, symbol iSymbol) { - // int i=m_pAlphabet->iEnd; - if (iSymbol == m_pAlphabet->iEnd) { - std::vector* pCProb(pParent->GetProbInfo()); - (void)pCProb; - DASHER_ASSERT(pCProb->size() == m_pAlphabet->iEnd + 1); // initial 0, final conversion prob - - // this used to be the "CloneAlphContext" method. Why it uses the - // ConversionManager's LM to clone a context from an Alphabet Node, - // I don't know - not sure how LanguageModelling WRT conversion - // is supposed to work... - CLanguageModel::Context iContext = (pParent->iContext) - ? m_pConvMgr->m_pLanguageModel->CloneContext(pParent->iContext) - : m_pConvMgr->m_pLanguageModel->CreateEmptyContext(); - - // ACL setting m_iOffset+1 for consistency with "proper" symbol nodes... - return m_pConvMgr->GetRoot(pParent->offset() + 1, iContext); - // Note: previous code used (*pCProb)[i-1] and (*pCProb)[i] instead of - // iLbnd and iHbnd passed in; presumably these must have been the same? - } else { - return CAlphabetManager::CreateSymbolNode(pParent, iSymbol); - } -} diff --git a/src/DasherCore/ConvertingAlphMgr.h b/src/DasherCore/ConvertingAlphMgr.h deleted file mode 100644 index 999cb0c7d..000000000 --- a/src/DasherCore/ConvertingAlphMgr.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * ConvertingAlphMgr.h - * Dasher - * - * Created by Alan Lawrence on 06/08/2010. - * Copyright 2010 Cavendish Laboratory. All rights reserved. - * - */ - -#pragma once - -#include "AlphabetManager.h" -#include "ConversionManager.h" - -namespace Dasher { -// TODO Need to override CreateLanguageModel to use something appropriate for conversion. -// The created model, needs to have a GetSize() _including_ the conversion node -// (as this is not included in the Alphabet's GetNumberTextSymbols). -// TODO in fact IterateChildGroups will not include the conversion symbol (expected by -// CreateSymbolNode below) either, as it stops at Alphabet GetNumberTextSymbols too... -// TODO do we also need to override GetProbs? Existing impl will add uniformity onto the conversion root too. -class CConvertingAlphMgr : public CAlphabetManager { - public: - CConvertingAlphMgr(CSettingsStore* pSettingsStore, CDasherInterfaceBase* pInterface, - CNodeCreationManager* pNCManager, CConversionManager* pConvMgr, const CAlphInfo* pAlphabet); - /// Override to also tell the ConversionManager that the screen has changed. - void MakeLabels(CDasherScreen* pScreen) override; - virtual ~CConvertingAlphMgr(); - - protected: - /// Override to return a conversion root for iSymbol==(one beyond last alphabet symbol) - virtual CDasherNode* CreateSymbolNode(CAlphNode* pParent, symbol iSymbol) override; - - private: - CConversionManager* m_pConvMgr; - - // TODO do we need to override - // void GetProbs(vector *pProbInfo, CLanguageModel::Context context); - // to do something for the conversion symbol, or does the LM do that for us??? -}; -} // namespace Dasher diff --git a/src/DasherCore/DasherButtons.cpp b/src/DasherCore/DasherButtons.cpp index 956e6d6d0..509612d27 100644 --- a/src/DasherCore/DasherButtons.cpp +++ b/src/DasherCore/DasherButtons.cpp @@ -3,6 +3,8 @@ // Idea - should back off button always just undo the previous 'forwards' button? +#include + #include "DasherButtons.h" #include "DasherScreen.h" #include "DasherInterfaceBase.h" diff --git a/src/DasherCore/DasherInterfaceBase.cpp b/src/DasherCore/DasherInterfaceBase.cpp index 08222cb40..0675ec2f6 100644 --- a/src/DasherCore/DasherInterfaceBase.cpp +++ b/src/DasherCore/DasherInterfaceBase.cpp @@ -27,8 +27,6 @@ #include "DasherModel.h" #include "Event.h" #include "NodeCreationManager.h" -#include "UserLog.h" -#include "BasicLog.h" #include "GameModule.h" // Input filters @@ -62,14 +60,6 @@ static std::string alphabetIdToFilename(const std::string& alphId) { #include "FileUtils.h" #include "SmoothingFilter.h" -#include "DasherCore/FileLogger.h" -#ifndef NDEBUG -const eLogLevel g_iLogLevel = eLogLevel::logDEBUG; -const int g_iLogOptions = logTimeStamp | logDateStamp | logDeleteOldFile; -#else -const eLogLevel g_iLogLevel = eLogLevel::logNORMAL; -const int g_iLogOptions = logTimeStamp | logDateStamp; -#endif using namespace Dasher; @@ -83,12 +73,6 @@ CDasherInterfaceBase::CDasherInterfaceBase(CSettingsStore* pSettingsStore) m_pSettingsStore->OnParameterChanged.Subscribe(this, [this](Parameter p) { HandleParameterChange(p); }); - // Global logging object we can use from anywhere - // Skip in low-memory mode (keyboard extension) to avoid sandbox write violations - if (!m_bLowMemoryMode) { - m_pGlobalApplicationLog = std::make_unique("dasher.log", g_iLogLevel, g_iLogOptions); - } - OnEditEvent.Subscribe(this, [this](CEditEvent::EditEventType type, const std::string& strText, CDasherNode*) { if (this->GetGameModule() || !m_pSettingsStore->GetBoolParameter(BP_SPEAK_WORDS) || !this->SupportsSpeech()) return; @@ -142,12 +126,6 @@ void CDasherInterfaceBase::Realize(unsigned long ulTime) { // TODO: Sort out log type selection - const int iUserLogLevel = m_pSettingsStore->GetLongParameter(LP_USER_LOG_LEVEL_MASK); - if (iUserLogLevel == 10) - m_pUserLog = std::make_unique(m_pSettingsStore, this); - else if (iUserLogLevel > 0) - m_pUserLog = std::make_unique(m_pSettingsStore, this, iUserLogLevel); - CreateModules(); ChangeAlphabet(); // This creates the NodeCreationManager, the Alphabet, @@ -166,9 +144,7 @@ void CDasherInterfaceBase::Realize(unsigned long ulTime) { // InvalidateContext(true); ScheduleRedraw(); - // All the setup is done by now, so let the user log object know - // that future parameter changes should be logged. - if (m_pUserLog != NULL) m_pUserLog->InitIsDone(); + ScheduleRedraw(); } CDasherInterfaceBase::~CDasherInterfaceBase() { @@ -182,9 +158,6 @@ CDasherInterfaceBase::~CDasherInterfaceBase() { // Clean up cached lock label (created by Redraw when locked) delete m_pLockLabel; - - // When we destruct on shutdown, we'll output any detailed log file - if (m_pUserLog) m_pUserLog->OutputFile(); } void CDasherInterfaceBase::HandleParameterChange(Parameter parameter) { @@ -349,8 +322,6 @@ bool CDasherInterfaceBase::hasDone() { void CDasherInterfaceBase::Done() { ScheduleRedraw(); - if (m_pUserLog != NULL) m_pUserLog->StopWriting((float)GetNats()); - if (m_pSettingsStore->GetBoolParameter(BP_COPY_ALL_ON_STOP) && SupportsClipboard()) { CopyToClipboard(GetAllContext()); } @@ -433,13 +404,6 @@ void CDasherInterfaceBase::NewFrame(unsigned long iTime, bool bForceRedraw) { } // 2. Render nodes decorations, messages bBlit = Redraw(iTime, bForceRedraw, *pol); - - if (m_pUserLog != nullptr) { - //(any) UserLogBase will have been watching output events to gather information - // about symbols added/deleted; this tells it to apply that information at end-of-frame - // (previously DashIntf gathered the info, and then passed it to the logger here). - m_pUserLog->FrameEnded(); - } } if (FinishRender(iTime)) bBlit = true; if (bBlit) m_DasherScreen->Display(); @@ -459,7 +423,6 @@ void CDasherInterfaceBase::ExecuteDelayedActions() { void CDasherInterfaceBase::onUnpause(unsigned long lTime) { // TODO When Game+UserLog modules are combined => reduce to just one call here if (m_pGameModule) m_pGameModule->StartWriting(lTime); - if (m_pUserLog) m_pUserLog->StartWriting(); } bool CDasherInterfaceBase::Redraw(unsigned long ulTime, bool bRedrawNodes, CExpansionPolicy& policy) { @@ -615,11 +578,6 @@ void CDasherInterfaceBase::ResetParameter(Parameter parameter) { m_pSettingsStore->ResetParameter(parameter); } -// We need to be able to get at the UserLog object from outside the interface -CUserLogBase* CDasherInterfaceBase::GetUserLogPtr() { - return m_pUserLog.get(); -} - void CDasherInterfaceBase::KeyDown(unsigned long iTime, Keys::VirtualKey Key) { if (isLocked()) return; @@ -690,7 +648,6 @@ void CDasherInterfaceBase::CreateModules() { GetModuleManager()->RegisterInputMethodModule(std::make_unique(m_pSettingsStore, this)); GetModuleManager()->RegisterInputMethodModule( std::make_unique(m_pSettingsStore, this, m_pFramerate.get())); - // WIP Temporary as too many segfaults! //RegisterModule(new CDemoFilter(this, this, m_pFramerate)); } std::vector CDasherInterfaceBase::GetPermittedValues(Parameter parameter) { diff --git a/src/DasherCore/DasherInterfaceBase.h b/src/DasherCore/DasherInterfaceBase.h index f6b2c719e..b25a56418 100644 --- a/src/DasherCore/DasherInterfaceBase.h +++ b/src/DasherCore/DasherInterfaceBase.h @@ -54,7 +54,6 @@ class CDasherInterfaceBase; class FileUtils; } // namespace Dasher -class CUserLogBase; class CNodeCreationManager; /// \defgroup Core Core Dasher classes @@ -83,8 +82,6 @@ class Dasher::CDasherInterfaceBase : public CMessageDisplay, private NoClones { /// be replaced by properly encapsulated equivalents. /// @{ - CUserLogBase* GetUserLogPtr(); - // @} /// @@ -115,8 +112,6 @@ class Dasher::CDasherInterfaceBase : public CMessageDisplay, private NoClones { CColorIO* GetColorIO() { return m_ColorIO.get(); } - CFileLogger* GetGlobalApplicationLogger() const { return m_pGlobalApplicationLog.get(); } - //@} /// Called when a parameter changes - but *after* components have been notified. @@ -519,8 +514,6 @@ class Dasher::CDasherInterfaceBase : public CMessageDisplay, private NoClones { std::unique_ptr m_AlphIO; std::unique_ptr m_ColorIO; std::unique_ptr m_pNCManager; - std::unique_ptr m_pUserLog; - std::unique_ptr m_pGlobalApplicationLog; // the game mode module - only // initialized if game mode is enabled diff --git a/src/DasherCore/DasherModel.cpp b/src/DasherCore/DasherModel.cpp index eafb4d4fa..8c6dc39e0 100644 --- a/src/DasherCore/DasherModel.cpp +++ b/src/DasherCore/DasherModel.cpp @@ -75,7 +75,6 @@ void CDasherModel::ClearScheduledSteps() { } void CDasherModel::Make_root(CDasherNode* pNewRoot) { - // std::cout << "Make root" << std::endl; DASHER_ASSERT(pNewRoot != NULL); DASHER_ASSERT(pNewRoot->Parent() == m_Root); @@ -93,7 +92,7 @@ void CDasherModel::Make_root(CDasherNode* pNewRoot) { delete oldroots[0]; oldroots.pop_front(); } - DASHER_ASSERT(pNewRoot->GetFlag(NF_SEEN)); + DASHER_ASSERT(pNewRoot->GetFlag(CDasherNode::NF_SEEN)); m_Root = pNewRoot; // Update the root coordinates, as well as any currently scheduled locations @@ -156,7 +155,7 @@ bool CDasherModel::Reparent_root() { // (or committing would enter the node into the LM a second time) // Update the root coordinates to reflect the new root - DASHER_ASSERT(pNewRoot->GetFlag(NF_SEEN)); + DASHER_ASSERT(pNewRoot->GetFlag(CDasherNode::NF_SEEN)); m_Root = pNewRoot; m_Rootmax = m_Rootmax + ((NORMALIZATION - upper) * iRootWidth) / iRange; @@ -375,12 +374,10 @@ void CDasherModel::ScheduleOneStep(dasherint y1, dasherint y2, int nSteps, int l d = (MAX_Y - targetRange) * 2 * static_cast(limX); bool bOver = std::max(llabs(m1), llabs(m2)) > std::numeric_limits::max() / n; if (bOver) { - // std::cout << "Overflow in max-speed-limit " << m1 << "," << m2 << " =wd> " << ((m1*n)/d) << "," << // ((m2*n)/d); so do it a harder way, but which uses smaller intermediates: // (Yes, this is valid even if !bOver. Could use it all the time?) m1 = (m1 / d) * n + ((m1 % d) * n) / d; m2 = (m2 / d) * n + ((m2 % d) * n) / d; - // std::cout << " => " << m1 << "," << m2 << std::endl; } else { m1 = (m1 * n) / d; m2 = (m2 * n) / d; diff --git a/src/DasherCore/DasherNode.cpp b/src/DasherCore/DasherNode.cpp index 1457952d9..50d7745b5 100644 --- a/src/DasherCore/DasherNode.cpp +++ b/src/DasherCore/DasherNode.cpp @@ -39,13 +39,10 @@ CDasherNode::CDasherNode(int iOffset, CDasherScreen::Label* pLabel) // TODO: put this back to being inlined CDasherNode::~CDasherNode() { - // std::cout << "Deleting node: " << this << std::endl; // Release any storage that the node manager has allocated, // unreference ref counted stuff etc. DeleteChildren(); - // std::cout << "done." << std::endl; - iNumNodes--; } diff --git a/src/DasherCore/DasherView.cpp b/src/DasherCore/DasherView.cpp index d01983afe..1175ec35f 100644 --- a/src/DasherCore/DasherView.cpp +++ b/src/DasherCore/DasherView.cpp @@ -55,11 +55,11 @@ bool CDasherView::ClipLineToVisible(myint& x1, myint& y1, myint& x2, myint& y2) // ok. have x1 <= x2... const CDasherView::DasherCoordScreenRegion vr = VisibleRegion(); if (x1 > vr.maxX) { - DASHER_ASSERT(x2 > visibleRegion.maxX); + DASHER_ASSERT(x2 > vr.maxX); return false; // entirely offscreen! } if (x2 < vr.minX) { - DASHER_ASSERT(x1 < visibleRegion.minX); + DASHER_ASSERT(x1 < vr.minX); return false; } if (x1 < vr.minX) { diff --git a/src/DasherCore/DasherViewSquare.cpp b/src/DasherCore/DasherViewSquare.cpp index d46e0af6a..418b791b1 100644 --- a/src/DasherCore/DasherViewSquare.cpp +++ b/src/DasherCore/DasherViewSquare.cpp @@ -744,7 +744,7 @@ void CDasherViewSquare::DasherDrawCube(myint iDasherMaxX, myint iDasherMinY, myi outlineColor, iThickness); } -bool CDasherViewSquare::CoversCrosshair(myint Range, myint y1, myint y2) { +bool CDasherViewSquare::CoversCrosshair(myint Range, myint y1, myint y2) const { if (Range > CDasherModel::ORIGIN_X && y1 < CDasherModel::ORIGIN_Y && y2 > CDasherModel::ORIGIN_Y) { switch (m_pSettingsStore->GetLongParameter(LP_SHAPE_TYPE)) { case Options::DISJOINT_RECTANGLE: @@ -803,7 +803,7 @@ void CDasherViewSquare::NewRender(CDasherNode* pCurrentNode, myint y1, myint y2, CExpansionPolicy& policy, double dMaxCost, CDasherNode*& pCurrentTopCenterNode, CubeDepthLevel nodeDepth, CubeDepthLevel parentDepth, ScreenRegion parentScreenBounds) { - DASHER_ASSERT_VALIDPTR_RW(pRender); + DASHER_ASSERT_VALIDPTR_RW(pCurrentNode); const DasherCoordScreenRegion visibleRegion = VisibleRegion(); diff --git a/src/DasherCore/DasherViewSquare.h b/src/DasherCore/DasherViewSquare.h index b104f37af..2da7f7de6 100644 --- a/src/DasherCore/DasherViewSquare.h +++ b/src/DasherCore/DasherViewSquare.h @@ -189,7 +189,7 @@ class Dasher::CDasherViewSquare : public CDasherView { const myint m_Y3 = static_cast(0.05 * CDasherModel::MAX_Y); inline void Crosshair(); - bool CoversCrosshair(myint Range, myint y1, myint y2); + bool CoversCrosshair(myint Range, myint y1, myint y2) const; ColorPalette::Color SimulateTransparency(CDasherNode* pCurrentNode) const; // Divides by SCALE_FACTOR, rounding away from 0 diff --git a/src/DasherCore/DefaultFilter.cpp b/src/DasherCore/DefaultFilter.cpp index 07df44b05..ecad86c59 100644 --- a/src/DasherCore/DefaultFilter.cpp +++ b/src/DasherCore/DefaultFilter.cpp @@ -258,7 +258,6 @@ double xmax(double y) { static const int a = 1, b = 1; static const double c = 100; return std::min(c, a * (exp(b * y * y) - 1)); - // cout << "xmax = " << xmax << endl; } void CDefaultFilter::ApplyTransform(myint& iDasherX, myint& iDasherY, CDasherView* pView) { diff --git a/src/DasherCore/DemoFilter.cpp b/src/DasherCore/DemoFilter.cpp deleted file mode 100644 index 9f5033652..000000000 --- a/src/DasherCore/DemoFilter.cpp +++ /dev/null @@ -1,144 +0,0 @@ -#include "DemoFilter.h" - -#include "DasherCore/Common/I18n.h" -#include - -#include "DasherInterfaceBase.h" - -#include "GameModule.h" - -using namespace Dasher; - -void CDemoFilter::GetUISettings(std::vector& List) { - CDynamicFilter::GetUISettings(List); - AddSettings(List, {BP_DRAW_MOUSE, BP_DRAW_MOUSE, BP_START_MOUSE, LP_MAX_BITRATE, LP_FRAMERATE, LP_FRAMERATE, - LP_LINE_WIDTH, LP_DEMO_SPRING, LP_DEMO_NOISE_MEM, LP_DEMO_NOISE_MAG}); -} - -CDemoFilter::CDemoFilter(CSettingsStore* pSettingsStore, CDasherInterfaceBase* pInterface, CFrameRate* pFramerate) - : CDynamicFilter(pSettingsStore, pInterface, pFramerate, _("Demo Mode (no input)")), m_dNoiseX(0.0), m_dNoiseY(0.0), - m_iDemoX(0), m_iDemoY(0) {} - -CDemoFilter::~CDemoFilter() {} - -bool CDemoFilter::DecorateView(CDasherView* pView, CDasherInput* pInput) { - - if (m_pSettingsStore->GetBoolParameter(BP_DRAW_MOUSE)) { - pView->DasherDrawCentredRectangle(m_iDemoX, m_iDemoY, 5, pView->GetNamedColor(NamedColor::inputPosition), - ColorPalette::noColor, false); - } - - myint x[2]; - myint y[2]; - - // Start of line is the crosshair location - - x[0] = CDasherModel::ORIGIN_X; - y[0] = CDasherModel::ORIGIN_Y; - - x[1] = m_iDemoX; - y[1] = m_iDemoY; - - // Actually plot the line - if (m_pSettingsStore->GetBoolParameter(BP_CURVE_MOUSE_LINE)) - pView->DasherSpaceLine(x[0], y[0], x[1], y[1], m_pSettingsStore->GetLongParameter(LP_LINE_WIDTH), - pView->GetNamedColor(NamedColor::inputLine)); - else - pView->DasherPolyline(x, y, 2, m_pSettingsStore->GetLongParameter(LP_LINE_WIDTH), - pView->GetNamedColor(NamedColor::inputLine)); - - return true; -} - -void CDemoFilter::Activate() { - m_pInterface->EnterGameMode(); - HandleEvent(LP_FRAMERATE); // just to make sure! -} - -void CDemoFilter::Deactivate() { - m_pInterface->LeaveGameMode(); -} - -std::pair GaussianRand() // Is there a random number class already? -{ - double u1 = (double(rand()) + 1) / (double(RAND_MAX) + 1.0); // NOLINT(cert-msc30-c,cert-msc50-cpp) - double u2 = (double(rand()) + 1) / (double(RAND_MAX) + 1.0); // NOLINT(cert-msc30-c,cert-msc50-cpp) - double g1 = sqrt(-2.0 * log(u1)) * cos(6.283185307 * u2); - double g2 = sqrt(-2.0 * log(u1)) * sin(6.283185307 * u2); - // std::cout << u1 << " : " << u2 << " : " << g1 << std::endl; - return std::pair(g1, g2); -} - -void CDemoFilter::Timer(unsigned long Time, CDasherView* m_pDasherView, CDasherInput* pInput, - CDasherModel* m_pDasherModel, CExpansionPolicy** pol) { - if (isPaused()) return; - CGameModule* mod = (CGameModule*)m_pInterface->GetGameModule(); - const myint iTargetY(mod->m_iTargetY); - myint iCenterY = mod->ComputeBrachCenter(); - - // ...and now calculate the ideal direction... - double iIdealUnitVec[2]; - - iIdealUnitVec[0] = double(CDasherModel::ORIGIN_Y < iTargetY ? (iCenterY - CDasherModel::ORIGIN_Y) - : (CDasherModel::ORIGIN_Y - iCenterY)); - iIdealUnitVec[1] = double(CDasherModel::ORIGIN_Y < iTargetY ? CDasherModel::ORIGIN_X : -CDasherModel::ORIGIN_X); - double mag = sqrt((double)(iIdealUnitVec[0] * iIdealUnitVec[0] + iIdealUnitVec[1] * iIdealUnitVec[1])); - iIdealUnitVec[0] = iIdealUnitVec[0] / mag; - iIdealUnitVec[1] = iIdealUnitVec[1] / mag; - - // ...and then modify for realism... - // ...by adding noise... - std::pair noise = GaussianRand(); - m_dNoiseX = m_dNoiseOld * m_dNoiseX + m_dNoiseNew * noise.first; - m_dNoiseY = m_dNoiseOld * m_dNoiseY + m_dNoiseNew * noise.second; - - // ...and springy behaviour... - // if(!m_bSentenceFinished) { - const myint iNoiseMag(m_pSettingsStore->GetLongParameter(LP_DEMO_NOISE_MAG)); - m_iDemoX = myint((CDasherModel::ORIGIN_X + (1500 * iIdealUnitVec[0]) + iNoiseMag * m_dNoiseX) * m_dSpring + - (1.0 - m_dSpring) * m_iDemoX); - m_iDemoY = myint((CDasherModel::ORIGIN_Y + (1500 * iIdealUnitVec[1]) + iNoiseMag * m_dNoiseY) * m_dSpring + - (1.0 - m_dSpring) * m_iDemoY); - //} else { - // m_iDemoX = myint(ORIGIN_X*m_DemoCfg.dSpring + (1.0-m_DemoCfg.dSpring)*m_iDemoX); - // m_iDemoY = myint(ORIGIN_Y*m_DemoCfg.dSpring + (1.0-m_DemoCfg.dSpring)*m_iDemoY); - //} - - // ...and finally set the mouse coordinates. - - OneStepTowards(m_pDasherModel, m_iDemoX, m_iDemoY, Time, 1.0); -} - -void CDemoFilter::KeyDown(unsigned long iTime, Keys::VirtualKey Key, CDasherView* pDasherView, CDasherInput* pInput, - CDasherModel* pModel) { - - if ((Key == Keys::Big_Start_Stop_Key && m_pSettingsStore->GetBoolParameter(BP_START_SPACE)) || - (Key == Keys::Primary_Input && m_pSettingsStore->GetBoolParameter(BP_START_MOUSE))) { - if (isPaused()) - run(iTime); - else - pause(); - } -} - -void CDemoFilter::HandleEvent(Parameter parameter) { - switch (parameter) { - case LP_DEMO_SPRING: - case LP_DEMO_NOISE_MEM: - case LP_MAX_BITRATE: - case LP_FRAMERATE: { - // Recalculates the parameters used in the demo following a change in framerate or speed. - double spring = m_pSettingsStore->GetLongParameter(LP_DEMO_SPRING) / 100.0; - double noisemem = m_pSettingsStore->GetLongParameter(LP_DEMO_NOISE_MEM) / 100.0; - double lambda = 0.7 * m_pSettingsStore->GetLongParameter(LP_MAX_BITRATE) / - (double)m_pSettingsStore->GetLongParameter(LP_FRAMERATE); - - m_dSpring = (1 - exp(-spring * lambda)); - m_dNoiseNew = noisemem * (1 - exp(-lambda)); - m_dNoiseOld = sqrt(1.0 - m_dNoiseNew * m_dNoiseNew); - break; - } - default: - break; - } -} diff --git a/src/DasherCore/DemoFilter.h b/src/DasherCore/DemoFilter.h deleted file mode 100644 index 36b707bd0..000000000 --- a/src/DasherCore/DemoFilter.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include "DynamicFilter.h" - -namespace Dasher { -/// \ingroup InputFilter -/// @{ -class CDemoFilter : public CDynamicFilter { - public: - CDemoFilter(CSettingsStore* pSettingsStore, CDasherInterfaceBase* pInterface, CFrameRate* pFramerate); - virtual ~CDemoFilter(); - - virtual void HandleEvent(Parameter parameter); - - virtual bool DecorateView(CDasherView* pView, CDasherInput* pInput) override; - virtual void Timer(unsigned long Time, CDasherView* m_pDasherView, CDasherInput* pInput, - CDasherModel* m_pDasherModel, CExpansionPolicy** pol) override; - virtual void KeyDown(unsigned long iTime, Keys::VirtualKey Key, CDasherView* pDasherView, CDasherInput* pInput, - CDasherModel* pModel) override; - virtual void Activate() override; - virtual void Deactivate() override; - - virtual void GetUISettings(std::vector& List) override; - - private: - double m_dSpring, m_dNoiseNew, m_dNoiseOld; - double m_dNoiseX, m_dNoiseY; - myint m_iDemoX, m_iDemoY; -}; -} // namespace Dasher -/// @} diff --git a/src/DasherCore/DynamicButtons.cpp b/src/DasherCore/DynamicButtons.cpp index 659b374e5..af2b79243 100644 --- a/src/DasherCore/DynamicButtons.cpp +++ b/src/DasherCore/DynamicButtons.cpp @@ -85,11 +85,9 @@ void CDynamicButtons::ButtonEvent(unsigned long iTime, Keys::VirtualKey Key, int // What happens next depends on the state: if (isPaused()) { // Any button causes a restart - if (CUserLogBase* pUserLog = m_pInterface->GetUserLogPtr()) pUserLog->KeyDown(Key, iType, 1); run(iTime); } else if (isReversing()) { // Any button pauses - if (CUserLogBase* pUserLog = m_pInterface->GetUserLogPtr()) pUserLog->KeyDown(Key, iType, 2); m_pInterface->Done(); pause(); } else { @@ -98,13 +96,11 @@ void CDynamicButtons::ButtonEvent(unsigned long iTime, Keys::VirtualKey Key, int case 0: // single press if ((Key == Keys::Big_Start_Stop_Key) || (Key == Keys::Primary_Input)) { // dedicated pause button - if (CUserLogBase* pUserLog = m_pInterface->GetUserLogPtr()) pUserLog->KeyDown(Key, iType, 2); m_pInterface->Done(); pause(); break; } else if (Key == 1) { // dedicated reverse button - if (CUserLogBase* pUserLog = m_pInterface->GetUserLogPtr()) pUserLog->KeyDown(Key, iType, 6); reverse(iTime); break; } diff --git a/src/DasherCore/Event.h b/src/DasherCore/Event.h index 7324a37df..560f76aac 100644 --- a/src/DasherCore/Event.h +++ b/src/DasherCore/Event.h @@ -1,21 +1,49 @@ -#pragma once +// Event.h +// +// Copyright (c) The Dasher Team +// +// Signal/Slot (Publisher/Subscriber) pattern for DasherCore's internal +// event system. Used for parameter-change notifications, edit events, +// and view/model lifecycle notifications. +// +// Design notes: +// +// This file enhances the original void*-keyed event system with: +// 1. RAII Subscription guard — auto-unsubscribes on destruction, +// eliminating the classic signal/slot lifetime hazard. +// 2. Keyless Subscribe overload — new code doesn't need to pass `this`. +// +// The original void* Subscribe/Unsubscribe API is preserved unchanged +// for the 18 Subscribe and 22 Unsubscribe call sites throughout the +// engine. +// +// Note on iteration order: we use unordered_map (not std::map) for +// subscriber storage. std::map was tried for deterministic broadcast +// order but caused stuttering on Windows — its red-black tree allocates +// each node separately, creating cache misses during Broadcast. With +// typical subscriber counts (5-10), unordered_map's contiguous buckets +// are faster. Subscribers must not depend on call order. -// Classes representing different event types. +#pragma once #include #include +#include +#include namespace Dasher { class CEditEvent; -class CGameNodeDrawEvent; -class CDasherNode; // fwd decl, avoid include...we just store ptr +class CDasherNode; // fwd decl } // namespace Dasher + /// \ingroup Core /// @{ /// \defgroup Events Events generated by Dasher modules. /// @{ +/// Edit event — fired when text is added, deleted, or converted. +/// Carries the edit type, the affected text, and the node that caused it. class Dasher::CEditEvent { public: enum EditEventType { EDIT_OUTPUT = 1, EDIT_DELETE = 2, EDIT_CONVERT = 10, EDIT_PROTECT = 11 }; @@ -24,30 +52,149 @@ class Dasher::CEditEvent { : m_iEditType(iEditType), m_sText(sText), m_pNode(pNode) {} const EditEventType m_iEditType; const std::string m_sText; - /// Node causing the event - allows calling GetSymbolProb, offset(), etc. - /// _if necessary_ + /// Node causing the event — allows calling GetSymbolProb, offset(), etc. + /// if necessary. const CDasherNode* m_pNode; }; -/// @} /// @} -// Simple Event Implementation, very similar to a Signal/Slot (Publisher/Subscriber) Pattern +/// Signal/Slot event system — a typed publisher/subscriber mechanism. +/// +/// Usage as a publisher (class that owns an Event): +/// \code +/// class CSettingsStore { +/// public: +/// Event OnParameterChanged; // the signal +/// }; +/// \endcode +/// +/// Usage as a subscriber (RAII pattern — preferred for new code): +/// \code +/// class CMyModule { +/// Event::Subscription m_sub; // member — auto-cleans +/// public: +/// CMyModule(CSettingsStore* s) { +/// m_sub = s->OnParameterChanged.Subscribe( +/// [this](Parameter p) { HandleChange(p); }); +/// } +/// // No Unsubscribe needed in destructor — m_sub handles it. +/// }; +/// \endcode +/// +/// Legacy usage (void* key — still works, but requires manual cleanup): +/// \code +/// CMyModule(CSettingsStore* s) { +/// s->OnParameterChanged.Subscribe(this, +/// [this](Parameter p) { HandleChange(p); }); +/// } +/// ~CMyModule() { +/// s->OnParameterChanged.Unsubscribe(this); +/// } +/// \endcode template class Event { public: - void Subscribe(void* Listener, const std::function& Function) { Listeners[Listener] = Function; } + /// RAII subscription guard. Auto-unsubscribes when destroyed. + /// + /// Store as a member of the subscribing class. When the member goes + /// out of scope (class destructor runs), the subscription is removed + /// from the Event automatically — no manual Unsubscribe() needed. + /// + /// This eliminates the classic signal/slot lifetime hazard where a + /// destroyed subscriber's callback remains in the Event's listener + /// list, causing a use-after-free on the next Broadcast(). + class Subscription { + public: + Subscription() = default; + ~Subscription() { + if (m_event) m_event->unsubscribe(m_key); + } + + // Movable (for assignment from Subscribe return value) + Subscription(Subscription&& other) noexcept : m_event(other.m_event), m_key(other.m_key) { + other.m_event = nullptr; + } + Subscription& operator=(Subscription&& other) noexcept { + if (this != &other) { + if (m_event) m_event->unsubscribe(m_key); + m_event = other.m_event; + m_key = other.m_key; + other.m_event = nullptr; + } + return *this; + } - void Unsubscribe(void* Listener) { Listeners.erase(Listener); } + // Non-copyable (each subscription is a unique resource) + Subscription(const Subscription&) = delete; + Subscription& operator=(const Subscription&) = delete; - void Clear() { Listeners.clear(); } + private: + friend class Event; + Subscription(Event* event, void* key) : m_event(event), m_key(key) {} + Event* m_event = nullptr; + void* m_key = nullptr; + }; - void Broadcast(Args... i) { - for (auto& [key, value] : Listeners) { - if (value) value(i...); + /// Subscribe with a void* key (legacy API — preserved for backward + /// compatibility with existing callers). + /// + /// The key is typically `this` (the subscriber's pointer). It must + /// be unique per subscriber per Event — subscribing with the same + /// key twice replaces the previous callback. + /// + /// The caller MUST call Unsubscribe(this) before destruction to + /// avoid dangling pointers in the Event's listener list. + /// + /// \param listener unique key for this subscriber (typically `this`) + /// \param fn callback to invoke on Broadcast() + void Subscribe(void* listener, const std::function& fn) { m_listeners[listener] = fn; } + + /// Subscribe without a key (RAII pattern — preferred for new code). + /// + /// Each call creates a unique internal key, so subscribers don't need + /// to pass `this`. The returned Subscription must be stored (typically + /// as a member) to keep the subscription alive. + Subscription Subscribe(const std::function& fn) { + // Use a synthetic unique key. Incrementing a counter avoids collisions + // with real pointers (which are always even on most architectures). + void* key = reinterpret_cast(m_nextSyntheticKey); + m_nextSyntheticKey += 2; // skip even numbers (real pointers are even) + m_listeners[key] = fn; + return Subscription(this, key); + } + + /// Remove a subscriber by its key (legacy API). + /// Safe to call even if the key was never subscribed. + void Unsubscribe(void* listener) { m_listeners.erase(listener); } + + /// Remove all subscribers. + void Clear() { m_listeners.clear(); } + + /// Invoke all subscriber callbacks with the given arguments. + /// + /// Listeners are called in hash-table iteration order (not sorted). + /// Do not rely on call order — if multiple subscribers have + /// order-dependent side effects, refactor to remove the coupling. + void Broadcast(Args... args) { + for (auto& [key, fn] : m_listeners) { + if (fn) fn(args...); } } private: - std::unordered_map> Listeners; -}; \ No newline at end of file + friend class Subscription; + void unsubscribe(void* key) { m_listeners.erase(key); } + + // std::unordered_map for O(1) subscribe/unsubscribe. We previously + // tried std::map for deterministic broadcast order, but it caused + // stuttering on Windows (MSVC) due to per-node heap allocations and + // cache misses in the red-black tree. unordered_map's contiguous bucket + // array has better cache locality for the small subscriber counts + // (typically 5-10) used in DasherCore. + std::unordered_map> m_listeners; + + // Counter for synthetic keys (used by the keyless Subscribe overload). + // Starts at 1 (odd) to avoid colliding with real pointers (even). + uintptr_t m_nextSyntheticKey = 1; +}; diff --git a/src/DasherCore/FileLogger.cpp b/src/DasherCore/FileLogger.cpp deleted file mode 100644 index 0f8b29314..000000000 --- a/src/DasherCore/FileLogger.cpp +++ /dev/null @@ -1,244 +0,0 @@ -#include "FileUtils.h" - -#include -#include -#include "FileLogger.h" - -#include - -CFileLogger::CFileLogger(const std::string& strFilenamePath, eLogLevel iLogLevel, int iOptionsMask) - : m_iLogLevel(iLogLevel) { - // See what options are set in our bit mask options parameter - if (iOptionsMask & logFunctionEntryExit) m_bFunctionLogging = true; - if (iOptionsMask & logTimeStamp) m_bTimeStamp = true; - if (iOptionsMask & logDateStamp) m_bDateStamp = true; - if (iOptionsMask & logDeleteOldFile) m_bDeleteOldFile = true; - if (iOptionsMask & logFunctionTiming) m_bFunctionTiming = true; - if (iOptionsMask & logOutputScreen) m_bOutputScreen = true; - - // On windows anyway if somebody can open up a file with CreateFile() in a different - // directory and cause the working directory to change. We don't want our log file - // moving around, so we'll find a absolute path when we are created and stick to - // that for the remainder of our life. - m_strFilenamePath = Dasher::FileUtils::GetFullFilenamePath(strFilenamePath); - - // See if we should get rid of any existing filename with our given name. This prevents having - // to remember to delete the file before every new debugging run. - if (m_bDeleteOldFile) { - Dasher::FileUtils::WriteUserDataFile(strFilenamePath, "", - false); // Delete file contents. Consistent with old behavior. - } -} - -CFileLogger::~CFileLogger() { - - if (!m_bFunctionTiming) return; - - // Dump the results of our function timing logging - Log("%-60s%20s%10s", eLogLevel::logNORMAL, "Function", "Ticks", "Percent"); - Log("%-60s%20s%10s", eLogLevel::logNORMAL, "--------", "-----", "-------"); - - // First pass to count the max ticks - // We assume that there was a function logger on the outer most (main) program. - // This allows the percent reflect the relative time spent inside embedded calls. - - double max_duration = 0; - for (const auto& [function_name, duration] : m_mapFunctionDuration) { - if (duration > max_duration) max_duration = duration; - } - - for (const auto& [function_name, duration] : m_mapFunctionDuration) { - Log("%-60s%20.2f%10.2f", eLogLevel::logNORMAL, function_name.c_str(), duration, - static_cast(duration) / max_duration * 100.0); - } -} - -// Changes the filename of this logging object -void CFileLogger::SetFilename(const std::string& strFilename) { - m_strFilenamePath = strFilename; - - // See if we should get rid of any existing filename with our given name. This prevents having - // to remember to delete the file before every new debugging run. - if (m_bDeleteOldFile) { - Dasher::FileUtils::WriteUserDataFile(strFilename, "", - false); // Delete file contents. Consistent with old behavior. - } -} - -void CFileLogger::Log(const char* szText, va_list args) { - if (m_strFilenamePath.length() <= 0 && szText == nullptr) return; - - std::string strIndented = GetTimeDateStamp() + " " + GetIndentedString(szText) + "\n"; - - std::string logLine; - - int length = vsnprintf(nullptr, 0, strIndented.c_str(), args); - logLine.resize(length); - vsnprintf(&logLine[0], length + 1, strIndented.c_str(), args); - - Dasher::FileUtils::WriteUserDataFile(m_strFilenamePath, logLine, true); - - // Optionally we can output message to stdout - if (m_bOutputScreen) { - std::cout << logLine; - } -} - -// Logs a string to our file. eLogLevel specifies the importance of this message, we -// only write to the log file if it is at least as important as the level set in the -// constructor. Accepts printf style formatting in the first string which must be -// filled with the variable parameter list at the end. -// NOTE: Currently not thread safe! -void CFileLogger::Log(const char* szText, eLogLevel iLogLevel, ...) { - if (m_iLogLevel > iLogLevel) return; - - va_list args; - - va_start(args, iLogLevel); - Log(szText, args); - va_end(args); -} - -// Overloaded version that takes a STD::string -void CFileLogger::Log(const std::string strText, eLogLevel iLogLevel, ...) { - if (m_iLogLevel > iLogLevel) return; - - va_list args; - - va_start(args, iLogLevel); - Log(strText.c_str(), args); - va_end(args); -} - -// Version that assume log level is logDEBUG -void CFileLogger::LogDebug(const char* szText, ...) { - if (m_iLogLevel > eLogLevel::logDEBUG) return; - - va_list args; - - va_start(args, szText); - Log(szText, args); - va_end(args); -} - -// Version that assume log level is logNormal -void CFileLogger::LogNormal(const char* szText, ...) { - if (m_iLogLevel > eLogLevel::logNORMAL) return; - - va_list args; - - va_start(args, szText); - Log(szText, args); - va_end(args); -} - -// Version that assume log level is logCRITICAL -void CFileLogger::LogCritical(const char* szText, ...) { - va_list args; - - va_start(args, szText); - Log(szText, args); - va_end(args); -} - -// Logs entry into a particular function -void CFileLogger::LogFunctionEntry(const std::string& strFunctionName) { - if (m_bFunctionLogging) { - Log("start: " + strFunctionName); - m_iFunctionIndentLevel++; - } -} - -// Logs exit into a particular function -void CFileLogger::LogFunctionExit(const std::string& strFunctionName) { - if (m_bFunctionLogging) { - m_iFunctionIndentLevel--; - Log("end: " + strFunctionName); - } -} - -void CFileLogger::LogFunctionTicks(const std::string& strFunctionName, double duration) { - m_mapFunctionDuration[strFunctionName] += duration; -} - -// Gets an indented version of the function name -std::string CFileLogger::GetIndentedString(const std::string& strText) { - std::string indentation(m_iFunctionIndentLevel, ' '); - return indentation + strText; -} - -bool CFileLogger::GetFunctionTiming() { - return m_bFunctionTiming; -} - -// Update what log level this object is using -void CFileLogger::SetLogLevel(const eLogLevel iNewLevel) { - m_iLogLevel = iNewLevel; -} - -// Update whether function entry/exit is logged -void CFileLogger::SetFunctionLogging(bool bFunctionLogging) { - m_bFunctionLogging = bFunctionLogging; -} - -// Gets the time and/or date stamp as specified -// by our construction options. - -// Format is: -// Oct 19 2022 18:35:12.862 -std::string CFileLogger::GetTimeDateStamp() { - std::string strTimeStamp; - std::string format; - if (m_bDateStamp) { - format = "%b %d %Y"; - } - if (m_bTimeStamp) { - if (m_bDateStamp) format += " "; - format += "%H:%M:%S"; - } - - if (m_bTimeStamp || m_bDateStamp) { - std::chrono::time_point timepoint = std::chrono::system_clock::now(); - std::time_t now = std::chrono::system_clock::to_time_t(timepoint); - int milliseconds = static_cast( - std::chrono::time_point_cast(timepoint).time_since_epoch().count() % 1000); - std::string strMillis(3, '0'); - snprintf(&strMillis[0], strMillis.size(), "%03d", milliseconds); - - std::string Buffer(30, '\0'); // never longer than 30 chars - size_t length = - std::strftime(&Buffer[0], Buffer.size(), format.c_str(), std::localtime(&now)); // Not thread safe! - Buffer.resize(length); // Resize to strip \0 characters depending on format - if (m_bTimeStamp) Buffer += "." + strMillis; - return Buffer; - } - - return strTimeStamp; -} - -/////////////////////////////////////// CFunctionLogger ///////////////////////////////////////////////////////////// - -CFunctionLogger::CFunctionLogger(const std::string& strFunctionName, CFileLogger* pLogger) : m_pLogger(pLogger) { - if (m_pLogger == nullptr && strFunctionName.length() <= 0) return; - - m_strFunctionName = strFunctionName; - - if (!m_pLogger->GetFunctionTiming()) { - m_pLogger->LogFunctionEntry(m_strFunctionName); - } else { - m_startTime = std::chrono::steady_clock::now(); - } -} - -CFunctionLogger::~CFunctionLogger() { - if (m_pLogger == nullptr && m_strFunctionName.length() <= 0) return; - - if (!m_pLogger->GetFunctionTiming()) { - m_pLogger->LogFunctionExit(m_strFunctionName); - } else { - const auto current_time = std::chrono::steady_clock::now(); - const auto span = std::chrono::duration_cast>(current_time - m_startTime); - // Add our total ticks to the tracking map object in the logger object - m_pLogger->LogFunctionTicks(m_strFunctionName, span.count()); - } -} \ No newline at end of file diff --git a/src/DasherCore/FileLogger.h b/src/DasherCore/FileLogger.h deleted file mode 100644 index caed6ff69..000000000 --- a/src/DasherCore/FileLogger.h +++ /dev/null @@ -1,94 +0,0 @@ -// FileLogger -// -// A very simple class that does logging to a file. -// -// Copyright 2004 by Keith Vertanen -// - -#pragma once - -#include -#include -#include - -// Macros that can be used to call a globally declared logging object. These -// would need to be modified if the global variable is named differently. By -// using these macros you are protected from using the logger if it hasn't -// yet been created (it should be intialized to NULL). Also has versions that -// automatically indicate the log level without sending a parameter. -// -// Note: to use these you must use double open and close parentheses, this -// is due to the variable parameter list that logging can take to do printf -// style output. GCC supports variadic macros, but Visual Studio doesn't yet. -// - -enum class eLogLevel { logDEBUG = 0, logNORMAL = 1, logCRITICAL = 2 }; - -// Bit mask options that are used when we construct object -enum eFileLoggerOptions { - logFunctionEntryExit = 1, - logTimeStamp = 2, - logDateStamp = 4, - logDeleteOldFile = 8, - logFunctionTiming = 16, - logOutputScreen = 32 -}; -/// \ingroup Logging -/// @{ -class CFileLogger { - public: - CFileLogger(const std::string& strFilenamePath, eLogLevel level, int optionsMask); - ~CFileLogger(); - - void Log(const char* szText, eLogLevel iLogLevel = eLogLevel::logNORMAL, - ...); // Logs a string to our file if it meets or exceeds our logging level - void LogDebug(const char* szText, ...); // Logs debug level messages - void LogNormal(const char* szText, ...); // Logs normal level messages - void LogCritical(const char* szText, ...); // Logs critical level messages - - // Versions that exists so we can pass in STD strings - void Log(const std::string strText, eLogLevel iLogLevel = eLogLevel::logNORMAL, - ...); // Logs a string to our file if it meets or exceeds our logging level - - void SetFilename(const std::string& strFilename); - void SetLogLevel(const eLogLevel newLevel); - void SetFunctionLogging(bool functionLogging); - - void LogFunctionEntry(const std::string& strFunctionName); // Used by FunctionLogger to log entry to a function - void LogFunctionExit(const std::string& strFunctionName); // Used by FunctionLogger to log exit from a function - void LogFunctionTicks(const std::string& strFunctionName, - double duration); // Used by FunctionLogger to log how long was spent in a function - bool GetFunctionTiming(); - - private: - void Log(const char* szText, va_list args); - - std::string m_strFilenamePath = ""; // Filename and path of our output file - eLogLevel m_iLogLevel; // What level of logging this object should write - bool m_bFunctionLogging = false; // Whether we will log function entry/exit - bool m_bTimeStamp = false; // Whether we log the time - bool m_bDateStamp = false; // Whether we log the date - bool m_bFunctionTiming = false; // Whether our FunctionLogger objects should do performance timing - bool m_bDeleteOldFile = false; // Should we delete a previous instance of the log file - bool m_bOutputScreen = false; // Should we output to stdout as well as the file - int m_iFunctionIndentLevel = 0; // How many nested calls to FunctionLogger we have - - std::string GetIndentedString(const std::string& strText); - std::string GetTimeDateStamp(); - - std::map m_mapFunctionDuration; // Keeps track of how many ticks spent in each of our functions - // (who create a CFunctionLogger object) -}; - -// Helper class, you can create CFunctionLogger objects at -// the top of a function and it will log its entry and exit. -class CFunctionLogger { - public: - CFunctionLogger(const std::string& strFunctionName, CFileLogger* pLogger); - ~CFunctionLogger(); - - private: - std::string m_strFunctionName; // Name of the function this object is logging - CFileLogger* m_pLogger; // Pointer to the logging object to use - std::chrono::steady_clock::time_point m_startTime; // Timestamp at start of timing -}; \ No newline at end of file diff --git a/src/DasherCore/FileUtils.cpp b/src/DasherCore/FileUtils.cpp index 0d17b036a..2b0cea7ae 100644 --- a/src/DasherCore/FileUtils.cpp +++ b/src/DasherCore/FileUtils.cpp @@ -9,11 +9,16 @@ namespace Dasher { // Static member initialization std::string FileUtils::s_dataDirectory; +std::string FileUtils::s_userDataDirectory; void FileUtils::SetDataDirectory(const std::string& dataDir) { s_dataDirectory = dataDir; } +void FileUtils::SetUserDataDirectory(const std::string& userDir) { + s_userDataDirectory = userDir; +} + } // namespace Dasher static bool IsFileWriteable(const std::filesystem::path& file_path) { @@ -67,11 +72,7 @@ void Dasher::FileUtils::ScanFiles(AbstractParser* parser, const std::string& str } bool Dasher::FileUtils::WriteUserDataFile(const std::string& filename, const std::string& strNewText, bool append) { - std::filesystem::path fullPath(filename); - if (fullPath.is_relative() && !s_dataDirectory.empty()) { - fullPath = std::filesystem::path(s_dataDirectory) / filename; - } - std::ofstream File(fullPath, (append) ? std::ios_base::app : std::ios_base::out); + std::ofstream File(ResolveUserDataPath(filename), (append) ? std::ios_base::app : std::ios_base::out); if (File.is_open()) { File << strNewText; @@ -81,6 +82,22 @@ bool Dasher::FileUtils::WriteUserDataFile(const std::string& filename, const std return false; } +std::string Dasher::FileUtils::ResolveUserDataPath(const std::string& filename) { + std::filesystem::path fullPath(filename); + if (!fullPath.is_relative()) return filename; + // Prefer the user-writable data directory for mutable files + // (training deltas, dasher.log, settings). Fall back to the bundled + // data directory if the client never configured a separate user dir + // — this preserves the historical single-dir behaviour. + if (!s_userDataDirectory.empty()) { + return (std::filesystem::path(s_userDataDirectory) / filename).string(); + } + if (!s_dataDirectory.empty()) { + return (std::filesystem::path(s_dataDirectory) / filename).string(); + } + return filename; +} + std::string Dasher::FileUtils::GetFullFilenamePath(const std::string strFilename) { // We get a weak canonical path in case the path does not exist std::filesystem::path path = std::filesystem::weakly_canonical(strFilename); diff --git a/src/DasherCore/FileUtils.h b/src/DasherCore/FileUtils.h index 8766165e7..5ede1e9f2 100644 --- a/src/DasherCore/FileUtils.h +++ b/src/DasherCore/FileUtils.h @@ -8,9 +8,16 @@ namespace Dasher { // needed File utilities class FileUtils { public: - // Set the data directory for file operations + // Set the data directory for file operations (read-only bundled data: + // alphabets, training corpora, etc.). Used by ScanFiles(). static void SetDataDirectory(const std::string& dataDir); + // Set the user data directory for writable per-user files (training + // text deltas, dasher.log, settings). Must point to a writable + // location. Falls back to the data directory if never set, preserving + // historical behaviour for clients that don't distinguish the two. + static void SetUserDataDirectory(const std::string& userDir); + // Return file size on disk static int GetFileSize(const std::string& strFileName); @@ -20,11 +27,18 @@ class FileUtils { // Writes into the user file static bool WriteUserDataFile(const std::string& filename, const std::string& strNewText, bool append); + // Resolve a relative filename against the user data directory (or, if + // unset, the data directory). Returns the input unchanged if absolute. + // Used by code that needs the resolved path up front (e.g. FileLogger + // captures its path at construction time). + static std::string ResolveUserDataPath(const std::string& filename); + // Convert relative to full paths static std::string GetFullFilenamePath(const std::string strFilename); private: static std::string s_dataDirectory; + static std::string s_userDataDirectory; }; // Just a function to Log XML errors diff --git a/src/DasherCore/GameModule.cpp b/src/DasherCore/GameModule.cpp index 2801265e8..c3d3e6f7d 100644 --- a/src/DasherCore/GameModule.cpp +++ b/src/DasherCore/GameModule.cpp @@ -21,7 +21,7 @@ CGameModule::CGameModule(CSettingsStore* pSettingsStore, Dasher::CDasherInterfac : m_pInterface(pInterface), m_pModel(pModel), m_pView(nullptr), m_pSettingsStore(pSettingsStore), m_iLastSym(-1), m_y1(std::numeric_limits::min()), m_y2(std::numeric_limits::max()), m_iTargetY(CDasherModel::ORIGIN_Y), m_uHelpStart(std::numeric_limits::max()), m_ulTotalTime(0), - m_dTotalNats(0.0), m_uiTotalSyms(0), m_iFontSize(36) { + m_dTotalNats(0.0), m_uiTotalSyms(0) { HandleViewChange(pView); m_pInterface->OnEditEvent.Subscribe(this, [this](CEditEvent::EditEventType type, const std::string& strText, @@ -75,12 +75,12 @@ void CGameModule::HandleEditEvent(CEditEvent::EditEventType type, const std::str case CEditEvent::EDIT_DELETE: if (iOffset == m_iLastSym) { // seems they've just deleted the last _correct_ character they'd entered... - DASHER_ASSERT(evt->m_sText == m_pAlph->GetText(m_vTargetSymbols[m_iLastSym])); + DASHER_ASSERT(strText == m_pAlph->GetText(m_vTargetSymbols[m_iLastSym])); --m_iLastSym; } else { // just deleted previously-entered wrong text - hopefully they're heading in the right direction! - DASHER_ASSERT(m_strWrong.length() >= evt->m_sText.length()); - DASHER_ASSERT(m_strWrong.substr(m_strWrong.length() - evt->m_sText.length()) == evt->m_sText); + DASHER_ASSERT(m_strWrong.length() >= strText.length()); + DASHER_ASSERT(m_strWrong.substr(m_strWrong.length() - strText.length()) == strText); m_strWrong = m_strWrong.substr(0, m_strWrong.length() - strText.length()); } break; diff --git a/src/DasherCore/GameModule.h b/src/DasherCore/GameModule.h index 69dacb66e..435cc8673 100644 --- a/src/DasherCore/GameModule.h +++ b/src/DasherCore/GameModule.h @@ -12,7 +12,6 @@ #include "DasherTypes.h" #include "DasherInterfaceBase.h" #include "WordGeneratorBase.h" -#include "DemoFilter.h" namespace Dasher { @@ -30,7 +29,6 @@ namespace Dasher { */ class CGameModule { public: - friend class CDemoFilter; /** * Constructor * @param pEventHandler A pointer to the event handler @@ -151,16 +149,6 @@ class CGameModule { /// Time and nats at which this sentence started unsigned long m_ulSentenceStartTime; double m_dSentenceStartNats; - - /* --------------------------------------------------------------------- - * Constants - * --------------------------------------------------------------------- - */ - - /** - * The font size used to draw the target string. - */ - const int m_iFontSize; }; } // namespace Dasher diff --git a/src/DasherCore/InputFilter.h b/src/DasherCore/InputFilter.h index 30e086b3e..f51f17164 100644 --- a/src/DasherCore/InputFilter.h +++ b/src/DasherCore/InputFilter.h @@ -3,7 +3,6 @@ #include "DasherView.h" #include "DasherModel.h" #include "ModuleManager.h" -#include "UserLogBase.h" #include "DasherInput.h" namespace Dasher { diff --git a/src/DasherCore/LanguageModelling/DictLanguageModel.cpp b/src/DasherCore/LanguageModelling/DictLanguageModel.cpp index d3ac055da..e65632a1e 100644 --- a/src/DasherCore/LanguageModelling/DictLanguageModel.cpp +++ b/src/DasherCore/LanguageModelling/DictLanguageModel.cpp @@ -35,7 +35,6 @@ void CDictLanguageModel::CDictContext::dump() CDictLanguageModel::CDictnode* CDictLanguageModel::CDictnode::find_symbol(int sym) const // see if symbol is a child of node { - // printf("finding symbol %d at node %d\n",sym,node->id); CDictnode* found = child; while (found) { if (found->sbl == sym) return found; @@ -509,8 +508,6 @@ void CDictLanguageModel::EnterSymbol(Context c, int Symbol) { // Add the symbol to the current word string - // cout << max_order << std::endl; - if (max_order > 0) { char sbuffer[5]; snprintf(sbuffer, 5, "%04d", Symbol); @@ -539,17 +536,13 @@ void CDictLanguageModel::EnterSymbol(Context c, int Symbol) { context.order++; context.head = find; - // std::cout << context.order << std::endl; return; } } // m_iMaxOrder = params->GetValue( std::string( "LMMaxOrder" ) ); - // std::cout << "a" << std::endl; - // while (context.order> params->GetValue( std::string( "LMMaxOrder" ))) // { - // std::cout << "b" << std::endl; context.order--; context.head = context.head->vine; @@ -560,5 +553,4 @@ void CDictLanguageModel::EnterSymbol(Context c, int Symbol) { context.head = m_pRoot; context.order = 0; } - // std::cout << "(reduced) " << context.order << std::endl; } diff --git a/src/DasherCore/LanguageModelling/Makefile.am b/src/DasherCore/LanguageModelling/Makefile.am deleted file mode 100644 index fbc53145b..000000000 --- a/src/DasherCore/LanguageModelling/Makefile.am +++ /dev/null @@ -1,5 +0,0 @@ -noinst_LTLIBRARIES = libdasherlm.la - -noinst_LIBRARIES = libdasherlm.a -libdasherlm_a_SOURCES = PPMLanguageModel.cpp WordLanguageModel.cpp DictLanguageModel.cpp LanguageModel.cpp -AM_CXXFLAGS = -O2 diff --git a/src/DasherCore/LanguageModelling/MixtureLanguageModel.h b/src/DasherCore/LanguageModelling/MixtureLanguageModel.h index e1c447635..b9fab869f 100644 --- a/src/DasherCore/LanguageModelling/MixtureLanguageModel.h +++ b/src/DasherCore/LanguageModelling/MixtureLanguageModel.h @@ -29,8 +29,6 @@ class CMixtureLanguageModel : public CLanguageModel { CMixtureLanguageModel(CSettingsStore* pSettingsStore, const CAlphInfo* pAlph, const CAlphabetMap* pAlphMap) : CLanguageModel(pAlph->iEnd - 1), m_pSettingsStore(pSettingsStore) { - // std::cout << m_pAlphabet << std::endl; - NextContext = 0; lma = new CPPMLanguageModel(m_pSettingsStore, m_iNumSyms); @@ -92,8 +90,28 @@ class CMixtureLanguageModel : public CLanguageModel { lma->GetProbs(ContextMap.find(static_cast(context))->second->GetContextA(), ProbsA, iNormA, 0); lmb->GetProbs(ContextMap.find(static_cast(context))->second->GetContextB(), ProbsB, iNormB, 0); + // Symbol 0 is the sentinel (root/end marker) — must carry zero + // probability so that AlphabetManager's cumulative-difference + // arithmetic remains valid. Same contract as every other LM. + Probs[0] = 0; + + // Blend the two sub-models' distributions + unsigned int iActual = 0; for (int i(1); i < iNumSymbols; i++) { Probs[i] = ProbsA[i] + ProbsB[i]; + iActual += Probs[i]; + } + + // Rounding correction: the blend may lose a few units to integer + // truncation in the sub-models. Distribute the residual so that + // sum(Probs) == iNorm exactly — same pattern as PPM's Step 4. + unsigned int iToSpend = iNorm - iActual; + int iLeft = iNumSymbols - 1; + for (int i(1); i < iNumSymbols; i++) { + unsigned int p = iToSpend / iLeft; + Probs[i] += p; + --iLeft; + iToSpend -= p; } }; diff --git a/src/DasherCore/LanguageModelling/PPMLanguageModel.cpp b/src/DasherCore/LanguageModelling/PPMLanguageModel.cpp index f66a9f07b..fb8b08716 100644 --- a/src/DasherCore/LanguageModelling/PPMLanguageModel.cpp +++ b/src/DasherCore/LanguageModelling/PPMLanguageModel.cpp @@ -33,81 +33,154 @@ bool CAbstractPPM::isValidContext(const Context context) const { void CPPMLanguageModel::GetProbs(Context context, std::vector& probs, int norm, int iUniform) const { const CPPMContext* ppmcontext = reinterpret_cast(context); - DASHER_ASSERT(isValidContext(context)); - int iNumSymbols = GetSize(); + int alpha = m_pSettingsStore->GetLongParameter(LP_LM_ALPHA); + int beta = m_pSettingsStore->GetLongParameter(LP_LM_BETA); + + mergePPMProbs(ppmcontext, probs, GetSize(), norm, iUniform, alpha, beta); +} + +///////////////////////////////////////////////////////////////////// +// CAbstractPPM — shared PPM probability-merge loop +// +// This is the heart of the Dasher predictive engine. Given a context +// (a chain of PPM trie nodes via vine pointers), it produces a probability +// distribution over all possible next symbols. +// +// The algorithm blends evidence from multiple context lengths: +// +// 1. UNIFORM BACKOFF: Start by distributing iUniform units of +// probability mass evenly across all symbols (except symbol 0, +// which is the sentinel/root and always gets probability 0). +// +// 2. VINE-CHAIN TRAVERSAL: Walk from the deepest context node back +// toward the root via vine pointers. At each level, children with +// observed counts receive additional probability mass proportional +// to their count, discounted by alpha (a smoothing constant) and +// beta (a count threshold): +// +// p = remaining_mass * (100 * count - beta) / (100 * total + alpha) +// +// Symbols seen at shorter contexts are "excluded" from longer +// contexts (when doExclusion is enabled — currently disabled, +// see FIXME below). The remaining_mass shrinks at each level. +// +// 3. LEFTOVER: Any probability mass still unspent after the vine +// traversal is distributed evenly among symbols that were never +// seen in any context. +// +// 4. ROUNDING CORRECTION: Integer arithmetic loses a few units to +// truncation. The final loop distributes the residual so that +// sum(probs) == norm exactly. +// +// The result is a cumulative probability vector where: +// probs[0] == 0 (sentinel — required by AlphabetManager) +// sum(probs) == norm (typically 65536 = 2^16) +// +// Subclasses customize child iteration via collectChildCounts(): +// - CPPMLanguageModel: iterates CPPMnode inline hash (standard PPM) +// - CPPMPYLanguageModel: iterates pychild map (Mandarin pinyin routing) + +void CAbstractPPM::collectChildCounts(const CPPMnode* node, std::vector& out) const { + // Default: iterate children via the CPPMnode inline hash table. + // The inline hash is an open-addressed array built into each node, + // sized proportionally to the number of children. See CPPMnode + // documentation in PPMLanguageModel.h for the storage strategy. + for (ChildIterator pSymbol = node->children(); pSymbol != node->end(); pSymbol++) { + out.push_back({(*pSymbol)->sym, (*pSymbol)->count}); + } +} +void CAbstractPPM::mergePPMProbs(const CPPMContext* ppmcontext, std::vector& probs, int iNumSymbols, + int norm, int iUniform, int alpha, int beta) const { probs.resize(iNumSymbols); + // exclusions[sym] = true once sym has received probability from + // a context level. When doExclusion is enabled, excluded symbols + // are skipped at deeper context levels to avoid double-counting. + // (doExclusion is currently disabled — see FIXME below.) std::vector exclusions(iNumSymbols); - unsigned int iToSpend = norm; + unsigned int iToSpend = norm; // remaining probability mass to distribute unsigned int iUniformLeft = iUniform; - // TODO: Sort out zero symbol case + // Symbol 0 is the sentinel (root/end marker). It must carry zero + // probability so that cumulative-difference arithmetic in + // AlphabetManager::IterateChildGroups remains valid. probs[0] = 0; exclusions[0] = false; + // --- Step 1: Uniform backoff distribution --- + // Distribute iUniform units evenly across symbols 1..N-1. + // This ensures every symbol gets at least some baseline probability, + // even if it was never observed in any context. for (int i = 1; i < iNumSymbols; i++) { probs[i] = iUniformLeft / (iNumSymbols - i); iUniformLeft -= probs[i]; iToSpend -= probs[i]; exclusions[i] = false; } - DASHER_ASSERT(iUniformLeft == 0); - // bool doExclusion = GetLongParameter( LP_LM_ALPHA ); - bool doExclusion = 0; // FIXME - - int alpha = m_pSettingsStore->GetLongParameter(LP_LM_ALPHA); - int beta = m_pSettingsStore->GetLongParameter(LP_LM_BETA); - + // FIXME: doExclusion is always 0. The exclusion mechanism was + // intended to prevent symbols seen at shorter contexts from + // receiving probability at longer contexts (a standard PPM + // technique). It's disabled because enabling it requires careful + // tuning to avoid degrading prediction quality. The infrastructure + // is preserved here so it can be enabled experimentally. + bool doExclusion = 0; + + // Reusable buffer for child counts (avoids per-level allocation) + std::vector childCounts; + + // --- Step 2: Vine-chain traversal --- + // Walk from the deepest context node toward the root. At each + // level, observed children receive additional probability mass + // proportional to their count, blended with alpha/beta smoothing. + // + // The vine pointer links each node to its parent in the suffix + // chain: if the context is "the cat sat", the vine chain visits + // nodes for "sat" → "cat sat" → "the cat sat" → root, progressively + // using shorter contexts as longer ones are exhausted. for (CPPMnode* pTemp = ppmcontext->head; pTemp; pTemp = pTemp->vine) { - int iTotal = 0; + childCounts.clear(); + collectChildCounts(pTemp, childCounts); - for (ChildIterator pSymbol = pTemp->children(); pSymbol != pTemp->end(); pSymbol++) { - symbol sym = (*pSymbol)->sym; - if (!(exclusions[sym] && doExclusion)) iTotal += (*pSymbol)->count; + // Sum counts of non-excluded children at this context level + int iTotal = 0; + for (const auto& sc : childCounts) { + if (!(exclusions[sc.sym] && doExclusion)) iTotal += sc.count; } if (iTotal) { + // size_of_slice = all remaining mass. Each child gets a + // fraction proportional to its count, discounted by alpha + // (smoothing) and beta (count threshold). unsigned int size_of_slice = iToSpend; - for (ChildIterator pSymbol = pTemp->children(); pSymbol != pTemp->end(); pSymbol++) { - if (!(exclusions[(*pSymbol)->sym] && doExclusion)) { - exclusions[(*pSymbol)->sym] = 1; + for (const auto& sc : childCounts) { + if (!(exclusions[sc.sym] && doExclusion)) { + exclusions[sc.sym] = 1; unsigned int p = - static_cast(size_of_slice) * (100 * (*pSymbol)->count - beta) / (100 * iTotal + alpha); + static_cast(size_of_slice) * (100 * sc.count - beta) / (100 * iTotal + alpha); - probs[(*pSymbol)->sym] += p; + probs[sc.sym] += p; iToSpend -= p; } - // Usprintf(debug,TEXT("sym %u counts %d p %u tospend %u - // \n"),sym,s->count,p,tospend); DebugOutput(debug); } } } + // --- Step 3: Leftover distribution --- + // Any mass not consumed by the vine traversal is distributed + // evenly among symbols that were never observed (not excluded). unsigned int size_of_slice = iToSpend; int symbolsleft = 0; for (int i = 1; i < iNumSymbols; i++) if (!(exclusions[i] && doExclusion)) symbolsleft++; - // std::ostringstream str; - // for (sym=0;sym& pro } } + // --- Step 4: Rounding correction --- + // Integer division loses a few units to truncation. Distribute + // the residual sequentially so that sum(probs) == norm exactly. int iLeft = iNumSymbols - 1; for (int i = 1; i < iNumSymbols; i++) { @@ -147,7 +223,6 @@ void CAbstractPPM::EnterSymbol(Context c, int Symbol) { // Usprintf(debug,TEXT("found context %x order %d\n"),head,order); // DebugOutput(debug); - // std::cout << context.order << std::endl; return; } } @@ -162,8 +237,6 @@ void CAbstractPPM::EnterSymbol(Context c, int Symbol) { context.head = m_pRoot; context.order = 0; } - - // std::cout << context.order << std::endl; } ///////////////////////////////////////////////////////////////////// @@ -325,7 +398,6 @@ CAbstractPPM::CPPMnode* CAbstractPPM::CPPMnode::find_symbol(symbol s) const if (m_ppChildren[i]->sym == s) return m_ppChildren[i]; return 0; } - // printf("finding symbol %d at node %d\n",sym,node->id); for (int i = s;; i++) { // search through elements which have overflowed into subsequent slots CPPMnode* found = this->m_ppChildren[i % m_iNumChildSlots]; // wrap round @@ -404,8 +476,6 @@ CAbstractPPM::CPPMnode* CAbstractPPM::AddSymbolToNode(CPPMnode* pNode, symbol sy CPPMnode* pReturn = pNode->find_symbol(sym); - // std::cout << sym << ","; - if (pReturn != NULL) { pReturn->count++; if (!bUpdateExclusion) { diff --git a/src/DasherCore/LanguageModelling/PPMLanguageModel.h b/src/DasherCore/LanguageModelling/PPMLanguageModel.h index e7b1bb46c..b9cc5cf60 100644 --- a/src/DasherCore/LanguageModelling/PPMLanguageModel.h +++ b/src/DasherCore/LanguageModelling/PPMLanguageModel.h @@ -143,6 +143,52 @@ class CAbstractPPM : public CLanguageModel, private NoClones { void dump(); bool isValidContext(const Context c) const; + protected: + /// A (symbol, count) pair representing one child of a PPM trie node. + /// Used by collectChildCounts() to abstract away the different child + /// storage mechanisms (inline hash vs pychild map). + struct SymbolCount { + symbol sym; + unsigned short count; + }; + + /// Collect children of a PPM node as (symbol, count) pairs. + /// + /// Base implementation iterates the CPPMnode inline hash table + /// (the fast open-addressed array built into each node). This is + /// used by the standard PPM and RoutingPPM language models. + /// + /// CPPMPYLanguageModel overrides this to iterate its pychild + /// std::map instead, which stores pinyin-specific counts separately + /// from the Chinese symbol trie. + /// + /// \param node the PPM trie node whose children to collect + /// \param out vector to fill with (symbol, count) pairs; cleared first + virtual void collectChildCounts(const CPPMnode* node, std::vector& out) const; + + /// The shared PPM probability-merge loop — the heart of Dasher's + /// predictive engine. + /// + /// Produces a cumulative probability vector by blending evidence from + /// multiple context lengths (via vine-pointer backoff) with a uniform + /// backoff distribution. See the implementation in PPMLanguageModel.cpp + /// for a detailed description of the four-phase algorithm. + /// + /// Subclasses customize child iteration via collectChildCounts(). + /// The caller provides the alpha/beta smoothing parameters (typically + /// read from LP_LM_ALPHA and LP_LM_BETA settings). + /// + /// \param ppmcontext the current PPM context (vine-chain head) + /// \param probs output vector; resized to iNumSymbols, filled with + /// cumulative probabilities summing to norm + /// \param iNumSymbols number of symbols (including sentinel 0) + /// \param norm total probability mass (typically 65536) + /// \param iUniform mass for the uniform backoff distribution + /// \param alpha smoothing constant from LP_LM_ALPHA + /// \param beta count threshold from LP_LM_BETA + void mergePPMProbs(const CPPMContext* ppmcontext, std::vector& probs, int iNumSymbols, int norm, + int iUniform, int alpha, int beta) const; + private: CPPMnode* AddSymbolToNode(CPPMnode* pNode, symbol sym); diff --git a/src/DasherCore/LanguageModelling/PPMPYLanguageModel.cpp b/src/DasherCore/LanguageModelling/PPMPYLanguageModel.cpp index 94e4334c4..1ebf8418b 100644 --- a/src/DasherCore/LanguageModelling/PPMPYLanguageModel.cpp +++ b/src/DasherCore/LanguageModelling/PPMPYLanguageModel.cpp @@ -37,7 +37,6 @@ CPPMPYLanguageModel::CPPMPYLanguageModel(CSettingsStore* pSettingsStore, int iNu // DASHER_ASSERT(m_setContexts.count(ppmcontext) > 0); - // std::cout<<"size of context set "<child[i]; - //std::cout<<"Symbols "<symbol<symbol; @@ -161,15 +158,11 @@ void CPPMPYLanguageModel::GetPartProbs(Context context, std::vector(context); // DASHER_ASSERT(m_setContexts.count(ppmcontext) > 0); - // std::cout<<"size of context set "<>::iterator it = vChildren.begin(); it != vChildren.end(); it++) { DASHER_ASSERT(it->first > 0 && it->first < GetSize()); // i.e., is valid CH symbol it->second = static_cast(iUniformLeft / (vChildren.size() - i)); - // std::cout<<"iUniformLeft: "<second; iToSpend -= it->second; i++; @@ -210,7 +202,6 @@ void CPPMPYLanguageModel::GetPartProbs(Context context, std::vectorsecond += p; iToSpend -= p; } - // std::cout<<"after lan mod fourth loop"<(vChildren.size()) - 1; - // std::cout<<"iNumsyjbols "<>::iterator it = vChildren.begin() + 1; it != vChildren.end(); it++) { - // std::cout<<"iLeft "<second += pRem; --iLeft; iToSpend -= pRem; } - // std::cout<<"after lan mod fifth loop"<& probs, int norm, int iUniform) const { - const CPPMContext* ppmcontext = reinterpret_cast(context); - - // std::cout<<"PPMCONTEXT symbol: "<head->symbol<child; - - while(pNode){ - std::cout<<"Next Symbol: "<symbol<<" "; - pNode = pNode->next; - } - std::cout<<" "< 0); - - int iNumSymbols = m_iNumPYsyms + 1; - - probs.resize(iNumSymbols); - - std::vector exclusions(iNumSymbols); - - unsigned int iToSpend = norm; - unsigned int iUniformLeft = iUniform; - - // TODO: Sort out zero symbol case - probs[0] = 0; - exclusions[0] = false; - - int i; - for (i = 1; i < iNumSymbols; i++) { - probs[i] = iUniformLeft / (iNumSymbols - i); - iUniformLeft -= probs[i]; - iToSpend -= probs[i]; - exclusions[i] = false; +void CPPMPYLanguageModel::collectChildCounts(const CPPMnode* node, std::vector& out) const { + // PPMPY stores children in a pychild map on CPPMPYnode, not in the + // CPPMnode inline hash. Cast to access it. + const CPPMPYnode* pyNode = dynamic_cast(node); + if (pyNode) { + for (const auto& [sym, count] : pyNode->pychild) { + out.push_back({sym, count}); + } } +} - DASHER_ASSERT(iUniformLeft == 0); - - // bool doExclusion = GetLongParameter( LP_LM_ALPHA ); - bool doExclusion = 0; // FIXME +void CPPMPYLanguageModel::GetProbs(Context context, std::vector& probs, int norm, int iUniform) const { + const CPPMContext* ppmcontext = reinterpret_cast(context); int alpha = m_pSettingsStore->GetLongParameter(LP_LM_ALPHA); int beta = m_pSettingsStore->GetLongParameter(LP_LM_BETA); - for (CPPMnode* pTemp = ppmcontext->head; pTemp; pTemp = pTemp->vine) { - int iTotal = 0; - const std::map& pychild(dynamic_cast(pTemp)->pychild); - - for (std::map::const_iterator it = pychild.begin(); it != pychild.end(); it++) { - if (!(exclusions[it->first] && doExclusion)) iTotal += it->second; - } - - if (iTotal) { - unsigned int size_of_slice = iToSpend; - - for (std::map::const_iterator it = pychild.begin(); it != pychild.end(); it++) { - if (!(exclusions[it->first] && doExclusion)) { - exclusions[it->first] = 1; - - unsigned int p = - static_cast(size_of_slice) * (100 * it->second - beta) / (100 * iTotal + alpha); - - probs[it->first] += p; - iToSpend -= p; - } - // Usprintf(debug,TEXT("sym %u counts %d p %u tospend %u - // \n"),sym,s->count,p,tospend); DebugOutput( debug); - } - } - } - - unsigned int size_of_slice = iToSpend; - int symbolsleft = 0; - - for (i = 1; i < iNumSymbols; i++) - if (!(exclusions[i] && doExclusion)) symbolsleft++; - - // std::ostringstream str; - // for (sym=0;sym 0 && pysym <= m_iNumPYsyms); CPPMPYLanguageModel::CPPMContext& context = *reinterpret_cast(c); - // std::cout<<"py learn context : "<symbol<child; while(pNode){ diff --git a/src/DasherCore/LanguageModelling/PPMPYLanguageModel.h b/src/DasherCore/LanguageModelling/PPMPYLanguageModel.h index bdce97361..ea57c0061 100644 --- a/src/DasherCore/LanguageModelling/PPMPYLanguageModel.h +++ b/src/DasherCore/LanguageModelling/PPMPYLanguageModel.h @@ -74,6 +74,9 @@ class CPPMPYLanguageModel : public CAbstractPPM { }; CPPMPYnode* makeNode(int sym); + /// Override to iterate the pychild map instead of CPPMnode children. + void collectChildCounts(const CPPMnode* node, std::vector& out) const override; + private: int NodesAllocated; mutable CSimplePooledAlloc m_NodeAlloc; diff --git a/src/DasherCore/LanguageModelling/WordLanguageModel.cpp b/src/DasherCore/LanguageModelling/WordLanguageModel.cpp index 5323339a4..26aa92359 100644 --- a/src/DasherCore/LanguageModelling/WordLanguageModel.cpp +++ b/src/DasherCore/LanguageModelling/WordLanguageModel.cpp @@ -71,8 +71,6 @@ CWordLanguageModel::CWordnode* CWordLanguageModel::AddSymbolToNode(CWordnode* pN if (pReturn != NULL) { if (*update) { - // std::cout << "USHRT_MAX: " << USHRT_MAX << " " << bLearn << std::endl; - // if( (pReturn->count < USHRT_MAX) && bLearn ) // Truncate counts at storage limit if (bLearn) // Truncate counts at storage limit pReturn->count++; @@ -92,8 +90,6 @@ CWordLanguageModel::CWordnode* CWordLanguageModel::AddSymbolToNode(CWordnode* pN // okay for now } - // std::cout << pReturn->count << std::endl; - ++NodesAllocated; return pReturn; @@ -283,6 +279,15 @@ void CWordLanguageModel::GetProbs(Context context, std::vector& pr iToSpend -= p; } + // Symbol 0 is a special dummy/sentinel that must carry zero probability + // (AlphabetManager::IterateChildGroups asserts (*pCProb)[0] == 0 so the + // cumulative-difference arithmetic cp[i]-cp[j] stays valid for all i,j). + // Every other LM (PPM, PPMPY, RoutingPPM, CTW, Dict) enforces this + // explicitly; Word LM was missing it, which only happened to go unnoticed + // on platforms where the spelling model assigned ~0 probability to + // symbol 0. macOS libc++ showed the real bug. + probs[0] = 0; + DASHER_ASSERT(iToSpend == 0); } @@ -340,8 +345,6 @@ void CWordLanguageModel::CollapseContext(CWordLanguageModel::CWordContext& conte while ((pCurrent != NULL) && !bUpdateExclusion) { - // std::cout << "Incrementing" << std::endl; - ++(pCurrent->count); int i(0); @@ -355,12 +358,8 @@ void CWordLanguageModel::CollapseContext(CWordLanguageModel::CWordContext& conte for (std::vector::iterator it(oSymbols.begin()); it != oSymbols.end(); ++it) { int iSymbol(*it); - // std::cout << "Symbol " << iSymbol << std::endl; - CWordnode* pTmpChild(pTmp->find_symbol(iSymbol)); - // std::cout << "pTmpChild: " << pTmpChild << std::endl; - if (pTmpChild == NULL) { // We don't already have this child, so add a new node @@ -384,8 +383,6 @@ void CWordLanguageModel::CollapseContext(CWordLanguageModel::CWordContext& conte } pCurrent = pCurrent->vine; - - // std::cout << "foo: " << pCurrent << " " << bUpdateExclusion << std::endl; } // Now we need to go through and fix up the vine pointers @@ -424,8 +421,6 @@ void CWordLanguageModel::CollapseContext(CWordLanguageModel::CWordContext& conte CWordnode* pTmpChild; CWordnode* pTmpVine(NULL); - // std::cout << "pTmp is " << pTmp << std::endl; - int iUpdateExclusion(1); { @@ -433,8 +428,6 @@ void CWordLanguageModel::CollapseContext(CWordLanguageModel::CWordContext& conte pTmpChild = AddSymbolToNode(pTmp, iNewSymbol, &iUpdateExclusion, false); // FIXME - might have added a new node here, so fix up vine pointers. - // std::cout << "New node: " << pTmpChild << std::endl; - context.word_head = pTmpChild; ++context.word_order; pTmpVine = pTmpChild; @@ -443,13 +436,9 @@ void CWordLanguageModel::CollapseContext(CWordLanguageModel::CWordContext& conte while (pTmp != NULL) { - // std::cout << "pTmp is " << pTmp << std::endl; - pTmpChild = AddSymbolToNode(pTmp, iNewSymbol, &iUpdateExclusion, false); // FIXME - might have added a new node here, so fix up vine pointers. - // std::cout << "New node: " << pTmpChild << std::endl; - if (pTmpVine) pTmpVine->vine = pTmpChild; pTmpVine = pTmpChild; @@ -460,11 +449,8 @@ void CWordLanguageModel::CollapseContext(CWordLanguageModel::CWordContext& conte // Finally get rid of the letter part of the context - // std::cout << "Changed head to " << context.word_head << std::endl; - while (context.word_order > 2) { context.word_head = context.word_head->vine; - // std::cout << " * Followed vine to head to " << context.word_head << std::endl; --(context.word_order); } @@ -520,8 +506,6 @@ void CWordLanguageModel::AddSymbol(CWordLanguageModel::CWordContext& context, sy int foo2(1); - // std::cout << "aa: " << pTmp << " " << m_pRoot << std::endl; - pTmpVine = AddSymbolToNode(pTmp, sym, &foo2, false); // Last parameter is whether to learn or not context.head = pTmpVine; diff --git a/src/DasherCore/MandarinAlphMgr.cpp b/src/DasherCore/MandarinAlphMgr.cpp index 74a52c8cc..7e605b25d 100644 --- a/src/DasherCore/MandarinAlphMgr.cpp +++ b/src/DasherCore/MandarinAlphMgr.cpp @@ -146,8 +146,6 @@ CMandarinAlphMgr::~CMandarinAlphMgr() { } void CMandarinAlphMgr::CreateLanguageModel() { - // std::cout<<"CHALphabet size "<< pCHAlphabet->GetNumberTextSymbols(); [7603] - // std::cout<<"Setting PPMPY model"<(m_vGroupsByConversion.size()) - 1, static_cast(m_vConversionsByGroup.size()) - 1); } @@ -329,7 +327,6 @@ void CMandarinAlphMgr::CConvRoot::PopulateChildrenWithExisting(CMandSym* existin // Finally loop through and create the children for (std::vector>::const_iterator it = m_vChInfo.begin(); it != m_vChInfo.end(); it++) { - // std::cout << "Current scec: " << pCurrentSCEChild << std::endl; const unsigned int iLbnd(iCum), iHbnd(iCum + it->second); iCum = iHbnd; @@ -416,8 +413,6 @@ void CMandarinAlphMgr::GetConversions(std::vector(m_pLanguageModel) ->GetPartProbs(context, vChildren, static_cast(iNorm), uniform); - // std::cout<<"after get probs "<>::const_iterator it = vChildren.begin(); it != vChildren.end(); it++) { diff --git a/src/DasherCore/ModuleManager.cpp b/src/DasherCore/ModuleManager.cpp index 9022a0fd4..68446d8f9 100644 --- a/src/DasherCore/ModuleManager.cpp +++ b/src/DasherCore/ModuleManager.cpp @@ -2,21 +2,8 @@ // // Copyright (c) 2008 The Dasher Team // -// This file is part of Dasher. -// -// Dasher is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// Dasher is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Dasher; if not, write to the Free Software -// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +// All logic lives in the ModuleMap template in the header. +// This file provides only the destructor and thin forwarding methods. #include "ModuleManager.h" @@ -27,92 +14,54 @@ using namespace Dasher; CModuleManager::~CModuleManager() {} -void CModuleManager::RegisterInputDeviceModule(CDasherInput* pModule, bool makeDefault) { - // Erase possibly known modules with same name - m_InputDeviceModules.erase(pModule->GetName()); - m_ManagedInputDeviceModules.erase(pModule->GetName()); - - if (makeDefault) m_sDefaultInputDevice = pModule->GetName(); - m_InputDeviceModules.emplace(pModule->GetName(), pModule); -} - -void CModuleManager::RegisterInputMethodModule(CInputFilter* pModule, bool makeDefault) { - // Erase possibly known modules with same name - m_InputMethodModules.erase(pModule->GetName()); - m_ManagedInputMethodModules.erase(pModule->GetName()); +// ── Input device forwarding ────────────────────────────────────────────── - if (makeDefault) m_sDefaultInputMethod = pModule->GetName(); - m_InputMethodModules.emplace(pModule->GetName(), pModule); +void CModuleManager::RegisterInputDeviceModule(CDasherInput* pModule, bool makeDefault) { + m_InputDevices.Register(pModule, makeDefault); } void CModuleManager::RegisterInputDeviceModule(std::unique_ptr pModule, bool makeDefault) { - // Erase possibly known modules with same name - m_InputDeviceModules.erase(pModule->GetName()); - m_ManagedInputDeviceModules.erase(pModule->GetName()); - - if (makeDefault) m_sDefaultInputDevice = pModule->GetName(); - m_ManagedInputDeviceModules.emplace(pModule->GetName(), std::move(pModule)); + m_InputDevices.Register(std::move(pModule), makeDefault); } -void CModuleManager::RegisterInputMethodModule(std::unique_ptr pModule, bool makeDefault) { - // Erase possibly known modules with same name - m_InputMethodModules.erase(pModule->GetName()); - m_ManagedInputMethodModules.erase(pModule->GetName()); - - if (makeDefault) m_sDefaultInputMethod = pModule->GetName(); - m_ManagedInputMethodModules.emplace(pModule->GetName(), std::move(pModule)); +CDasherInput* CModuleManager::GetDefaultInputDevice() { + return m_InputDevices.GetDefault(); } -void CModuleManager::ListInputDeviceModules(std::vector& vList) { - for (auto& [key, _] : m_InputDeviceModules) { - vList.push_back(key); - } - for (auto& [key, _] : m_ManagedInputDeviceModules) { - vList.push_back(key); - } +void CModuleManager::SetDefaultInputDevice(CDasherInput* p) { + m_InputDevices.SetDefault(p); } -void CModuleManager::ListInputMethodModules(std::vector& vList) { - for (auto& [key, _] : m_InputMethodModules) { - vList.push_back(key); - } - for (auto& [key, _] : m_ManagedInputMethodModules) { - vList.push_back(key); - } +void CModuleManager::ListInputDeviceModules(std::vector& vList) { + m_InputDevices.List(vList); } CDasherInput* CModuleManager::GetInputDeviceByName(const std::string strName) { - if (m_InputDeviceModules.find(strName) != m_InputDeviceModules.end()) { - return m_InputDeviceModules[strName]; - } - if (m_ManagedInputDeviceModules.find(strName) != m_ManagedInputDeviceModules.end()) { - return m_ManagedInputDeviceModules[strName].get(); - } - return nullptr; + return m_InputDevices.GetByName(strName); } -CInputFilter* CModuleManager::GetInputMethodByName(const std::string strName) { - if (m_InputMethodModules.find(strName) != m_InputMethodModules.end()) { - return m_InputMethodModules[strName]; - } - if (m_ManagedInputMethodModules.find(strName) != m_ManagedInputMethodModules.end()) { - return m_ManagedInputMethodModules[strName].get(); - } - return nullptr; +// ── Input method forwarding ────────────────────────────────────────────── + +void CModuleManager::RegisterInputMethodModule(CInputFilter* pModule, bool makeDefault) { + m_InputMethods.Register(pModule, makeDefault); } -CDasherInput* CModuleManager::GetDefaultInputDevice() { - return GetInputDeviceByName(m_sDefaultInputDevice); +void CModuleManager::RegisterInputMethodModule(std::unique_ptr pModule, bool makeDefault) { + m_InputMethods.Register(std::move(pModule), makeDefault); } CInputFilter* CModuleManager::GetDefaultInputMethod() { - return GetInputMethodByName(m_sDefaultInputMethod); + return m_InputMethods.GetDefault(); } -void CModuleManager::SetDefaultInputDevice(CDasherInput* p) { - m_sDefaultInputDevice = p->GetName(); +void CModuleManager::SetDefaultInputMethod(CInputFilter* p) { + m_InputMethods.SetDefault(p); } -void CModuleManager::SetDefaultInputMethod(CInputFilter* p) { - m_sDefaultInputMethod = p->GetName(); -} \ No newline at end of file +void CModuleManager::ListInputMethodModules(std::vector& vList) { + m_InputMethods.List(vList); +} + +CInputFilter* CModuleManager::GetInputMethodByName(const std::string strName) { + return m_InputMethods.GetByName(strName); +} diff --git a/src/DasherCore/ModuleManager.h b/src/DasherCore/ModuleManager.h index 1b9f09f98..8eab3bb05 100644 --- a/src/DasherCore/ModuleManager.h +++ b/src/DasherCore/ModuleManager.h @@ -1,3 +1,12 @@ +// ModuleManager.h +// +// Copyright (c) 2008 The Dasher Team +// +// Manages registration, lookup, and default selection for engine modules +// (input devices and input method filters). Uses a generic ModuleMap +// helper to avoid duplicating the identical bookkeeping logic for each +// module type. + #pragma once #include @@ -9,35 +18,86 @@ namespace Dasher { class CDasherInput; class CInputFilter; +/// Generic module registry for a single module type (e.g. CDasherInput, +/// CInputFilter). Handles both externally-owned modules (raw pointer) and +/// self-owned modules (unique_ptr). Each module is keyed by its name +/// (from T::GetName()). At most one default module can be active at a time. +template +class ModuleMap { + public: + /// Register an externally-owned module. The caller retains ownership. + void Register(T* pModule, bool makeDefault = false) { + const std::string name = pModule->GetName(); + m_External.erase(name); + m_Managed.erase(name); + if (makeDefault) m_DefaultName = name; + m_External.emplace(name, pModule); + } + + /// Register a self-owned module. Ownership transfers to this ModuleMap. + void Register(std::unique_ptr pModule, bool makeDefault = false) { + const std::string name = pModule->GetName(); + m_External.erase(name); + m_Managed.erase(name); + if (makeDefault) m_DefaultName = name; + m_Managed.emplace(name, std::move(pModule)); + } + + /// Collect all registered module names into the output vector. + void List(std::vector& vList) const { + for (const auto& [key, _] : m_External) + vList.push_back(key); + for (const auto& [key, _] : m_Managed) + vList.push_back(key); + } + + /// Look up a module by name. Returns nullptr if not found. + /// Searches both externally-owned and self-owned registrations. + T* GetByName(const std::string& name) const { + auto extIt = m_External.find(name); + if (extIt != m_External.end()) return extIt->second; + auto mgdIt = m_Managed.find(name); + if (mgdIt != m_Managed.end()) return mgdIt->second.get(); + return nullptr; + } + + /// Return the default module, or nullptr if none is set. + T* GetDefault() const { return GetByName(m_DefaultName); } + + /// Set the default module by pointer. + void SetDefault(T* p) { m_DefaultName = p->GetName(); } + + private: + std::unordered_map m_External; ///< externally-owned modules + std::unordered_map> m_Managed; ///< self-owned modules + std::string m_DefaultName; ///< name of the default module (empty = none) +}; + +/// Manages input device and input method filter registrations. +/// Thin wrapper around two ModuleMap instances — all logic is in the template. class CModuleManager { public: ~CModuleManager(); - // Externally Memory Managed Modules + // ── Input device modules (CDasherInput) ────────────────────────────── void RegisterInputDeviceModule(CDasherInput* pModule, bool makeDefault = false); - void RegisterInputMethodModule(CInputFilter* pModule, bool makeDefault = false); - // Memory Managed Modules void RegisterInputDeviceModule(std::unique_ptr pModule, bool makeDefault = false); - void RegisterInputMethodModule(std::unique_ptr pModule, bool makeDefault = false); - CDasherInput* GetDefaultInputDevice(); void SetDefaultInputDevice(CDasherInput*); void ListInputDeviceModules(std::vector& vList); CDasherInput* GetInputDeviceByName(const std::string strName); + // ── Input method modules (CInputFilter) ────────────────────────────── + void RegisterInputMethodModule(CInputFilter* pModule, bool makeDefault = false); + void RegisterInputMethodModule(std::unique_ptr pModule, bool makeDefault = false); CInputFilter* GetDefaultInputMethod(); void SetDefaultInputMethod(CInputFilter*); void ListInputMethodModules(std::vector& vList); CInputFilter* GetInputMethodByName(const std::string strName); private: - std::unordered_map m_InputDeviceModules; - std::unordered_map m_InputMethodModules; - std::unordered_map> m_ManagedInputDeviceModules; - std::unordered_map> m_ManagedInputMethodModules; - - std::string m_sDefaultInputDevice; - std::string m_sDefaultInputMethod; + ModuleMap m_InputDevices; + ModuleMap m_InputMethods; }; } // namespace Dasher diff --git a/src/DasherCore/OneButtonDynamicFilter.cpp b/src/DasherCore/OneButtonDynamicFilter.cpp index 9bd1aff8f..183f61b54 100644 --- a/src/DasherCore/OneButtonDynamicFilter.cpp +++ b/src/DasherCore/OneButtonDynamicFilter.cpp @@ -125,11 +125,9 @@ void COneButtonDynamicFilter::ActionButton(unsigned long iTime, Keys::VirtualKey } if ((Key == Keys::Button_2) || (Key == Keys::Button_3) || (Key == Keys::Button_4)) { - if (CUserLogBase* pUserLog = m_pInterface->GetUserLogPtr()) pUserLog->KeyDown(Key, iType, 5); m_iTarget = 1 - m_iTarget; m_bDecorationChanged = true; } else { - if (CUserLogBase* pUserLog = m_pInterface->GetUserLogPtr()) pUserLog->KeyDown(Key, iType, 0); } } diff --git a/src/DasherCore/Parameters.cpp b/src/DasherCore/Parameters.cpp index 0570bb4ce..98702ce86 100644 --- a/src/DasherCore/Parameters.cpp +++ b/src/DasherCore/Parameters.cpp @@ -1,5 +1,5 @@ // ============================================================================= -// AUTOGENERATED FILE — DO NOT EDIT DIRECTLY +// AUTOGENERATED FILE — DO NOT EDIT DIRECTLY // Generated by: python3 Scripts/generate_parameters.py // Source: settings_manifest.json // ============================================================================= @@ -223,11 +223,10 @@ const std::unordered_map parameter_defaults = "LP_START_MODE", "Control", "Input"}}, - {LP_UNIFORM, - Parameter_Value{"UniformTimes1000", PARAM_LONG, Persistence::PERSISTENT, 50l, - "Uniform probability weight (×1000). Higher values make less-probable symbols larger.", - "Uniform Probability", Settings::UIControlType::Slider, 0, 1000, 1, 10, true, "LP_UNIFORM", - "Advanced", "Input"}}, + {LP_UNIFORM, Parameter_Value{"UniformTimes1000", PARAM_LONG, Persistence::PERSISTENT, 50l, + "Uniform probability weight (×1000). Higher values make less-probable symbols larger.", + "Uniform Probability", Settings::UIControlType::Slider, 0, 1000, 1, 10, true, + "LP_UNIFORM", "Advanced", "Input"}}, {LP_MOUSEPOSDIST, Parameter_Value{"MousePositionBoxDistance", PARAM_LONG, Persistence::PERSISTENT, 50l, "MousePositionBoxDistance.", "Mouse Position Distance", Settings::UIControlType::Step, 0, 500, 1, 10, true, "LP_MOUSEPOSDIST", @@ -284,19 +283,6 @@ const std::unordered_map parameter_defaults = Parameter_Value{"WordAlpha", PARAM_LONG, Persistence::PERSISTENT, 50l, "Alpha value for word-based model.", "Word Alpha", Settings::UIControlType::Slider, 0, 100, 1, 1, true, "LP_LM_WORD_ALPHA", "Advanced", "Language"}}, - {LP_USER_LOG_LEVEL_MASK, - Parameter_Value{"UserLogLevelMask", - PARAM_LONG, - Persistence::PERSISTENT, - 0l, - "Controls level of user logging, 0 = none, 1 = short, 2 = detailed, 3 = both.", - "User Log Level", - Settings::UIControlType::Enum, - {{"None", 0}, {"Short", 1}, {"Detailed", 2}, {"Both", 3}}, - true, - "LP_USER_LOG_LEVEL_MASK", - "Logging", - "Output"}}, {LP_ZOOMSTEPS, Parameter_Value{"Zoomsteps", PARAM_LONG, Persistence::PERSISTENT, 32l, "Defines how many discrete steps are needed to zoom from the outer edge to the center.", @@ -361,26 +347,6 @@ const std::unordered_map parameter_defaults = "Sensitivity of automatic speed control (percent).", "Auto-Speed Sensitivity", Settings::UIControlType::Slider, 0, 200, 1, 5, true, "LP_AUTOSPEED_SENSITIVITY", "CDefaultFilter", "Input"}}, - {LP_SOCKET_PORT, - Parameter_Value{"SocketPort", PARAM_LONG, Persistence::PERSISTENT, 20320l, - "UDP/TCP socket to use for network socket input.", "Socket Port", Settings::UIControlType::Step, - 1024, 65535, 1, 1, true, "LP_SOCKET_PORT", "CSocket", "Input"}}, - {LP_SOCKET_INPUT_X_MIN, Parameter_Value{"SocketInputXMinTimes1000", PARAM_LONG, Persistence::PERSISTENT, 0l, - "Bottom of range of X values expected from network input.", "Socket X Min", - Settings::UIControlType::Step, -10000, 10000, 1, 100, true, - "LP_SOCKET_INPUT_X_MIN", "CSocket", "Input"}}, - {LP_SOCKET_INPUT_X_MAX, Parameter_Value{"SocketInputXMaxTimes1000", PARAM_LONG, Persistence::PERSISTENT, 1000l, - "Top of range of X values expected from network input.", "Socket X Max", - Settings::UIControlType::Step, -10000, 10000, 1, 100, true, - "LP_SOCKET_INPUT_X_MAX", "CSocket", "Input"}}, - {LP_SOCKET_INPUT_Y_MIN, Parameter_Value{"SocketInputYMinTimes1000", PARAM_LONG, Persistence::PERSISTENT, 0l, - "Bottom of range of Y values expected from network input.", "Socket Y Min", - Settings::UIControlType::Step, -10000, 10000, 1, 100, true, - "LP_SOCKET_INPUT_Y_MIN", "CSocket", "Input"}}, - {LP_SOCKET_INPUT_Y_MAX, Parameter_Value{"SocketInputYMaxTimes1000", PARAM_LONG, Persistence::PERSISTENT, 1000l, - "Top of range of Y values expected from network input.", "Socket Y Max", - Settings::UIControlType::Step, -10000, 10000, 1, 100, true, - "LP_SOCKET_INPUT_Y_MAX", "CSocket", "Input"}}, {LP_CIRCLE_PERCENT, Parameter_Value{"CirclePercent", PARAM_LONG, Persistence::PERSISTENT, 10l, "Percentage of nominal vertical range to use for radius of start circle.", "Circle Start Size", Settings::UIControlType::Slider, 1, 50, 1, 1, true, @@ -438,19 +404,6 @@ const std::unordered_map parameter_defaults = Parameter_Value{"Static1BZoom", PARAM_LONG, Persistence::PERSISTENT, 8l, "The amount of zoom applied per step in static mode.", "Static Zoom Step", Settings::UIControlType::Step, 1, 16, 1, 1, true, "LP_STATIC1B_ZOOM", "CStaticFilter", "Input"}}, - {LP_DEMO_SPRING, - Parameter_Value{"DemoSpring", PARAM_LONG, Persistence::PERSISTENT, 100l, - "A physics parameter for the automated cursor's 'springiness' in demo mode.", "Demo Springiness", - Settings::UIControlType::Step, 0, 1000, 1, 1, true, "LP_DEMO_SPRING", "CDemoFilter", "Input"}}, - {LP_DEMO_NOISE_MEM, - Parameter_Value{"DemoNoiseMem", PARAM_LONG, Persistence::PERSISTENT, 100l, - "A physics parameter controlling the 'memory' or consistency of random movements in demo mode.", - "Demo Noise Memory", Settings::UIControlType::Step, 0, 1000, 1, 1, true, "LP_DEMO_NOISE_MEM", - "CDemoFilter", "Input"}}, - {LP_DEMO_NOISE_MAG, - Parameter_Value{"DemoNoiseMag", PARAM_LONG, Persistence::PERSISTENT, 325l, - "Controls the magnitude or intensity of random movements in demo mode.", "Demo Noise Magnitude", - Settings::UIControlType::Step, 0, 1000, 1, 1, true, "LP_DEMO_NOISE_MAG", "CDemoFilter", "Input"}}, {LP_MAXZOOM, Parameter_Value{"ClickMaxZoom", PARAM_LONG, Persistence::PERSISTENT, 200l, "Controls zoom mechanics in click mode.", "Zoom Sensitivity / Max Zoom", diff --git a/src/DasherCore/SettingsStore.cpp b/src/DasherCore/SettingsStore.cpp index ca6088300..cffa78d3c 100644 --- a/src/DasherCore/SettingsStore.cpp +++ b/src/DasherCore/SettingsStore.cpp @@ -27,19 +27,19 @@ void CSettingsStore::AddParameters(const std::unordered_map(parameters_.at(key).value)) { - DASHER_ASSERT(parameters_.at(key).type == Settings::ParamBool); + DASHER_ASSERT(parameters_.at(key).type == Settings::PARAM_BOOL); if (!LoadSetting(parameters_.at(key).storageName, &std::get(parameters_.at(key).value))) { parameters_.at(key).value = value.value; SaveSetting(value.storageName, std::get(value.value)); } } else if (std::holds_alternative(parameters_.at(key).value)) { - DASHER_ASSERT(parameters_.at(key).type == Settings::ParamLong); + DASHER_ASSERT(parameters_.at(key).type == Settings::PARAM_LONG); if (!LoadSetting(parameters_.at(key).storageName, &std::get(parameters_.at(key).value))) { parameters_.at(key).value = value.value; SaveSetting(value.storageName, std::get(value.value)); } } else if (std::holds_alternative(parameters_.at(key).value)) { - DASHER_ASSERT(parameters_.at(key).type == Settings::ParamString); + DASHER_ASSERT(parameters_.at(key).type == Settings::PARAM_STRING); if (!LoadSetting(parameters_.at(key).storageName, &std::get(parameters_.at(key).value))) { parameters_.at(key).value = value.value; SaveSetting(value.storageName, std::get(value.value)); @@ -119,8 +119,11 @@ void CSettingsStore::SetStringParameter(Parameter parameter, const std::string s template const T& CSettingsStore::GetParameter(Parameter parameter) const { auto p = parameters_.find(parameter); - // Check that the parameter is in fact in the right spot in the table - DASHER_ASSERT(p != parameters_.end() && std::holds_alternative(p->second.type)); + // Parameter must exist in the table. A type mismatch (e.g. caller asks + // for a string on a bool parameter) is a recoverable condition — + // std::get below will throw std::bad_variant_access, which CAPI + // callers like dasher_get_string_parameter catch and map to "". + DASHER_ASSERT(p != parameters_.end()); return std::get(p->second.value); } diff --git a/src/DasherCore/SimpleTimer.cpp b/src/DasherCore/SimpleTimer.cpp deleted file mode 100644 index 243bc0a78..000000000 --- a/src/DasherCore/SimpleTimer.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "SimpleTimer.h" -using namespace std::chrono; - -CSimpleTimer::CSimpleTimer() { - start = steady_clock::now(); -} - -CSimpleTimer::~CSimpleTimer() = default; - -double CSimpleTimer::GetElapsed() const { - const steady_clock::time_point end = steady_clock::now(); - const auto span = duration_cast>(end - start); - return span.count(); -} diff --git a/src/DasherCore/SimpleTimer.h b/src/DasherCore/SimpleTimer.h deleted file mode 100644 index 106cbca36..000000000 --- a/src/DasherCore/SimpleTimer.h +++ /dev/null @@ -1,22 +0,0 @@ - -// Simple millisecond accurate timer. -// -// Copyright 2004 by Keith Vertanen - -#pragma once - -#include - -/// \ingroup Logging -/// \{ -class CSimpleTimer { - public: - CSimpleTimer(); - ~CSimpleTimer(); - - double GetElapsed() const; - - private: - std::chrono::steady_clock::time_point start; -}; -/// \} diff --git a/src/DasherCore/SocketInput.cpp b/src/DasherCore/SocketInput.cpp deleted file mode 100644 index e8a9a205f..000000000 --- a/src/DasherCore/SocketInput.cpp +++ /dev/null @@ -1,38 +0,0 @@ -// SocketInput.cpp -// -// (C) Copyright Seb Wills 2005 -// -// Linux-specific subclass of SocketInputBase: provides network socket control of Dasher cursor - -#ifndef _WIN32 -#include "DasherCore/Common/Common.h" - -#include "SocketInput.h" -#include "SocketInputBase.h" - -#include - -using namespace Dasher; - -CSocketInput::CSocketInput(CSettingsUser* pCreator, CMessageDisplay* pMsgs) : CSocketInputBase(pCreator, pMsgs) {} - -CSocketInput::~CSocketInput() { - StopListening(); -} - -// private methods: - -bool CSocketInput::LaunchReaderThread() { - if (pthread_create(&readerThread, NULL, ThreadLauncherStub, this) == 0) { - return true; - } else { - // TODO should probably pop up a Gtk error message and think about how to do i18n: - cerr << _("Dasher socket input: failed to launch reader thread.") << endl; - return false; - } -} - -void CSocketInput::CancelReaderThread() { - pthread_cancel(readerThread); -} -#endif \ No newline at end of file diff --git a/src/DasherCore/SocketInput.h b/src/DasherCore/SocketInput.h deleted file mode 100644 index 073df87fe..000000000 --- a/src/DasherCore/SocketInput.h +++ /dev/null @@ -1,58 +0,0 @@ -// SocketInput.h -// -// (C) Copyright Seb Wills 2005 -// -// Linux-specific subclass of SocketInputBase: provides network socket control of Dasher cursor - -#pragma once - -#ifndef _WIN32 - -#include "./SocketInputBase.h" -#include "./Observable.h" - -#include -#include - -#define GCC_VERSION (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) - -namespace Dasher { -class CSocketInput; -#if GCC_VERSION >= 40100 -void* ThreadLauncherStub(void* _myClass); -#endif - -/// \ingroup Input -/// \{ -class CSocketInput : public CSocketInputBase { - - // This non-member launcher stub function is required because pthreads can't launch a non-static member method. - friend void* ThreadLauncherStub(void* _myClass) { - CSocketInput* myClass = reinterpret_cast(_myClass); - - pthread_setcanceltype( - PTHREAD_CANCEL_ASYNCHRONOUS, - NULL); // NOLINT(cert-pos47-c) // kill this thread immediately if another thread cancels it - // don't know how this interacts with recv blocking - - myClass->ReadForever(); - - return NULL; - } - - public: - CSocketInput(CSettingsUser* pCreator, CMessageDisplay* pMsgs); - ~CSocketInput(); - - private: - pthread_t readerThread; - - bool LaunchReaderThread(); - - void CancelReaderThread(); - - // TODO: should probably override ReportErrnoError() to popup a Gtk error message -}; -} // namespace Dasher -/// \} -#endif diff --git a/src/DasherCore/SocketInputBase.cpp b/src/DasherCore/SocketInputBase.cpp deleted file mode 100644 index 033f564e9..000000000 --- a/src/DasherCore/SocketInputBase.cpp +++ /dev/null @@ -1,333 +0,0 @@ -// SocketInputBase.cpp -// -// (C) Copyright Seb Wills 2005 -#ifndef _WIN32 - -#include "DasherCore/Common/Common.h" - -#include "SocketInputBase.h" - -#include "DasherInterfaceBase.h" - -#include -#include -#include -#ifdef _WIN32 -#include -#define DASHER_SOCKET_CLOSE_FUNCTION closesocket -#else -#include -#include -#include -#define DASHER_SOCKET_CLOSE_FUNCTION close -#endif - -using namespace Dasher; - -static SModuleSettings sSettings[] = { - {LP_SOCKET_PORT, T_LONGSPIN, 0, 65535, 1, 10, _("Port:")}, - {SP_SOCKET_INPUT_X_LABEL, T_STRING, -1, -1, -1, -1, _("X label:")}, - {LP_SOCKET_INPUT_X_MIN, T_LONGSPIN, -2147480000, 2147480000, 1000, 10000, _("X minimum:")}, - {LP_SOCKET_INPUT_X_MAX, T_LONGSPIN, -2147480000, 2147480000, 1000, 10000, _("X maximum:")}, - {SP_SOCKET_INPUT_Y_LABEL, T_STRING, -1, -1, -1, -1, _("Y label:")}, - {LP_SOCKET_INPUT_Y_MIN, T_LONGSPIN, -2147480000, 2147480000, 1000, 10000, _("Y minimum:")}, - {LP_SOCKET_INPUT_Y_MAX, T_LONGSPIN, -2147480000, 2147480000, 1000, 10000, _("Y maximum:")}, - {BP_SOCKET_DEBUG, T_BOOL, -1, -1, -1, -1, _("Print socket-related debugging information to console:")}}; - -Dasher::CSocketInputBase::CSocketInputBase(CSettingsUser* pCreator, CMessageDisplay* pMsgs) - : CScreenCoordInput(1, _("Socket Input")), CSettingsUserObserver(pCreator), m_pMsgs(pMsgs) { - port = -1; - debug_socket_input = false; - readerRunning = false; - sock = -1; - SetCoordinateCount(2); - for (int i = 0; i < DASHER_SOCKET_INPUT_MAX_COORDINATE_COUNT; i++) { - dasherMaxCoordinateValues[i] = 4096; // the real value will come later when SetMaxCoordinates is invoked - rawMinValues[i] = 0.0; // suitable defaults for BCI2000 - rawMaxValues[i] = 512.0; - memset(coordinateNames[i], '\0', DASHER_SOCKET_INPUT_MAX_COORDINATE_LABEL_LENGTH + 1); - dasherCoordinates[i] = 2048; // initialise to mid-range value - } - - // initialise using parameter settings: - SetDebug(GetBoolParameter(BP_SOCKET_DEBUG)); - SetReaderPort(GetLongParameter(LP_SOCKET_PORT)); - SetRawRange(0, ((double)GetLongParameter(LP_SOCKET_INPUT_X_MIN)) / 1000.0, - ((double)GetLongParameter(LP_SOCKET_INPUT_X_MAX)) / 1000.0); - SetRawRange(1, ((double)GetLongParameter(LP_SOCKET_INPUT_Y_MIN)) / 1000.0, - ((double)GetLongParameter(LP_SOCKET_INPUT_Y_MAX)) / 1000.0); - SetCoordinateLabel(0, GetStringParameter(SP_SOCKET_INPUT_X_LABEL).c_str()); - SetCoordinateLabel(1, GetStringParameter(SP_SOCKET_INPUT_Y_LABEL).c_str()); - SocketDebugMsg("Socket input is initialised but not yet enabled"); -} - -Dasher::CSocketInputBase::~CSocketInputBase() { - // Would like to call StopListening(); Can't do this here because by the time this (base class) destructor is - // called, - // the derived class instance has been deleted, so we can no longer call it's StopListening. - // Instead, you should call StopListening in the derived class's destructor. -} - -void Dasher::CSocketInputBase::HandleEvent(int iParameter) { - switch (iParameter) { - case LP_SOCKET_PORT: - SetReaderPort(GetLongParameter(LP_SOCKET_PORT)); - break; - case SP_SOCKET_INPUT_X_LABEL: - SetCoordinateLabel(0, GetStringParameter(SP_SOCKET_INPUT_X_LABEL).c_str()); - break; - case SP_SOCKET_INPUT_Y_LABEL: - SetCoordinateLabel(1, GetStringParameter(SP_SOCKET_INPUT_Y_LABEL).c_str()); - break; - case LP_SOCKET_INPUT_X_MIN: - case LP_SOCKET_INPUT_X_MAX: - SetRawRange(0, ((double)GetLongParameter(LP_SOCKET_INPUT_X_MIN)) / 1000.0, - ((double)GetLongParameter(LP_SOCKET_INPUT_X_MAX)) / 1000.0); - break; - case LP_SOCKET_INPUT_Y_MIN: - case LP_SOCKET_INPUT_Y_MAX: - SetRawRange(1, ((double)GetLongParameter(LP_SOCKET_INPUT_Y_MIN)) / 1000.0, - ((double)GetLongParameter(LP_SOCKET_INPUT_Y_MAX)) / 1000.0); - break; - case BP_SOCKET_DEBUG: - SetDebug(GetBoolParameter(BP_SOCKET_DEBUG)); - break; - default: - break; - } -} - -bool Dasher::CSocketInputBase::StartListening() { - struct sockaddr_in name; - - // this shouldn't be called if we are already listening, but if it is, let's failsafe - // rather than attempt to bind to the same port twice. - if (readerRunning) { - StopListening(); - } - - SocketDebugMsg("Socket input: binding to socket and starting to listen."); - - sock = socket(PF_INET, SOCK_DGRAM, 0); - if (sock == -1) { - // TODO This is not a very good error message even in English...??? - m_pMsgs->Message(_("Error creating socket"), true); - return false; - } - - name.sin_family = AF_INET; - name.sin_port = htons(port); - name.sin_addr.s_addr = htonl(INADDR_ANY); - if (::bind(sock, reinterpret_cast(&name), sizeof(name)) < 0) { - ReportErrnoError(_("Error binding to socket - already in use?")); - DASHER_SOCKET_CLOSE_FUNCTION(sock); - sock = -1; - return false; - } - - if (!LaunchReaderThread()) { - // LaunchReaderThread will already have displayed an error message - DASHER_SOCKET_CLOSE_FUNCTION(sock); - sock = -1; - return false; - } - - readerRunning = true; - return true; -} - -void Dasher::CSocketInputBase::StopListening() { - - if (!readerRunning) { - return; - } - - CancelReaderThread(); - - if (sock >= 0) { - DASHER_SOCKET_CLOSE_FUNCTION(sock); - } - readerRunning = false; - SocketDebugMsg("Socket input: stopped listening to socket."); -} - -void CSocketInputBase::SetReaderPort(int _port) { - if (_port == port) { - SocketDebugMsg("SetReaderPort called with same value (%d), so ignoring.", port); - return; - } - - SocketDebugMsg("Setting socket input port to %d.", _port); - if (readerRunning) { - StopListening(); - port = _port; - StartListening(); - } else { - port = _port; - } -} - -void CSocketInputBase::SetCoordinateLabel(int iWhichCoordinate, const char* Label) { - DASHER_ASSERT(iWhichCoordinate < DASHER_SOCKET_INPUT_MAX_COORDINATE_COUNT); - if (strlen(Label) > DASHER_SOCKET_INPUT_MAX_COORDINATE_LABEL_LENGTH) { - const char* msg = _("Warning truncating socket input label '%s' to %i characters."); - char* buf(new char[strlen(msg) + strlen(Label) + DASHER_SOCKET_INPUT_MAX_COORDINATE_LABEL_LENGTH]); - sprintf(buf, msg, Label, DASHER_SOCKET_INPUT_MAX_COORDINATE_LABEL_LENGTH); - m_pMsgs->Message(buf, true); - delete[] buf; - } - strncpy(coordinateNames[iWhichCoordinate], Label, DASHER_SOCKET_INPUT_MAX_COORDINATE_LABEL_LENGTH); - SocketDebugMsg("Socket input: set coordinate %d label to '%s'.", iWhichCoordinate, - coordinateNames[iWhichCoordinate]); -} - -void CSocketInputBase::SetRawRange(int iWhich, double dMin, double dMax) { - rawMinValues[iWhich] = dMin; - rawMaxValues[iWhich] = dMax; - SocketDebugMsg("Socket input: set coordinate %d input range to: min: %lf, max: %lf.", iWhich, dMin, dMax); -} - -// private methods: - -void CSocketInputBase::ReadForever() { - // this gets called in its own thread. It reads datagrams and updates the coordinate variables - - int numbytes; - while (sock >= 0) { - SocketDebugMsg("Reading from socket..."); - numbytes = recv(sock, buffer, sizeof(buffer) - 1, 0); - if (numbytes == -1) { - m_pMsgs->Message(_("Socket input: Error reading from socket"), false); - continue; - } - buffer[numbytes] = '\0'; - - SocketDebugMsg(" received string: '%s'.", buffer); - - ParseMessage(buffer); - } -} - -// Parse and act on a message received from the socket -// Allowed to modify contents of memory pointed to by message, up to its final '\0'. -void CSocketInputBase::ParseMessage(char* message) { - - char* p; - double rawdouble; - // myint dasherCoordinateTemp; - // parse line by line - while ((p = strchr(message, '\n')) != NULL) { - *p = '\0'; - // Each line is expected to be of the form "Label " - // We run through each coordinate label, checking if this line matches it - for (int i = 0; i < coordinateCount; i++) { - int len = strlen(coordinateNames[i]); - if (strncmp(coordinateNames[i], message, len) == 0) { - SocketDebugMsg("Matched label '%s'...", coordinateNames[i]); - // First len chars match the label of this coordinate. Value should be at the next non-space char. - if (sscanf(message + len, "%lf", &rawdouble) == 1) { - SocketDebugMsg("...parsed value as %lf.", rawdouble); - -#ifdef DASHER_SOCKET_INPUT_BCI2000_OVERFLOW_WORKAROUND - // a temporary workaround to undo an integer overflow that occurs in messages sent from BCI2000 - if (rawdouble > 32000) { - rawdouble = 0; - } - if (rawdouble > 768 && rawdouble < 32000) { - rawdouble = 768; - } -#endif - - // Clipping: - // for clipping purposes, we want to ignore whether Max < Min (which indicates that - // we need to flip the sense of the input) - double actualMax = (rawMaxValues[i] > rawMinValues[i]) ? rawMaxValues[i] : rawMinValues[i]; - double actualMin = (rawMaxValues[i] > rawMinValues[i]) ? rawMinValues[i] : rawMaxValues[i]; - if (rawdouble < actualMin) { - // TODO: Should these be converted to calls to Message() ? On first occurrence only??? - cerr << "Socket input: clipped " << coordinateNames[i] << " value of " << rawdouble - << "to configured minimum of " << actualMin << endl; - rawdouble = actualMin; - } - if (rawdouble > actualMax) { - // TODO: Should these be converted to calls to Message() ? On first occurrence only??? - cerr << "Socket input: clipped " << message << " value of " << rawdouble - << "to configured maximum of " << actualMax << endl; - rawdouble = actualMax; - } - - // convert to dasher coordinates: - - const bool do_lowpass = false; - if (do_lowpass) { - // initial attempt at putting a low-pass filter in. Not well tested; disabled for now. - double timeconst = 100.0; // no of updates - double newcoord = ((rawdouble - rawMinValues[i]) / (rawMaxValues[i] - rawMinValues[i]) * - dasherMaxCoordinateValues[i]); - dasherCoordinates[i] = - (myint)((1 - 1 / timeconst) * (double)dasherCoordinates[i] + (1 / timeconst) * newcoord); - } else { - // straightforward linear mapping to dasher coordinates: - // Treat X coordinate specially: reverse sense so it has the more intuitive left-to-right - // direction - double min = (i == 0) ? rawMaxValues[i] : rawMinValues[i]; - double max = (i == 0) ? rawMinValues[i] : rawMaxValues[i]; - if (max != min) { // prevent nasty explosion - dasherCoordinates[i] = - (myint)((rawdouble - min) / (max - min) * (double)dasherMaxCoordinateValues[i]); - } - } - - SocketDebugMsg("Socket input: new value for coordinate %d rescales to %u in Dasher's internal " - "coordinates (range 0-%d).", - i, (unsigned int)dasherCoordinates[i], (int)dasherMaxCoordinateValues[i]); - - // don't break out of the for loop in case we get asked to drive two coordinates from same label - } else { - SocketDebugMsg("... but couldn't parse the text following that label as a number."); - } - } - } - - message = p + 1; // move on to next line (if there isn't one, we'll point at the terminating '\0') - } -} - -void CSocketInputBase::SetDebug(bool _debug) { - if (!_debug) { - SocketDebugMsg("Disabling socket debug messages."); - } - debug_socket_input = _debug; - if (_debug) { - SocketDebugMsg("Enabled socket debug messages."); - } -} - -void CSocketInputBase::ReportErrnoError(const std::string& prefix) { - int err = errno; - errno = 0; - const char* msg = _("Dasher Socket Input error: %s: %s"); - char* e = strerror(err); - char* buf(new char[strlen(msg) + prefix.length() + strlen(e)]); - sprintf(buf, msg, prefix.c_str(), e); - m_pMsgs->Message(buf, true); - delete[] buf; -} - -void CSocketInputBase::SocketDebugMsg(const char* pszFormat, ...) { - if (debug_socket_input) { - va_list v; - va_start(v, pszFormat); - vfprintf(stderr, pszFormat, v); - fprintf(stderr, "\n"); - va_end(v); - } -} - -bool CSocketInputBase::GetSettings(SModuleSettings** pSettings, int* iCount) { - *pSettings = sSettings; - *iCount = sizeof(sSettings) / sizeof(SModuleSettings); - - return true; -} -#endif \ No newline at end of file diff --git a/src/DasherCore/SocketInputBase.h b/src/DasherCore/SocketInputBase.h deleted file mode 100644 index ae9b80a70..000000000 --- a/src/DasherCore/SocketInputBase.h +++ /dev/null @@ -1,121 +0,0 @@ -// SocketInputBase.h -// -// (C) Copyright Seb Wills 2005 -// -// Abstract base class for socket input: parent of non-abstract classes in each implementation (Windows, Linux, ...), -#ifndef _WIN32 - -#pragma once - -#include "DasherInput.h" -#include "SettingsStore.h" -#include "Messages.h" - -#include - -#define DASHER_SOCKET_INPUT_MAX_COORDINATE_COUNT 2 // just X and Y for now -#define DASHER_SOCKET_INPUT_MAX_COORDINATE_LABEL_LENGTH 128 - -namespace Dasher { -class CSocketInputBase; - -/// \ingroup Input -/// \{ -class CSocketInputBase : public CScreenCoordInput, public CSettingsUserObserver { - - public: - CSocketInputBase(CSettingsUser* pCreator, CMessageDisplay* pMsgs); - - virtual ~CSocketInputBase(); - - virtual void HandleEvent(Parameter parameter); - - virtual void SetDebug(bool _debug); - - virtual bool StartListening(); - - virtual void StopListening(); - - virtual bool isListening() { return readerRunning; } - - virtual void SetReaderPort(int port); - - virtual int GetPort() { return port; } - - void SetCoordinateCount(int _coordinateCount) { - DASHER_ASSERT(_coordinateCount <= DASHER_SOCKET_INPUT_MAX_COORDINATE_COUNT); - coordinateCount = _coordinateCount; - } - - /// Gets the last coordinates received; if only one coordinate is being read, this is put - /// into iDasherY (and iDasherX set to 0). - bool GetScreenCoords(screenint& iScreenX, screenint& iScreenY, CDasherView* pView) { - - // update max values for reader thread...(note any changes here won't be incorporated - // until values are next received over socket, but never mind) - dasherMaxCoordinateValues[0] = pView->Screen()->GetWidth(); - dasherMaxCoordinateValues[1] = pView->Screen()->GetHeight(); - - if (coordinateCount == 1) { - iScreenX = 0; - iScreenY = dasherCoordinates[0]; - } else if (coordinateCount == 2) { - iScreenX = dasherCoordinates[0]; - iScreenY = dasherCoordinates[1]; - } else { - // Aiieee, we're receiving >2 coords? Don't know what to do... - return false; - } - return true; - }; - - void Activate() { StartListening(); }; - - void Deactivate() { StopListening(); }; - - // Defines the label used in the input stream for a particular coordinate. - // We make our own copy of the label, in our own buffer. This should ensure thread-safety. - // Even if this method is called while our other thread is doing a strcmp on the label, - // the buffer will always be null-terminated somewhere (even if the last byte of the buffer, which is - // never overwritten), so won't segfault. - virtual void SetCoordinateLabel(int iWhichCoordinate, const char* Label); - - virtual void SetRawRange(int iWhich, double dMin, double dMax); - - bool GetSettings(SModuleSettings** pSettings, int* iCount); - - protected: - myint dasherCoordinates[DASHER_SOCKET_INPUT_MAX_COORDINATE_COUNT]; - myint dasherMaxCoordinateValues[DASHER_SOCKET_INPUT_MAX_COORDINATE_COUNT]; - double rawMinValues[DASHER_SOCKET_INPUT_MAX_COORDINATE_COUNT]; - double rawMaxValues[DASHER_SOCKET_INPUT_MAX_COORDINATE_COUNT]; - int coordinateCount; - char coordinateNames[DASHER_SOCKET_INPUT_MAX_COORDINATE_COUNT][DASHER_SOCKET_INPUT_MAX_COORDINATE_LABEL_LENGTH + 1]; - - int port; - bool debug_socket_input; - - int sock; - - char buffer[4096]; - - bool readerRunning; - - virtual bool LaunchReaderThread() = 0; - - virtual void CancelReaderThread() = 0; - - virtual void ReadForever(); - - virtual void ParseMessage(char* message); - - // Reports an error by appending an error message obtained from strerror(errno) onto the provided prefix - void ReportErrnoError(const std::string& prefix); - - virtual void SocketDebugMsg(const char* pszFormat, ...); - - CMessageDisplay* const m_pMsgs; -}; -} // namespace Dasher -/// \} -#endif diff --git a/src/DasherCore/TimeSpan.cpp b/src/DasherCore/TimeSpan.cpp deleted file mode 100644 index b02cd932b..000000000 --- a/src/DasherCore/TimeSpan.cpp +++ /dev/null @@ -1,167 +0,0 @@ -#include -#include -#include -#include "TimeSpan.h" - -#include "XMLUtil.h" - -CTimeSpan::CTimeSpan(const std::string& strName, bool bAddDate) { - InitMemberVars(); - - m_strName = strName; - m_strStartTime = GetTimeStamp(); - m_pTimer = new CSimpleTimer(); - m_dElapsed = 0.0; - m_strEndTime = ""; - m_strStartDate = ""; - - // A time span can optionally record the current date - if (bAddDate) m_strStartDate = GetDateStamp(); -} - -CTimeSpan::~CTimeSpan() { - if (m_pTimer != NULL) { - delete m_pTimer; - m_pTimer = NULL; - } -} - -// Get the XML for this TimeSpan object. If bSinglePointInTime is true, then -// this is just a single point in time and we don't need the end time or -// elapsed time. -std::string CTimeSpan::GetXML(const std::string& strPrefix, bool bSinglePointInTime) { - std::string strResult; - - // Only stop if we haven't called Stop() explicitly - if (m_strEndTime.empty()) Stop(); - - strResult += strPrefix; - strResult += "<"; - strResult += m_strName; - strResult += ">\n"; - - if (!bSinglePointInTime) { - strResult += strPrefix; - strResult += "\t"; - char strNum[256]; - snprintf(strNum, sizeof(strNum), "%0.3f", m_dElapsed); - strResult += strNum; - strResult += "\n"; - } - - if (m_strStartDate.length() > 0) { - strResult += strPrefix; - strResult += "\t"; - strResult += m_strStartDate; - strResult += "\n"; - } - - if (!bSinglePointInTime) { - strResult += strPrefix; - strResult += "\t"; - strResult += m_strStartTime; - strResult += "\n"; - - strResult += strPrefix; - strResult += "\t"; - strResult += m_strEndTime; - strResult += "\n"; - } else { - strResult += strPrefix; - strResult += "\t\n"; - } - - strResult += strPrefix; - strResult += "\n"; - - return strResult; -} - -std::string CTimeSpan::GetTimeStamp() { - auto now = std::chrono::system_clock::now(); - auto ms = std::chrono::duration_cast(now.time_since_epoch()) % 1000; - auto time_t = std::chrono::system_clock::to_time_t(now); - std::tm time_tm = *std::localtime(&time_t); - char buf[80]; - std::strftime(buf, sizeof(buf), "%H:%M:%S", &time_tm); - - return std::string(buf) + "." + std::to_string(ms.count()); -} - -void CTimeSpan::Stop() { - // Only do this if we actually have a timer, if we were - // created from XML then we don't want to change what - // we read from the file. - if (m_pTimer != NULL) { - m_strEndTime = GetTimeStamp(); - m_dElapsed = m_pTimer->GetElapsed(); - } -} - -// We allow a time span to continue to erase the -// effects of a previous Stop() call -void CTimeSpan::Continue() { - m_strEndTime = ""; - m_dElapsed = 0.0; -} - -bool CTimeSpan::IsStopped() { - if (m_strEndTime.length() > 0) return true; - - return false; -} - -double CTimeSpan::GetElapsed() { - return m_dElapsed; -} - -std::string CTimeSpan::GetDateStamp() { - std::string strDateStamp = ""; - char* szTimeLine = NULL; - time_t t; - - t = time(NULL); - szTimeLine = ctime(&t); - - // Format is: - // Wed Jun 22 10:22:00 2005 - // 0123456789012345678901234 - if ((szTimeLine != NULL) && (strlen(szTimeLine) > 23)) { - for (int i = 4; i < 10; i++) - strDateStamp += szTimeLine[i]; - for (int i = 19; i < 24; i++) - strDateStamp += szTimeLine[i]; - } - - return strDateStamp; -} - -void CTimeSpan::InitMemberVars() { - m_pTimer = NULL; - m_strName = ""; - m_strStartTime = ""; - m_dElapsed = 0.0; - m_strEndTime = ""; - m_strStartDate = ""; -} - -// Construct based on some yummy XML like: -// 12.062 -// Jul 04 2005 -// 15:48:52.625 -// 15:49:04.687 -CTimeSpan::CTimeSpan(const std::string& strName, const std::string& strXML) { - InitMemberVars(); - - m_dElapsed = (double)XMLUtil::GetElementFloat("Elapsed", strXML); - - m_strStartDate = XMLUtil::GetElementString("Date", strXML); - m_strStartTime = XMLUtil::GetElementString("Start", strXML); - m_strEndTime = XMLUtil::GetElementString("End", strXML); - - m_strName = strName; -} diff --git a/src/DasherCore/TimeSpan.h b/src/DasherCore/TimeSpan.h deleted file mode 100644 index 064679f7b..000000000 --- a/src/DasherCore/TimeSpan.h +++ /dev/null @@ -1,50 +0,0 @@ - -// Object that keeps track of a time span. -// Span starts when the object is created, and -// ends when someone asks it for its XML representation. -// User can also call stop to cause timer to stop and -// call for XML representation later. -// -// Copyright 2005 by Keith Vertanen - -#pragma once - -#include -#include "SimpleTimer.h" -#include - -class CTimeSpan; - -typedef std::vector VECTOR_TIME_SPAN; -typedef std::vector VECTOR_TIME_SPAN_PTR; - -/// \ingroup Logging -/// @{ -class CTimeSpan { - public: - CTimeSpan(const std::string& strName, bool bAddDate); - CTimeSpan(const std::string& strName, const std::string& strXML); - - ~CTimeSpan(); - - void Stop(); - std::string GetXML(const std::string& strPrefix = "", bool bSinglePointInTime = false); - - void Continue(); - bool IsStopped(); - double GetElapsed(); - - static std::string GetTimeStamp(); - static std::string GetDateStamp(); - - private: - std::string m_strName; - std::string m_strStartTime; - std::string m_strEndTime; - double m_dElapsed; - CSimpleTimer* m_pTimer; - std::string m_strStartDate; - - void InitMemberVars(); -}; -/// @} diff --git a/src/DasherCore/TwoBoxStartHandler.cpp b/src/DasherCore/TwoBoxStartHandler.cpp index 223b12245..91f250d0e 100644 --- a/src/DasherCore/TwoBoxStartHandler.cpp +++ b/src/DasherCore/TwoBoxStartHandler.cpp @@ -2,6 +2,8 @@ #include "DefaultFilter.h" #include "DasherView.h" +#include + using namespace Dasher; CTwoBoxStartHandler::CTwoBoxStartHandler(CDefaultFilter* pCreator, CSettingsStore* pSettingsStore) diff --git a/src/DasherCore/TwoButtonDynamicFilter.cpp b/src/DasherCore/TwoButtonDynamicFilter.cpp index a8cc2c708..e82703388 100644 --- a/src/DasherCore/TwoButtonDynamicFilter.cpp +++ b/src/DasherCore/TwoButtonDynamicFilter.cpp @@ -179,15 +179,12 @@ void CTwoButtonDynamicFilter::ActionButton(unsigned long iTime, Keys::VirtualKey iEffect = 4; // fall through to apply offset } else { - if (CUserLogBase* pUserLog = m_pInterface->GetUserLogPtr()) pUserLog->KeyDown(Key, iType, 0); return; } // fell through to apply offset ApplyOffset(pModel, static_cast(dFactor * m_pSettingsStore->GetLongParameter(LP_TWO_BUTTON_OFFSET) * exp(m_dLagBits * FrameSpeedMul(pModel, iTime)))); pModel->ResetNats(); - - if (CUserLogBase* pUserLog = m_pInterface->GetUserLogPtr()) pUserLog->KeyDown(Key, iType, iEffect); } bool CTwoButtonDynamicFilter::GetSettings(SModuleSettings** pSettings, int* iCount) { diff --git a/src/DasherCore/TwoPushDynamicFilter.cpp b/src/DasherCore/TwoPushDynamicFilter.cpp index 15912a510..0d0e1ac6e 100644 --- a/src/DasherCore/TwoPushDynamicFilter.cpp +++ b/src/DasherCore/TwoPushDynamicFilter.cpp @@ -56,7 +56,7 @@ void CTwoPushDynamicFilter::GetUISettings(std::vector& List) CTwoPushDynamicFilter::CTwoPushDynamicFilter(CSettingsStore* pSettingsStore, CDasherInterfaceBase* pInterface, CFrameRate* pFramerate) : CDynamicButtons(pSettingsStore, pInterface, pFramerate, _("Two-push Dynamic Mode (New One Button)")), - m_dNatsSinceFirstPush(-std::numeric_limits::infinity()) { + m_dNatsSinceFirstPush(-std::numeric_limits::infinity()), m_iActiveMarker(-1) { CTwoPushDynamicFilter::HandleParameterChange(LP_TWO_PUSH_OUTER); // and all the others too! m_pSettingsStore->OnParameterChanged.Subscribe(this, [this](Parameter p) { HandleParameterChange(p); }); } @@ -136,7 +136,6 @@ void CTwoPushDynamicFilter::HandleParameterChange(Parameter parameter) { double dOuter = m_pSettingsStore->GetLongParameter(LP_TWO_PUSH_OUTER); m_dLogUpMul = log(dOuter / upDist()); m_dLogDownMul = log(dOuter / downDist()); - // cout << "bitsUp " << m_dLogUpMul << " bitsDown " << m_dLogDownMul << std::endl; } [[fallthrough]]; case LP_TWO_PUSH_TOLERANCE: @@ -155,7 +154,6 @@ void CTwoPushDynamicFilter::updateBitrate(double dBitrate) { m_dLastBitRate = dBitrate; double dPressBits = dBitrate * (double)m_pSettingsStore->GetLongParameter(LP_TWO_PUSH_TOLERANCE) / 1000.0; - // cout << "Max Bitrate changed - now " << dBitrate << " user accuracy " << dPressBits; m_dMinShortTwoPushTime = m_dLogUpMul - dPressBits; m_dMaxShortTwoPushTime = m_dLogUpMul + dPressBits; m_dMinLongTwoPushTime = m_dLogDownMul - dPressBits; @@ -164,8 +162,6 @@ void CTwoPushDynamicFilter::updateBitrate(double dBitrate) { m_dMaxLongTwoPushTime = m_dLogDownMul + dPressBits; // TODO, what requirements do we actually need to make to ensure sanity (specifically, that computed m_aiTarget's // are in range)? - // cout << "bits; minShort " << m_dMinShortTwoPushTime << " maxShort " << m_dMaxShortTwoPushTime << " minLong " << - // m_dMinLongTwoPushTime << " maxLong " << m_dMaxLongTwoPushTime << std::endl; m_bDecorationChanged = true; m_dLagBits = dBitrate * m_pSettingsStore->GetLongParameter(LP_DYNAMIC_BUTTON_LAG) / 1000.0; @@ -178,8 +174,6 @@ void CTwoPushDynamicFilter::updateBitrate(double dBitrate) { m_aaiGuideAreas[0][1] = 2048 - static_cast(up * exp(m_dMinShortTwoPushTime)); m_aaiGuideAreas[1][0] = 2048 + static_cast(down * exp(m_dMinLongTwoPushTime)); m_aaiGuideAreas[1][1] = 2048 + static_cast(down * exp(m_dMaxLongTwoPushTime)); - // cout << "Short " << m_aaiGuideAreas[0][0] << " to " << m_aaiGuideAreas[0][1] << ", Long " << - // m_aaiGuideAreas[1][0] << " to " << m_aaiGuideAreas[1][1]; } void CTwoPushDynamicFilter::KeyDown(unsigned long Time, Keys::VirtualKey Key, CDasherView* pView, CDasherInput* pInput, @@ -217,9 +211,7 @@ void CTwoPushDynamicFilter::ActionButton(unsigned long iTime, Keys::VirtualKey K // no button pushed (recently) m_dNatsSinceFirstPush = pModel->GetNats(); // note, could be negative if overall reversed since last ResetNats (Offset) - // cout << "First push - got " << m_dNatsSinceFirstPush << std::endl; } else { - // cout << "Second push - event type " << iType << " logGrowth " << pModel->GetNats() << std::endl; if (m_iActiveMarker == -1) reverse(iTime); else { @@ -265,7 +257,6 @@ void CTwoPushDynamicFilter::TimerImpl(unsigned long iTime, CDasherView* m_pDashe m_bDecorationChanged |= doSet(m_aiMarker[1], static_cast(2048 + exp(m_dLagBits + dLogGrowth) * dDown)); if (dLogGrowth > m_dMaxLongTwoPushTime) { - // cout << " growth " << dLogGrowth << " - reversing" << std::endl; // button pushed, but then waited too long. reverse(iTime); } else if (dLogGrowth >= m_dMinShortTwoPushTime && dLogGrowth <= m_dMaxShortTwoPushTime) diff --git a/src/DasherCore/UserButton.cpp b/src/DasherCore/UserButton.cpp deleted file mode 100644 index 134b6799f..000000000 --- a/src/DasherCore/UserButton.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "TimeSpan.h" -#include "UserButton.h" - -#include - -CUserButton::CUserButton(int iId, int iType, int iEffect) { - m_iId = iId; - m_iType = iType; - m_iEffect = iEffect; - m_strStringTime = CTimeSpan::GetTimeStamp(); -} - -std::string CUserButton::GetXML(const std::string& strPrefix) { - std::stringstream ssResult; - - ssResult << strPrefix << "" << std::endl; - - return ssResult.str(); -} diff --git a/src/DasherCore/UserButton.h b/src/DasherCore/UserButton.h deleted file mode 100644 index 945719120..000000000 --- a/src/DasherCore/UserButton.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include -#include - -class CUserButton; - -typedef std::vector VECTOR_USER_BUTTON; -typedef std::vector::iterator VECTOR_USER_BUTTON_ITER; -typedef std::vector VECTOR_USER_BUTTON_PTR; -typedef std::vector::iterator VECTOR_USER_BUTTON_PTR_ITER; - -/// \ingroup Logging -/// \{ -class CUserButton { - public: - CUserButton(int iId, int iType, int iEffect); - - std::string GetXML(const std::string& strPrefix = ""); - - int GetCount() { - if (m_iType == 0) - return 1; - else - return 0; - } - - protected: - int m_iId; - int m_iType; - int m_iEffect; - std::string m_strStringTime; -}; -/// \} diff --git a/src/DasherCore/UserLocation.cpp b/src/DasherCore/UserLocation.cpp deleted file mode 100644 index 35a752b12..000000000 --- a/src/DasherCore/UserLocation.cpp +++ /dev/null @@ -1,228 +0,0 @@ -#include "UserLocation.h" - -#include - -#include "TimeSpan.h" -#include "XMLUtil.h" - -// Construct a new location at the current point in time and at the specified coordinates. -// This version only stores the integer coordinate data. -CUserLocation::CUserLocation(int iX, int iY, float dNats) { - // CFunctionLogger f1("CUserLocation::CUserLocation(1)", gLogger); - - InitMemeberVars(); - - m_strTime = CTimeSpan::GetTimeStamp(); - m_iLocationX = iX; - m_iLocationY = iY; - m_bHasInteger = true; - m_dNats = dNats; -} - -// Stores only the normalized floating point data. -CUserLocation::CUserLocation(float iX, float iY, float dNats) { - // CFunctionLogger f1("CUserLocation::CUserLocation(2)", gLogger); - - InitMemeberVars(); - - m_strTime = CTimeSpan::GetTimeStamp(); - m_dNormalizedLocationX = iX; - m_dNormalizedLocationY = iY; - m_bHasNormalized = true; - m_dNats = dNats; -} - -// Stores only the normalized floating point data. -// This version calculates the normalization itself. -CUserLocation::CUserLocation(int iX, int iY, int iTop, int iLeft, int iBottom, int iRight, bool bStoreIntegerRep, - float dNats) { - // CFunctionLogger f1("CUserLocation::CUserLocation(3)", gLogger); - - InitMemeberVars(); - - m_strTime = CTimeSpan::GetTimeStamp(); - m_dNormalizedLocationX = (float)ComputeNormalizedX(iX, iLeft, iRight); - m_dNormalizedLocationY = (float)ComputeNormalizedY(iY, iTop, iBottom); - m_bHasNormalized = true; - m_dNats = dNats; - - if (bStoreIntegerRep) { - m_bHasInteger = true; - m_iLocationX = iX; - m_iLocationY = iY; - } else - m_bHasInteger = false; -} - -// We want both the integer representation and the normalized. -CUserLocation::CUserLocation(int iX1, int iY1, float iX2, float iY2, float dNats) { - // CFunctionLogger f1("CUserLocation::CUserLocation(4)", gLogger); - - InitMemeberVars(); - - m_strTime = CTimeSpan::GetTimeStamp(); - m_iLocationX = iX1; - m_iLocationY = iY1; - m_dNormalizedLocationX = iX2; - m_dNormalizedLocationY = iY2; - m_bHasNormalized = true; - m_bHasInteger = true; - m_dNats = dNats; -} - -CUserLocation::~CUserLocation() { - // CFunctionLogger f1("CUserLocation::~CUserLocation", gLogger); -} - -std::string CUserLocation::GetXML(const std::string& strPrefix) { - // CFunctionLogger f1("CUserLocation::GetXML", gLogger); - - std::string strResult = ""; - - strResult += strPrefix; - strResult += "\n"; - - strResult += strPrefix; - strResult += "\t\n"; - - char strNum[256]; - if (m_bHasInteger) { - strResult += strPrefix; - strResult += "\t"; - snprintf(strNum, sizeof(strNum), "%d", m_iLocationX); - strResult += strNum; - strResult += "\n"; - - strResult += strPrefix; - strResult += "\t"; - snprintf(strNum, sizeof(strNum), "%d", m_iLocationY); - strResult += strNum; - strResult += "\n"; - } - if (m_bHasNormalized) { - strResult += strPrefix; - strResult += "\t"; - snprintf(strNum, sizeof(strNum), "%0.4f", m_dNormalizedLocationX); - strResult += strNum; - strResult += "\n"; - - strResult += strPrefix; - strResult += "\t"; - snprintf(strNum, sizeof(strNum), "%0.4f", m_dNormalizedLocationY); - strResult += strNum; - strResult += "\n"; - } - - strResult += strPrefix; - strResult += "\t"; - snprintf(strNum, sizeof(strNum), "%0.3f", m_dNats / log(2.0)); - strResult += strNum; - strResult += "\n"; - - strResult += strPrefix; - strResult += "\n"; - - return strResult; -} - -// Static helper method for computing normalized X coordinate -double CUserLocation::ComputeNormalizedX(int iX, int iLeft, int iRight) { - // CFunctionLogger f1("CUserLocation::ComputeNormalizedX", gLogger); - - return (double)(iX - iLeft) / (double)abs(iRight - iLeft); -} - -// Static helper method for computing normalized Y coordinate -double CUserLocation::ComputeNormalizedY(int iY, int iTop, int iBottom) { - // CFunctionLogger f1("CUserLocation::ComputeNormalizedY", gLogger); - - return (double)(iY - iTop) / (double)abs(iBottom - iTop); -} - -void CUserLocation::InitMemeberVars() { - // CFunctionLogger f1("CUserLocation::InitMemeberVars", gLogger); - - m_strTime = ""; - m_iLocationX = 0; - m_iLocationY = 0; - m_dNormalizedLocationX = 0.0; - m_dNormalizedLocationY = 0.0; - m_bHasNormalized = false; - m_bHasInteger = false; - m_dNats = 0.0; -} - -// Construct based on some XML like: -// -// -// 807 -// 382 -// 0.7274 -// 0.1853 -// 0.555 -// -CUserLocation::CUserLocation(const std::string& strXML) { - // CFunctionLogger f1("CUserLocation::CUserLocation(XML)", gLogger); - - InitMemeberVars(); - - bool bFoundNormX = false; - bool bFoundNormY = false; - bool bFoundX = false; - bool bFoundY = false; - - m_strTime = XMLUtil::GetElementString("Time", strXML, true); - m_iLocationX = XMLUtil::GetElementInt("X", strXML, &bFoundX); - m_iLocationY = XMLUtil::GetElementInt("Y", strXML, &bFoundY); - m_dNormalizedLocationX = (float)XMLUtil::GetElementFloat("XNorm", strXML, &bFoundNormX); - m_dNormalizedLocationY = (float)XMLUtil::GetElementFloat("YNorm", strXML, &bFoundNormY); - - // Convert the bits back to dNats - m_dNats = (float)((double)XMLUtil::GetElementFloat("Bits", strXML) * (double)log(2.0)); - - // If there weren't X, Y elements, we want them set to 0 and mark - // ourselves as not having them. - if ((!bFoundX) && (!bFoundY)) { - m_iLocationX = 0; - m_iLocationY = 0; - m_bHasInteger = false; - } else - m_bHasInteger = true; - - // Require that we find both XNorm and YNorm in order to count - if ((!bFoundNormX) || (!bFoundNormY)) { - m_dNormalizedLocationX = 0.0; - m_dNormalizedLocationY = 0.0; - m_bHasNormalized = false; - } else - m_bHasNormalized = true; -} - -// Returns a tab delimited version of this location's X & Y coordinate -std::string CUserLocation::GetTabMouseXY(bool bReturnNormalized) { - // CFunctionLogger f1("CUserLocation::GetTabMouseXY", gLogger); - - std::string strResult = ""; - char szNum[256]; - - if (bReturnNormalized) - snprintf(szNum, sizeof(szNum), "%0.4f\t%0.4f\n", m_dNormalizedLocationX, m_dNormalizedLocationY); - else - snprintf(szNum, sizeof(szNum), "%0.4f\t%0.4f\n", (double)m_iLocationX, (double)m_iLocationY); - - strResult += szNum; - - return strResult; -} - -// Figure out what grid location this normalized mouse coordinate should go. -void CUserLocation::GetMouseGridLocation(int iGridSize, int* pRow, int* pCol) { - // CFunctionLogger f1("CUserLocation::GetMouseGridLocation", gLogger); - - if ((pRow == NULL) || (pCol == NULL)) return; - - *pRow = (int)(m_dNormalizedLocationX * (double)iGridSize); - *pCol = (int)(m_dNormalizedLocationY * (double)iGridSize); -} diff --git a/src/DasherCore/UserLocation.h b/src/DasherCore/UserLocation.h deleted file mode 100644 index 1d651cc85..000000000 --- a/src/DasherCore/UserLocation.h +++ /dev/null @@ -1,53 +0,0 @@ - -// Keeps track of the user's mouse location at a certain point in time. -// -// Can store a integer x, y coordinate and/or a normalized floating -// point x, y pair where (1.0, 1.0) is the lower right corener of the -// window. -// -// Copyright 2005 by Keith Vertanen - -#pragma once - -#include -#include - -class CUserLocation; - -typedef std::vector VECTOR_USER_LOCATION; -typedef std::vector::iterator VECTOR_USER_LOCATION_ITER; -typedef std::vector VECTOR_USER_LOCATION_PTR; -typedef std::vector::iterator VECTOR_USER_LOCATION_PTR_ITER; - -/// \ingroup Logging -/// @{ -class CUserLocation { - public: - CUserLocation(int iX, int iY, float dNats); - CUserLocation(float iX, float iY, float dNats); - CUserLocation(int iX1, int iY1, float iX2, float iY2, float dNats); - CUserLocation(int iX, int iY, int iTop, int iLeft, int iBottom, int iRight, bool bStoreIntegerRep, float dNats); - ~CUserLocation(); - - std::string GetXML(const std::string& strPrefix = ""); - static double ComputeNormalizedX(int iX, int iLeft, int iRight); - static double ComputeNormalizedY(int iY, int iTop, int iBottom); - - // Used when we want to post-process a XML log file: - CUserLocation(const std::string& strXML); - std::string GetTabMouseXY(bool bReturnNormalized); - void GetMouseGridLocation(int iGridSize, int* pRow, int* pCol); - - private: - std::string m_strTime; - int m_iLocationX; - int m_iLocationY; - float m_dNormalizedLocationX; - float m_dNormalizedLocationY; - bool m_bHasNormalized; // Are we storing a normalized representation? - bool m_bHasInteger; // Are we storing an integer representation? - float m_dNats; - - void InitMemeberVars(); -}; -/// @} diff --git a/src/DasherCore/UserLog.cpp b/src/DasherCore/UserLog.cpp deleted file mode 100644 index b315916bd..000000000 --- a/src/DasherCore/UserLog.cpp +++ /dev/null @@ -1,913 +0,0 @@ -#include "UserLog.h" - -#include -#include -#include -#include - -#include "FileLogger.h" -#include "FileUtils.h" - -using namespace std::chrono; -using namespace Dasher; -using namespace Dasher::Settings; - -static UserLogParamMask s_UserLogParamMaskTable[] = { - {SP_ALPHABET_ID, userLogParamOutputToSimple}, - {SP_COLOUR_ID, userLogParamOutputToSimple}, - {LP_MAX_BITRATE, userLogParamOutputToSimple | userLogParamTrackMultiple | userLogParamTrackInTrial | - userLogParamForceInTrial | userLogParamShortInCycle}, - {LP_UNIFORM, userLogParamOutputToSimple}, - {LP_LANGUAGE_MODEL_ID, userLogParamOutputToSimple}, - {LP_LM_MAX_ORDER, userLogParamOutputToSimple}, - {LP_LM_EXCLUSION, userLogParamOutputToSimple}, - {LP_LM_UPDATE_EXCLUSION, userLogParamOutputToSimple}, - {LP_LM_ALPHA, userLogParamOutputToSimple}, - {LP_LM_BETA, userLogParamOutputToSimple}, - {LP_LM_MIXTURE, userLogParamOutputToSimple}, - {LP_LM_WORD_ALPHA, userLogParamOutputToSimple}}; - -CUserLog::CUserLog(CSettingsStore* pSettingsStore, CDasherInterfaceBase* pInterface, int iLogTypeMask) - : CUserLogBase(pInterface), m_pSettingsStore(pSettingsStore) { - // CFunctionLogger f1("CUserLog::CUserLog", g_pLogger); - m_pSettingsStore->OnParameterChanged.Subscribe(this, [this](const Parameter parameter) { - // Go through each of the parameters in our lookup table from UserLogParam.h. - // If the key matches the notification event, then we want to push the - // parameter change to the logging object. - for (auto [key, mask] : s_UserLogParamMaskTable) { - if (key == parameter) { - UpdateParam(parameter, mask); - return; - } - } - }); - - InitMemberVars(); - - m_iLevelMask = iLogTypeMask; - - InitUsingMask(iLogTypeMask); - - if ((m_bSimple) && (m_pSimpleLogger != NULL)) m_pSimpleLogger->LogDebug("start, %s", GetVersionInfo().c_str()); - - SetOuputFilename(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) - m_pApplicationSpan = new CTimeSpan("Application", true); - - if (m_pApplicationSpan == NULL) - m_pInterface->GetGlobalApplicationLogger()->LogNormal( - "CUserLog::CUserLog, failed to create m_pApplicationSpan!"); - - // TODO: for the load test harness, we apparently need to create the object directly - // without a settings store (which will break CSettingsObserver, etc.); and then, - // don't call the following: - AddInitialParam(); -} - -CUserLog::~CUserLog() { - // CFunctionLogger f1("CUserLog::~CUserLog", g_pLogger); - - if ((m_bSimple) && (m_pSimpleLogger != NULL)) m_pSimpleLogger->LogDebug("stop"); - - if (m_pApplicationSpan != NULL) { - delete m_pApplicationSpan; - m_pApplicationSpan = NULL; - } - - for (unsigned int i = 0; i < m_vpTrials.size(); i++) { - CUserLogTrial* pTrial = (CUserLogTrial*)m_vpTrials[i]; - - if (pTrial != NULL) { - delete pTrial; - pTrial = NULL; - } - } - - for (unsigned int i = 0; i < m_vParams.size(); i++) { - CUserLogParam* pParam = (CUserLogParam*)m_vParams[i]; - - if (pParam != NULL) { - delete pParam; - pParam = NULL; - } - } - - if (m_pSimpleLogger != NULL) { - delete m_pSimpleLogger; - m_pSimpleLogger = NULL; - } - - if (m_pCycleTimer != NULL) { - delete m_pCycleTimer; - m_pCycleTimer = NULL; - } - - m_pSettingsStore->OnParameterChanged.Unsubscribe(this); -} - -// Do initialization of member variables based on the user log level mask -void CUserLog::InitUsingMask(int iLogLevelMask) { - // CFunctionLogger f1("CUserLog::InitUsingMask", g_pLogger); - - m_bInitIsDone = false; - - if (iLogLevelMask & userLogSimple) { - // First we check to see if the file exists, if it does not - // then we want to force all parameter values to be sent to - // the log file even before InitIsDone() is called. - FILE* fp = fopen(USER_LOG_SIMPLE_FILENAME.c_str(), "r"); - if (fp == NULL) { - m_bInitIsDone = true; - } else { - fclose(fp); - fp = NULL; - } - - m_bSimple = true; - - if (m_pSimpleLogger == NULL) - m_pSimpleLogger = - new CFileLogger(USER_LOG_SIMPLE_FILENAME, eLogLevel::logDEBUG, logTimeStamp | logDateStamp); - } - - if (iLogLevelMask & userLogDetailed) m_bDetailed = true; -} - -// Called when we want to output the log file (usually on exit of dasher) -void CUserLog::OutputFile() { - // CFunctionLogger f1("CUserLog::OutputFile", g_pLogger); - - if (m_bDetailed) { - // Let the last pTrial object know we are done with it, this lets it do - // any final calculations. - if (m_vpTrials.size() > 0) { - CUserLogTrial* pTrial = m_vpTrials[m_vpTrials.size() - 1]; - - if (pTrial != NULL) pTrial->Done(); - } - - // Output our data to an XML file before we destruct - WriteXML(); - } -} - -void CUserLog::StartWriting() { - // CFunctionLogger f1("CUserLog::StartWriting", g_pLogger); - - if (m_bSimple) { - // The canvas size changes multiple times as a user resizes it. We just want to write - // one short log entry for the final position the next time they start writing. - if ((m_bNeedToWriteCanvas) && (m_pSimpleLogger != NULL)) { - m_pSimpleLogger->LogDebug("canvas:\t%d\t%d\t%d\t%d", m_sCanvasCoordinates.top, m_sCanvasCoordinates.left, - m_sCanvasCoordinates.bottom, m_sCanvasCoordinates.right); - m_bNeedToWriteCanvas = false; - } - - // We log what happened between StartWriting() and StopWriting() - // so clear out any previous history. - ResetCycle(); - } - - if (m_bDetailed) { - CUserLogTrial* pTrial = GetCurrentTrial(); - - // This could be the first use in this pTrial, create a new one if needed - if (pTrial == NULL) pTrial = AddTrial(); - - if (pTrial != NULL) - pTrial->StartWriting(); - else - m_pInterface->GetGlobalApplicationLogger()->LogNormal( - "CUserLog::StartWriting, failed to create new pTrial!"); - } - - m_bIsWriting = true; -} - -// This version should be called at the end of navigation with the dNats -// value under the current mouse position. This would be more accurate -// then the last value from a mouse event since some time may have -// elapsed. -void CUserLog::StopWriting(float dNats) { - // CFunctionLogger f1("CUserLog::StopWriting", g_pLogger); - - if (m_bIsWriting) { - m_dCycleNats = (double)dNats; - StopWriting(); - } -} - -void CUserLog::StopWriting() { - // CFunctionLogger f1("CUserLog::StopWriting", g_pLogger); - - if (m_bIsWriting) { - m_bIsWriting = false; - - // In simple logging mode, we'll output the stats for this navigation cycle - if ((m_bSimple) && (m_pSimpleLogger != NULL)) m_pSimpleLogger->LogDebug("%s", GetStartStopCycleStats().c_str()); - - if (m_bDetailed) { - CUserLogTrial* pTrial = GetCurrentTrial(); - - if (pTrial == NULL) { - m_pInterface->GetGlobalApplicationLogger()->LogNormal("CUserLog::StopWriting, pTrial was NULL!"); - return; - } - - pTrial->StopWriting(GetCycleBits()); - } - } -} - -void CUserLog::AddSymbols(Dasher::VECTOR_SYMBOL_PROB* vpNewSymbols, eUserLogEventType iEvent) { - // CFunctionLogger f1("CUserLog::AddSymbols", g_pLogger); - - if (!m_bIsWriting) { - // StartWriting() wasn't called, so we'll do it implicitly now - m_pInterface->GetGlobalApplicationLogger()->LogDebug("CUserLog::AddSymbols, StartWriting() not called?"); - StartWriting(); - } - - if (vpNewSymbols == NULL) { - m_pInterface->GetGlobalApplicationLogger()->LogNormal("CUserLog::AddSymbols, vpNewSymbols was NULL!"); - return; - } - - if (m_bSimple) { - // Also store a copy in a vector that gets cleared - // time StartWriting() is called. - m_vCycleHistory.insert(m_vCycleHistory.end(), vpNewSymbols->begin(), vpNewSymbols->end()); - } - - if (m_bDetailed) { - CUserLogTrial* pTrial = GetCurrentTrial(); - - // We should have a pTrial object since StartWriting() should have been called before us - if (pTrial == NULL) { - m_pInterface->GetGlobalApplicationLogger()->LogNormal("CUserLog::AddSymbols, pTrial was NULL!"); - return; - } - - pTrial->AddSymbols(vpNewSymbols, iEvent); - } -} - -void CUserLog::DeleteSymbols(int iNumToDelete, eUserLogEventType iEvent) { - // CFunctionLogger f1("CUserLog::DeleteSymbols", g_pLogger); - - if (iNumToDelete <= 0) return; - - if (!m_bIsWriting) { - // StartWriting() wasn't called, so we'll do it implicitly now - m_pInterface->GetGlobalApplicationLogger()->LogDebug("CUserLog::DeleteSymbols, StartWriting() not called?"); - StartWriting(); - } - - if (m_bSimple) { - m_iCycleNumDeletes += iNumToDelete; - - // Be careful not to pop more things than we have (this will hork the - // memory up on linux but not windows). - int iActualNumToDelete = std::min((int)m_vCycleHistory.size(), iNumToDelete); - for (int i = 0; i < iActualNumToDelete; i++) - m_vCycleHistory.pop_back(); - } - - if (m_bDetailed) { - CUserLogTrial* pTrial = GetCurrentTrial(); - - // We should have a pTrial object since StartWriting() should have been called before us - if (pTrial == NULL) { - m_pInterface->GetGlobalApplicationLogger()->LogNormal("CUserLog::DeleteSymbols, pTrial was NULL!"); - return; - } - - pTrial->DeleteSymbols(iNumToDelete, iEvent); - } -} - -void CUserLog::NewTrial() { - // CFunctionLogger f1("CUserLog::NewTrial", g_pLogger); - - if (m_bIsWriting) { - // We should have called StopWriting(), but we'll do it here implicitly - m_pInterface->GetGlobalApplicationLogger()->LogDebug("CUserLog::NewTrial, StopWriting() not called?"); - StopWriting(); - } - - // For safety we can dump the XML to file after each pTrial is done. This - // might be a good idea for long user pTrial sessions just in case Dasher - // were to do something completely crazy like crash. - if (USER_LOG_DUMP_AFTER_TRIAL) WriteXML(); - - if (m_bDetailed) { - CUserLogTrial* pTrial = GetCurrentTrial(); - - if (pTrial == NULL) { - // Not an error, they may just hit new doc before anything else at start - return; - } - - if (pTrial->HasWritingOccured()) { - // Create a new pTrial if the existing one has already been used - AddTrial(); - } - } -} - -// Overloaded version that converts a double to a string -void CUserLog::AddParam(const std::string& strName, double dValue, int iOptionMask) { - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "%0.4f", dValue); - AddParam(strName, m_szTempBuffer, iOptionMask); -} - -// Overloaded version that converts a int to a string -void CUserLog::AddParam(const std::string& strName, int iValue, int iOptionMask) { - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "%d", iValue); - AddParam(strName, m_szTempBuffer, iOptionMask); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) -} - -// Adds a general parameter to our XML. This lets various Dasher components -// record what they are set at. Optionally can be set to track multiple -// values for the same parameter or to always output a line to the simple -// log file when the parameter is set. -void CUserLog::AddParam(const std::string& strName, const std::string& strValue, int iOptionMask) { - // CFunctionLogger f1("CUserLog::AddParam", g_pLogger); - - bool bOutputToSimple = false; - bool bTrackMultiple = false; - bool bTrackInTrial = false; - bool bForceInTrial = false; - (void)bForceInTrial; - bool bShortInCycle = false; - - if (iOptionMask & userLogParamOutputToSimple) bOutputToSimple = true; - if (iOptionMask & userLogParamTrackMultiple) bTrackMultiple = true; - if (iOptionMask & userLogParamTrackInTrial) bTrackInTrial = true; - if (iOptionMask & userLogParamShortInCycle) bShortInCycle = true; - - // See if we want to immediately output this name/value pair to - // the running simple log file. If we are tracking the parameter - // in the short cycle stats line, then don't output here. - if ((bOutputToSimple) && (m_bSimple) && (m_pSimpleLogger != NULL) && (m_bInitIsDone) && (!bShortInCycle)) { - m_pSimpleLogger->LogNormal("%s = %s", strName.c_str(), strValue.c_str()); - } - - // See if this matches an existing parameter value that we may want to - // overwrite. But only if we aren't suppose to keep track of multiple changes. - if (!bTrackMultiple) { - for (unsigned int i = 0; i < m_vParams.size(); i++) { - CUserLogParam* pParam = (CUserLogParam*)m_vParams[i]; - - if (pParam != NULL) { - if (pParam->strName.compare(strName) == 0) { - pParam->strValue = strValue; - return; - } - } - } - } - - // We need to add a new param - CUserLogParam* pNewParam = new CUserLogParam; - - if (pNewParam == NULL) { - m_pInterface->GetGlobalApplicationLogger()->LogNormal( - "CUserLog::AddParam, failed to create CUserLogParam object!"); - return; - } - - pNewParam->strName = strName; - pNewParam->strValue = strValue; - pNewParam->strTimeStamp = ""; - pNewParam->options = iOptionMask; - - // Parameters that can have multiple values logged will also log when they were changed - if (bTrackMultiple) pNewParam->strTimeStamp = CTimeSpan::GetTimeStamp(); - - m_vParams.push_back(pNewParam); - - if ((bTrackInTrial) && (m_bDetailed)) { - // See if we need to pass the parameter onto the current pTrial object - CUserLogTrial* pTrial = GetCurrentTrial(); - - if (pTrial != NULL) pTrial->AddParam(strName, strValue, iOptionMask); - } -} - -// Adds a new point in our tracking of mouse locations -void CUserLog::AddMouseLocation(int iX, int iY, float dNats) { - // CFunctionLogger f1("CUserLog::AddMouseLocation", g_pLogger); - - // Check to see if it is time to actually push a mouse location update - if (UpdateMouseLocation()) { - if (m_bDetailed) { - CUserLogTrial* pTrial = GetCurrentTrial(); - - if (pTrial == NULL) { - // Only track during an actual pTrial - return; - } - - // Only record mouse locations during navigation - if (pTrial->IsWriting()) pTrial->AddMouseLocation(iX, iY, dNats); - } - - // Keep track of the dNats for the current mouse position - if (m_bIsWriting) m_dCycleNats = dNats; - } -} - -// Adds the size of the current window -void CUserLog::AddWindowSize(int iTop, int iLeft, int iBottom, int iRight) { - // CFunctionLogger f1("CUserLog::AddWindowSize", g_pLogger); - - m_sWindowCoordinates.top = iTop; - m_sWindowCoordinates.left = iLeft; - m_sWindowCoordinates.bottom = iBottom; - m_sWindowCoordinates.right = iRight; - - if (m_bDetailed) { - CUserLogTrial* pTrial = GetCurrentTrial(); - - if (pTrial == NULL) { - // Only track during an actual pTrial - return; - } - - pTrial->AddWindowSize(iTop, iLeft, iBottom, iRight); - } -} - -// Adds the size of the current canvas, this should be called when our -// window is initially created and whenever somebody mucks with the -// size. -void CUserLog::AddCanvasSize(int iTop, int iLeft, int iBottom, int iRight) { - // CFunctionLogger f1("CUserLog::AddCanvasSize", g_pLogger); - - // Only log to simple log object if the coordinates are different from - // what we had prior to now. - if ((m_bSimple) && (m_pSimpleLogger != NULL) && - ((m_sCanvasCoordinates.top != iTop) || (m_sCanvasCoordinates.left != iLeft) || - (m_sCanvasCoordinates.bottom != iBottom) || (m_sCanvasCoordinates.right != iRight))) - m_bNeedToWriteCanvas = true; - - m_sCanvasCoordinates.top = iTop; - m_sCanvasCoordinates.left = iLeft; - m_sCanvasCoordinates.bottom = iBottom; - m_sCanvasCoordinates.right = iRight; - - if (m_bDetailed) { - CUserLogTrial* pTrial = GetCurrentTrial(); - - if (pTrial == NULL) { - // Only track during an actual pTrial - return; - } - - pTrial->AddCanvasSize(iTop, iLeft, iBottom, iRight); - } -} - -// We may want to use a noramlized version of mouse coordinates, this way it is -// invariant to changes in the window size before, during, or after navigation. -// The caller must send us both the x, y coordinates and the current window size. -void CUserLog::AddMouseLocationNormalized(int iX, int iY, bool bStoreIntegerRep, float dNats) { - // CFunctionLogger f1("CUserLog::AddMouseLocationNormalized", g_pLogger); - - // Check to see if it is time to actually push a mouse location update - if (UpdateMouseLocation()) { - if ((m_sCanvasCoordinates.bottom == 0) && (m_sCanvasCoordinates.left == 0) && - (m_sCanvasCoordinates.right == 0) && (m_sCanvasCoordinates.top == 0)) { - m_pInterface->GetGlobalApplicationLogger()->LogNormal( - "CUserLog::AddMouseLocationNormalized, called before AddCanvasSize()?"); - return; - } - - ComputeSimpleMousePos(iX, iY); - - if (m_bDetailed) { - CUserLogTrial* pTrial = GetCurrentTrial(); - - if (pTrial == NULL) { - // Only track during an actual pTrial - return; - } - - // Only record mouse locations during navigation - if (pTrial->IsWriting()) pTrial->AddMouseLocationNormalized(iX, iY, bStoreIntegerRep, dNats); - } - - // Keep track of the dNats for the current mouse position - if (m_bIsWriting) m_dCycleNats = dNats; - } -} - -// For simple logging, we don't want to log the same parameters settings -// in the file every single time Dasher starts up. So we require that -// this method be called once the initial loading of parameters is -// complete. This way only changes during a session are logged (we can -// also force logging of the parameter setting when the log file is -// created by setting m_bInitIsDone to true in the constructor). -void CUserLog::InitIsDone() { - // CFunctionLogger f1("CUserLog::InitIsDone", g_pLogger); - - m_bInitIsDone = true; -} - -// Sets our output filename based on the current date and time. -// Or if a parameter is passed in, use that as the output name. -void CUserLog::SetOuputFilename(const std::string& strFilename) { - // CFunctionLogger f1("CUserLog::SetOuputFilename", g_pLogger); - - if (strFilename.length() > 0) { - m_strFilename = strFilename; - } else { - m_strFilename = USER_LOG_DETAILED_PREFIX; - char* szTimeLine = NULL; - time_t t; - - t = time(NULL); - szTimeLine = ctime(&t); - - if ((szTimeLine != NULL) && (strlen(szTimeLine) > 18)) { - for (int i = 4; i < 19; i++) { - if (szTimeLine[i] == ' ') - m_strFilename += "_"; - else if (szTimeLine[i] != ':') - m_strFilename += szTimeLine[i]; - } - } - - m_strFilename += ".xml"; - } - - // Make sure we store a fully qualified form, to prevent movent - // if the working directory changes - m_strFilename = Dasher::FileUtils::GetFullFilenamePath(m_strFilename); -} - -// Find out what level mask this object was created with -int CUserLog::GetLogLevelMask() { - // CFunctionLogger f1("CUserLog::GetLogLevelMask", g_pLogger); - - return m_iLevelMask; -} - -void CUserLog::KeyDown(Dasher::Keys::VirtualKey Key, int iType, int iEffect) { - CUserLogTrial* pTrial = GetCurrentTrial(); - - if (pTrial) pTrial->AddKeyDown(Key, iType, iEffect); -} - -////////////////////////////////////////// private methods //////////////////////////////////////////////// - -// Just inits all our member variables, called by the constructors -void CUserLog::InitMemberVars() { - // CFunctionLogger f1("CUserLog::InitMemberVars", g_pLogger); - - m_strFilename = ""; - m_pApplicationSpan = NULL; - m_dLastMouseUpdate = steady_clock::now(); - m_bSimple = false; - m_bDetailed = false; - m_pSimpleLogger = NULL; - m_bIsWriting = false; - m_bInitIsDone = false; - m_bNeedToWriteCanvas = false; - - m_pCycleTimer = NULL; - m_iCycleNumDeletes = 0; - m_iCycleMouseCount = 0; - m_dCycleMouseNormXSum = 0.0; - m_dCycleMouseNormYSum = 0.0; - m_dCycleNats = 0.0; - - m_sCanvasCoordinates.bottom = 0; - m_sCanvasCoordinates.top = 0; - m_sCanvasCoordinates.right = 0; - m_sCanvasCoordinates.left = 0; - - m_sWindowCoordinates.bottom = 0; - m_sWindowCoordinates.top = 0; - m_sWindowCoordinates.right = 0; - m_sWindowCoordinates.left = 0; - - // We want to use a fully qualified path so that we always - // look in the same spot, regardless of if the working - // directory has moved during runtime. - m_strCurrentTrialFilename = Dasher::FileUtils::GetFullFilenamePath(USER_LOG_CURRENT_TRIAL_FILENAME); -} - -// Write this objects XML out -bool CUserLog::WriteXML() { - std::fstream fout(m_strFilename.c_str(), std::ios::trunc | std::ios::out); - fout << GetXML(); - fout.close(); - - return true; -} - -// Serializes our data to XML -std::string CUserLog::GetXML() { - // CFunctionLogger f1("CUserLog::GetXML", g_pLogger); - - std::string strResult = ""; - strResult.reserve(USER_LOG_DEFAULT_SIZE_TRIAL_XML * (m_vpTrials.size() + 1)); - - strResult += "\n"; - - strResult += "\n"; - if (m_pApplicationSpan != NULL) strResult += m_pApplicationSpan->GetXML("\t"); - - strResult += "\t\n"; - strResult += GetParamsXML(); - strResult += "\t\n"; - - strResult += "\t\n"; - for (unsigned int i = 0; i < m_vpTrials.size(); i++) { - CUserLogTrial* pTrial = (CUserLogTrial*)m_vpTrials[i]; - - // Only log trials that actually had some writing in it - if ((pTrial != NULL) && (pTrial->HasWritingOccured())) { - strResult += pTrial->GetXML("\t\t"); - } - } - strResult += "\t\n"; - - strResult += "\n"; - - return strResult; -} - -// Returns pointer to the current user pTrial, NULL if we don't have one yet -CUserLogTrial* CUserLog::GetCurrentTrial() { - // CFunctionLogger f1("CUserLog::GetCurrentTrial", g_pLogger); - - if (m_vpTrials.size() <= 0) return NULL; - return m_vpTrials[m_vpTrials.size() - 1]; -} - -// Creates a new pTrial, adds to our vector and returns the pointer -CUserLogTrial* CUserLog::AddTrial() { - // CFunctionLogger f1("CUserLog::AddTrial", g_pLogger); - - // Let the last pTrial object know we are done with it - if (m_vpTrials.size() > 0) { - CUserLogTrial* pTrial = m_vpTrials[m_vpTrials.size() - 1]; - - if (pTrial != NULL) pTrial->Done(); - } - - CUserLogTrial* pTrial = new CUserLogTrial(m_strCurrentTrialFilename, m_pInterface->GetGlobalApplicationLogger()); - if (pTrial != NULL) { - m_vpTrials.push_back(pTrial); - PrepareNewTrial(); - } else - m_pInterface->GetGlobalApplicationLogger()->LogNormal( - "CUserLog::AddTrial, failed to create CUserLogTrialSpeech!"); - - return pTrial; -} - -// See if the specified number of milliseconds has elapsed since the last mouse location update -bool CUserLog::UpdateMouseLocation() { - // CFunctionLogger f1("CUserLog::UpdateMouseLocation", g_pLogger); - const auto current_time = steady_clock::now(); - double d_time = duration_cast>(current_time - m_dLastMouseUpdate).count(); // delta time in seconds - if ((d_time * 1000) > LOG_MOUSE_EVERY_MS) { - m_dLastMouseUpdate = current_time; - return true; - } - return false; -} - -// Calculate how many bits entered in the last Start/Stop cycle -double CUserLog::GetCycleBits() { - // CFunctionLogger f1("CUserLog::GetCycleBits", g_pLogger); - - return m_dCycleNats / log(2.0); -} - -// For lightweight logging, we want a string that represents the critical -// stats for what happened between start and stop -std::string CUserLog::GetStartStopCycleStats() { - // CFunctionLogger f1("CUserLog::GetStartStopCycleStats", g_pLogger); - - std::string strResult = ""; - - double dNormX = 0.0; - double dNormY = 0.0; - if (m_iCycleMouseCount > 0) { - dNormX = m_dCycleMouseNormXSum / (double)m_iCycleMouseCount, - dNormY = m_dCycleMouseNormYSum / (double)m_iCycleMouseCount; - } - - if (m_pCycleTimer == NULL) { - m_pInterface->GetGlobalApplicationLogger()->LogNormal( - "CUserLog::GetStartStopCycleStats, cycle timer was NULL!"); - return ""; - } - - // Tab delimited fields are: - // elapsed time, symbols written, bits written, symbols deleted, - // avg normalized x mouse coordinate, avg normalized y mouse - // coordinate, (any parameters marked to be put in cycle stats) - // - // tsbdxym stands for: time symbols bits deletes x y maxbitrate - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "tsbdxym:\t%0.3f\t%zu\t%0.6f\t%d\t%0.3f\t%0.3f%s", - m_pCycleTimer->GetElapsed(), m_vCycleHistory.size(), GetCycleBits(), m_iCycleNumDeletes, dNormX, dNormY, - GetCycleParamStats().c_str()); - strResult = m_szTempBuffer; - - return strResult; -} - -// Helper that computes update of the simple logging's mouse -// position tracking member variables. -void CUserLog::ComputeSimpleMousePos(int iX, int iY) { - // CFunctionLogger f1("CUserLog::ComputeSimpleMousePos", g_pLogger); - - if ((m_bSimple) && (m_bIsWriting)) { - // We keep a running sum of normalized X, Y coordinates - // for use in the simple log file. - m_dCycleMouseNormXSum += - CUserLocation::ComputeNormalizedX(iX, m_sCanvasCoordinates.left, m_sCanvasCoordinates.right); - - m_dCycleMouseNormYSum += - CUserLocation::ComputeNormalizedY(iY, m_sCanvasCoordinates.top, m_sCanvasCoordinates.bottom); - m_iCycleMouseCount++; - } -} - -// Resets member variables that track a cycle for simple logging -void CUserLog::ResetCycle() { - // CFunctionLogger f1("CUserLog::ResetCycle", g_pLogger); - - m_vCycleHistory.clear(); - m_iCycleNumDeletes = 0; - m_iCycleMouseCount = 0; - m_dCycleMouseNormXSum = 0.0; - m_dCycleMouseNormYSum = 0.0; - - if (m_pCycleTimer != NULL) { - delete m_pCycleTimer; - m_pCycleTimer = NULL; - } - - m_pCycleTimer = new CSimpleTimer(); -} - -// Gets the XML that goes in the tag, but not the tags themselves. -std::string CUserLog::GetParamsXML() { - // CFunctionLogger f1("CUserLog::GetParamsXML", g_pLogger); - - std::string strResult = ""; - - // Make parameters with the same name appear near each other in the results - sort(m_vParams.begin(), m_vParams.end(), CUserLogParam::ComparePtr); - - for (unsigned int i = 0; i < m_vParams.size(); i++) { - CUserLogParam* pParam = (CUserLogParam*)m_vParams[i]; - - strResult += CUserLogTrial::GetParamXML(pParam, "\t\t"); - } - - return strResult; -} - -// Prepares a new pTrial for use. Passes on the current canvas and window -// size so normalized mouse coordinates can be calculated. Also -// parameters can be marked to force them into the Trial object. Looks for -// these and push into the current Trial object. -void CUserLog::PrepareNewTrial() { - // CFunctionLogger f1("CUserLog::PrepareNewTrial", g_pLogger); - - CUserLogTrial* pTrial = GetCurrentTrial(); - - if (pTrial != NULL) { - // We want to force the current value of any parameters that we marked - // with the userLogParamForceInTrial option when created. We can - // do this by going backwards through the parameter vector and only - // pushing through the first value of a given parameter name. - VECTOR_STRING vFound; - if (m_vParams.size() > 0) { - for (VECTOR_USER_LOG_PARAM_PTR_REV_ITER iter = m_vParams.rbegin(); iter != m_vParams.rend(); ++iter) { - if (((*iter) != NULL) && ((*iter)->options & userLogParamForceInTrial)) { - // Make sure we haven't output this one already - VECTOR_STRING_ITER strIter; - strIter = find(vFound.begin(), vFound.end(), (*iter)->strName); - if (strIter == vFound.end()) { - pTrial->AddParam((*iter)->strName, (*iter)->strValue, (*iter)->options); - vFound.push_back((*iter)->strName); - } - } - } - } - - // Make sure the pTrial has the current canvas and window coordinate sizes - pTrial->AddCanvasSize(m_sCanvasCoordinates.top, m_sCanvasCoordinates.left, m_sCanvasCoordinates.bottom, - m_sCanvasCoordinates.right); - - pTrial->AddWindowSize(m_sWindowCoordinates.top, m_sWindowCoordinates.left, m_sWindowCoordinates.bottom, - m_sWindowCoordinates.right); - - } else - m_pInterface->GetGlobalApplicationLogger()->LogNormal( - "CUserLog::PrepareNewTrial, failed to create CUserLogTrial"); -} - -// Parameters can be marked to always end them at the cycle stats in short logging. -// We'll look through our parameters and return a tab delimited list of their -// values. -std::string CUserLog::GetCycleParamStats() { - // CFunctionLogger f1("CUserLog::GetCycleParamStats", g_pLogger); - - std::string strResult = ""; - VECTOR_STRING vFound; - - if (m_vParams.size() <= 0) return strResult; - - // We may have more than one parameter that needs to be added and we want - // the stats line to be invariant to the order in which AddParam() was - // called. So we'll sort by param name and then by time stamp (for - // parameters with multiple values). - sort(m_vParams.begin(), m_vParams.end(), CUserLogParam::ComparePtr); - - // Find the last instance of any parameter marked as needed to be on - // the cycle stats line. - for (VECTOR_USER_LOG_PARAM_PTR_REV_ITER iter = m_vParams.rbegin(); iter != m_vParams.rend(); ++iter) { - if (((*iter) != NULL) && ((*iter)->options & userLogParamShortInCycle)) { - // Make sure we haven't output this one already - VECTOR_STRING_ITER strIter; - strIter = find(vFound.begin(), vFound.end(), (*iter)->strName); - if (strIter == vFound.end()) { - strResult += "\t"; - strResult += (*iter)->strValue; - vFound.push_back((*iter)->strName); - } - } - } - - return strResult; -} - -// Return a string with the operating system and product version -std::string CUserLog::GetVersionInfo() { - // CFunctionLogger f1("CUserLog::GetVersionInfo", g_pLogger); - - std::string strResult = ""; -#ifdef _WIN32 - strResult += "win "; - - // TBD: getting version from resource is quite tricky and requires linking in - // a whole library to do. Maybe we can just #DEFINE the product version? -#else - strResult += "not win "; -#endif - - return strResult; -} - -// Forces all the parameters we are tracking to be intially set, used when the -// object is first starting up. -void CUserLog::AddInitialParam() { - for (auto [key, mask] : s_UserLogParamMaskTable) { - UpdateParam(key, mask); - } -} - -// Helper method that takes a parameter ID a la Parameters.h and -// looks up its type, name and value and pushes into our object -// using the specified options mask. -void CUserLog::UpdateParam(Parameter parameter, int iOptionMask) { - std::string strParamName = GetParameterName(parameter); - - // What method we call depends on the type of the parameter - switch (GetParameterType(parameter)) { - case (PARAM_BOOL): { - // Convert bool to a integer - int iValue = 0; - if (m_pSettingsStore->GetBoolParameter(parameter)) iValue = 1; - AddParam(strParamName, iValue, iOptionMask); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) - return; - break; - } - case (PARAM_LONG): { - AddParam(strParamName, (int)m_pSettingsStore->GetLongParameter(parameter), - iOptionMask); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) - return; - break; - } - case (PARAM_STRING): { - AddParam(strParamName, m_pSettingsStore->GetStringParameter(parameter), - iOptionMask); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) - return; - break; - } - default: { - m_pInterface->GetGlobalApplicationLogger()->LogNormal( - "CUserLog::UpdateParam, matched parameter %d but unknown type %d", parameter, GetParameterType(parameter)); - break; - } - }; -} \ No newline at end of file diff --git a/src/DasherCore/UserLog.h b/src/DasherCore/UserLog.h deleted file mode 100644 index b61266e6b..000000000 --- a/src/DasherCore/UserLog.h +++ /dev/null @@ -1,155 +0,0 @@ - -// Handles logging of user activities such as what they write, how -// long they were writing, mouse positions, parameters, etc -// -// Two different kinds of logging can be produced: -// 1) Running stats file that records basic stats about -// how much and how fast a user is writing. -// 2) Detailed per session log file for use during -// user trials. -// -// If detailed mode isn't enabled, calls should stop here in this object -// and not go on to create UserLogTrial objects or anything that isn't -// strictly needed to do the simple logging. -// -// For normal dasher, a new trial involves this sequence: -// 1) Program start or new doc -// 2) Nav start -// 3) Nav stop -// 4) Optionally goto 2 -// A new trial must be indicated by the user resetting using new doc event. -// Time from 2-4 is elapsed trial time. -// -// Copyright 2005 by Keith Vertanen - -#pragma once - -#include "FileLogger.h" -#include -#include -#include -#include "SimpleTimer.h" -#include "TimeSpan.h" -#include "UserLogTrial.h" -#include "UserLogParam.h" -#include "UserLogBase.h" -#include "XMLUtil.h" -#include "SettingsStore.h" -#include "DasherInterfaceBase.h" - -const int USER_LOG_DEFAULT_SIZE_TRIAL_XML = - 65536; // How big we think the XML string representing a single trial will be -const int LOG_MOUSE_EVERY_MS = 200; // How often to log the mouse position (-1 for never), the frequency is also depends - // on how often the WM_TIMER event fires in dasher - -static const std::string USER_LOG_SIMPLE_FILENAME = "dasher_usage.log"; // Filename of the short text log file -static const std::string USER_LOG_DETAILED_PREFIX = "dasher_"; // Prefix of the detailed XML log files -static const bool USER_LOG_DUMP_AFTER_TRIAL = true; // Do we want to dump the XML after each trial is complete? -static const std::string USER_LOG_CURRENT_TRIAL_FILENAME = - "CurrentTrial.xml"; // Filename we look for information on what the subject is doing - -enum eUserLogLevel { - userLogSimple = 1, // Simple running log file - userLogDetailed = 2 // Detailed per session user trial style -}; - -#ifndef VECTOR_STRING -typedef std::vector VECTOR_STRING; -#endif -#ifndef VECTOR_STRING_ITER -typedef std::vector::iterator VECTOR_STRING_ITER; -#endif -#ifndef VECTOR_VECTOR_STRING -typedef std::vector VECTOR_VECTOR_STRING; -#endif -#ifndef VECTOR_VECTOR_STRING_ITER -typedef std::vector::iterator VECTOR_VECTOR_STRING_ITER; -#endif - -/// \ingroup Logging -/// @{ - -// We need to be notified when parameters we are logging get changed, but UserLogBase -// is already watching BP_DASHER_PAUSED -class CUserLog : public CUserLogBase { - public: - CUserLog(Dasher::CSettingsStore* pSettingsStore, Dasher::CDasherInterfaceBase* pInterface, int iLogTypeMask); - - ~CUserLog() override; - - // Methods called whenever our user interface gets a relevant event, this - // object will decide how to put it into its representation. - void AddParam(const std::string& strName, const std::string& strValue, int iOptionMask = 0) override; - void AddParam(const std::string& strName, double dValue, int iOptionMask = 0) override; - void AddParam(const std::string& strName, int iValue, int iOptionMask = 0) override; - void StartWriting() override; - void StopWriting(float dNats) override; - void StopWriting() override; - void AddSymbols(Dasher::VECTOR_SYMBOL_PROB* pVectorNewSymbolProbs, - eUserLogEventType iEvent = userLogEventMouse) override; - void DeleteSymbols(int iNumToDelete, eUserLogEventType iEvent = userLogEventMouse) override; - void NewTrial() override; - - void AddWindowSize(int iTop, int iLeft, int iBottom, int iRight) override; - void AddCanvasSize(int iTop, int iLeft, int iBottom, int iRight) override; - void AddMouseLocation(int iX, int iY, float dNats) override; - void AddMouseLocationNormalized(int iX, int iY, bool bStoreIntegerRep, float dNats) override; - void OutputFile() override; - void InitIsDone() override; - void SetOuputFilename(const std::string& strFilename = "") override; - int GetLogLevelMask() override; - void KeyDown(Dasher::Keys::VirtualKey Key, int iType, int iEffect) override; - - protected: - CTimeSpan* m_pApplicationSpan; // How long the application has been up - std::string m_strFilename; // Name we output our XML file to - VECTOR_USER_LOG_TRIAL_PTR m_vpTrials; // Holds object for each trial in this session - VECTOR_USER_LOG_PARAM_PTR m_vParams; // Stores general parameters we want in the XML - std::chrono::steady_clock::time_point m_dLastMouseUpdate; // When the last mouse update was pushed - bool m_bSimple; // Are we outputting the simple running log file? - bool m_bDetailed; // Are we outputting per session detailed logs? - CFileLogger* m_pSimpleLogger; // Used to log the simple running log file - bool m_bIsWriting; // Has StartWriting() been called but not StopWriting()? - bool m_bInitIsDone; // Set to true once the initialization of default values is done - WindowSize m_sCanvasCoordinates; // The size of our canvas from the last call to AddCanvasSize() - WindowSize m_sWindowCoordinates; // Records the window coordinates at the start of navigation - bool m_bNeedToWriteCanvas; // Do we need to write new canvas coordinates on the next navigation? - int m_iLevelMask; // What log level mask we were created with. - std::string m_strCurrentTrialFilename; // Where info about the current subject's trial is stored - - // Used whenever we need a temporary char* buffer - static const int TEMP_BUFFER_SIZE = 4096; - char m_szTempBuffer[TEMP_BUFFER_SIZE]; - - CUserLogTrial* AddTrial(); - CUserLogTrial* GetCurrentTrial(); - std::string GetXML(); - bool WriteXML(); - bool UpdateMouseLocation(); - std::string GetParamsXML(); - void PrepareNewTrial(); - std::string GetCycleParamStats(); - std::string GetVersionInfo(); - void InitMemberVars(); - void AddInitialParam(); - void UpdateParam(Dasher::Parameter parameter, int iOptionMask); - - // Things that support simple stats of a single Start/Stop cycle: - Dasher::VECTOR_SYMBOL_PROB m_vCycleHistory; // Tracks just the most recent Start/Stop cycle, used for simple logging - unsigned int m_iCycleNumDeletes; // Track number of deletes in last Start/Stop cycle - CSimpleTimer* m_pCycleTimer; // Length of the last Start/Stop cycle - double m_dCycleMouseNormXSum; // Sum of all normalized mouse X coordinates - double m_dCycleMouseNormYSum; // Sum of all normalized mouse Y coordinates - unsigned long m_iCycleMouseCount; // How many mouse updates have been stores - double m_dCycleNats; // The last nats value we got from a mouse event - - std::string GetStartStopCycleStats(); - double GetCycleBits(); - void ComputeSimpleMousePos(int iX, int iY); - void ResetCycle(); - void InitUsingMask(int iLogLevelMask); - - private: - Dasher::CSettingsStore* m_pSettingsStore; -}; -/// @} diff --git a/src/DasherCore/UserLogBase.cpp b/src/DasherCore/UserLogBase.cpp deleted file mode 100644 index 52cb045f6..000000000 --- a/src/DasherCore/UserLogBase.cpp +++ /dev/null @@ -1,46 +0,0 @@ -/* - * UserLogBase.cpp - * Dasher - * - * Created by Alan Lawrence on 28/03/2011. - * Copyright 2011 Cavendish Laboratory. All rights reserved. - * - */ - -#include "UserLogBase.h" -#include "Event.h" -#include "DasherNode.h" -#include "DasherInterfaceBase.h" - -using namespace Dasher; - -CUserLogBase::CUserLogBase(CDasherInterfaceBase* pInterface) : m_pInterface(pInterface), m_iNumDeleted(0) { - m_pInterface->OnEditEvent.Subscribe(this, - [this](CEditEvent::EditEventType type, const std::string&, CDasherNode* node) { - if (type == CEditEvent::EDIT_OUTPUT) { - m_vAdded.push_back(node->GetSymbolProb()); - // output - } else if (type == CEditEvent::EDIT_DELETE) { - // delete - m_iNumDeleted++; - } - }); -}; - -CUserLogBase::~CUserLogBase() { - m_pInterface->OnEditEvent.Unsubscribe(this); -} - -void CUserLogBase::HandleEvent(const CEditEvent* evt) {} - -void CUserLogBase::FrameEnded() { - // pass on added/deleted if any, and get ready for next frame - if (m_iNumDeleted) { - DeleteSymbols(m_iNumDeleted); - m_iNumDeleted = 0; - } - if (!m_vAdded.empty()) { - AddSymbols(&m_vAdded); - m_vAdded.clear(); - } -} diff --git a/src/DasherCore/UserLogBase.h b/src/DasherCore/UserLogBase.h deleted file mode 100644 index 8d4bfb251..000000000 --- a/src/DasherCore/UserLogBase.h +++ /dev/null @@ -1,55 +0,0 @@ -#pragma once - -#include "DasherTypes.h" -#include "UserLogTrial.h" // Don't want to include this, but needed for event type enum -#include "Event.h" - -#include -#include - -namespace Dasher { -class CDasherInterfaceBase; -} - -/// \defgroup Logging Logging routines -/// @{ -class CUserLogBase { - public: - CUserLogBase(Dasher::CDasherInterfaceBase* pInterface); - virtual ~CUserLogBase(); - - virtual void AddParam(const std::string& strName, const std::string& strValue, int iOptionMask = 0) = 0; - virtual void AddParam(const std::string& strName, double dValue, int iOptionMask = 0) = 0; - virtual void AddParam(const std::string& strName, int iValue, int iOptionMask = 0) = 0; - // Called when the user starts moving. Note this happens for _each_ click/zoom - // in Click Mode, Direct Mode, Menu Mode etc. (so should ignore extra calls - // after the first.) - virtual void StartWriting() = 0; - virtual void StopWriting(float dNats) = 0; - virtual void StopWriting() = 0; - virtual void NewTrial() = 0; - virtual void AddWindowSize(int iTop, int iLeft, int iBottom, int iRight) = 0; - virtual void AddCanvasSize(int iTop, int iLeft, int iBottom, int iRight) = 0; - virtual void AddMouseLocation(int iX, int iY, float dNats) = 0; - virtual void AddMouseLocationNormalized(int iX, int iY, bool bStoreIntegerRep, float dNats) = 0; - virtual void OutputFile() = 0; - virtual void InitIsDone() = 0; - virtual void SetOuputFilename(const std::string& strFilename = "") = 0; - virtual int GetLogLevelMask() = 0; - virtual void KeyDown(Dasher::Keys::VirtualKey Key, int iType, int iEffect) = 0; - /// Watches output events to record symbols added/deleted - virtual void HandleEvent(const Dasher::CEditEvent* pEvent); - /// Passes record of symbols added/deleted to AddSymbols/DeleteSymbols - void FrameEnded(); - - protected: - virtual void AddSymbols(Dasher::VECTOR_SYMBOL_PROB* pVectorNewSymbolProbs, - eUserLogEventType iEvent = userLogEventMouse) = 0; - virtual void DeleteSymbols(int iNumToDelete, eUserLogEventType iEvent = userLogEventMouse) = 0; - Dasher::CDasherInterfaceBase* m_pInterface; - - private: - std::vector m_vAdded; - int m_iNumDeleted; -}; -/// @} diff --git a/src/DasherCore/UserLogParam.cpp b/src/DasherCore/UserLogParam.cpp deleted file mode 100644 index 45e52dcd5..000000000 --- a/src/DasherCore/UserLogParam.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "UserLogParam.h" - -// Needed so we can sort() vectors of parameters -bool CUserLogParam::ComparePtr(CUserLogParam* pA, CUserLogParam* pB) { - if ((pA == NULL) || (pB == NULL)) return false; - int iResult = pA->strName.compare(pB->strName); - - if (iResult < 0) - return true; - else if (iResult == 0) { - if (pA->strTimeStamp.compare(pB->strTimeStamp) < 0) return true; - } - - return false; -} diff --git a/src/DasherCore/UserLogParam.h b/src/DasherCore/UserLogParam.h deleted file mode 100644 index 249af6a7b..000000000 --- a/src/DasherCore/UserLogParam.h +++ /dev/null @@ -1,48 +0,0 @@ - -#pragma once - -// Class that stores a particular parameter -// -// Could be a struct, but we want to sort and need to -// overload the < operator. -// -// Copyright 2005 by Keith Vertanen - -#include -#include -#include "Parameters.h" - -// Bit mask options that are sent in when logging parameters -enum eUserLogParam { - userLogParamTrackMultiple = 1, // Should we track multiple values of a given parameter? - userLogParamOutputToSimple = 2, // Does this parameter get sent to the simple log file? - userLogParamTrackInTrial = 4, // Do we also store a copy of the parameter value within a trial? - userLogParamForceInTrial = 8, // Do we always log the value of this parameter when a new trial is created? - userLogParamShortInCycle = 16 // In short logging, does the value get added to the end of a cycle stats line? -}; - -// We need to have a lookup table that maps parameters we want to track in -// the UserLog object and what their behavior is. -struct UserLogParamMask { - Dasher::Parameter key; - int mask; -}; - -class CUserLogParam; - -typedef std::vector VECTOR_USER_LOG_PARAM_PTR; -typedef std::vector::iterator VECTOR_USER_LOG_PARAM_PTR_ITER; -typedef std::vector::reverse_iterator VECTOR_USER_LOG_PARAM_PTR_REV_ITER; - -/// \ingroup Logging -/// @{ -class CUserLogParam { - public: - std::string strName; // Name of the parameter - std::string strValue; // String version of the value - std::string strTimeStamp; // Optional timestamp if we want to know when a parameter was changed - int options; // The options that were used on the parameter - - static bool ComparePtr(CUserLogParam* pA, CUserLogParam* pB); -}; -/// @} diff --git a/src/DasherCore/UserLogTrial.cpp b/src/DasherCore/UserLogTrial.cpp deleted file mode 100644 index 963301fc1..000000000 --- a/src/DasherCore/UserLogTrial.cpp +++ /dev/null @@ -1,1231 +0,0 @@ -#include -#include "UserLogTrial.h" - -#include -#include -#include -#include - -CUserLogTrial::CUserLogTrial(const std::string& strCurrentTrialFilename, CFileLogger* GlobalFileLogger) - : m_strCurrentTrialFilename(strCurrentTrialFilename), m_pGlobalFileLogger(GlobalFileLogger) {} - -CUserLogTrial::~CUserLogTrial() { - - for (unsigned int i = 0; i < m_vpParams.size(); i++) { - CUserLogParam* pParam = (CUserLogParam*)m_vpParams[i]; - - if (pParam != NULL) { - delete pParam; - pParam = NULL; - } - } - - for (unsigned int i = 0; i < m_vpNavCycles.size(); i++) { - NavCycle* pCycle = (NavCycle*)m_vpNavCycles[i]; - - if (pCycle != NULL) { - if (pCycle->pSpan != NULL) { - delete pCycle->pSpan; - pCycle->pSpan = NULL; - } - - for (unsigned int j = 0; j < pCycle->vectorButtons.size(); j++) { - CUserButton* pButton = (CUserButton*)pCycle->vectorButtons[j]; - - if (pButton != NULL) { - delete pButton; - pButton = NULL; - } - } - - for (unsigned int j = 0; j < pCycle->vectorMouseLocations.size(); j++) { - CUserLocation* pLocation = (CUserLocation*)pCycle->vectorMouseLocations[j]; - - if (pLocation != NULL) { - delete pLocation; - pLocation = NULL; - } - } - - for (unsigned int j = 0; j < pCycle->vectorNavLocations.size(); j++) { - NavLocation* pLocation = (NavLocation*)pCycle->vectorNavLocations[j]; - if (pLocation != NULL) { - CTimeSpan* pSpan = pLocation->span; - - if (pSpan != NULL) { - delete pSpan; - pSpan = NULL; - } - - Dasher::VECTOR_SYMBOL_PROB* pVectorAdded = pLocation->pVectorAdded; - if (pVectorAdded != NULL) { - delete pVectorAdded; - pVectorAdded = NULL; - } - - delete pLocation; - pLocation = NULL; - } - } - - delete pCycle; - pCycle = NULL; - } - } - - if (m_pSpan != NULL) { - delete m_pSpan; - m_pSpan = NULL; - } -} - -// Returns an XML version of all our information -std::string CUserLogTrial::GetXML(const std::string& strPrefix) { - - std::string strResult = ""; - - std::string strPrefixTab = strPrefix; - strPrefixTab += "\t"; - - std::string strPrefixTabTab = strPrefixTab; - strPrefixTabTab += "\t"; - - strResult += strPrefix; - strResult += "\n"; - - // Summarize what happened at the beginning of the block - strResult += GetSummaryXML(strPrefix); - - // Parameters that we want tracked on a per trial basis - strResult += GetParamsXML(strPrefix); - - // Size of the window and the canvas in screen coordniates - strResult += GetWindowCanvasXML(strPrefix); - - strResult += m_strCurrentTrial; - - // All the start and stop navigation events - strResult += GetNavCyclesXML(strPrefix); - - strResult += strPrefix; - strResult += "\n"; - - return strResult; -} - -// See if any navigation has occured during this trial yet -bool CUserLogTrial::HasWritingOccured() { - - if (m_vpNavCycles.size() > 0) return true; - return false; -} - -void CUserLogTrial::StartWriting() { - - if (m_bWritingStart) { - m_pGlobalFileLogger->LogNormal("CUserLogTrial::StartWriting, nav already marked as started!"); - return; - } - - // Make sure our trial time span is running - if (m_pSpan != NULL) m_pSpan->Continue(); - - // Start the task timer if we haven't already done so - if (m_pSpan == NULL) m_pSpan = new CTimeSpan("Time", false); - - if (m_pSpan == NULL) { - m_pGlobalFileLogger->LogNormal("CUserLogTrial::StartWriting, m_pSpan was NULL!"); - return; - } - - m_bWritingStart = true; - - // If we have already done some navigation, then the previous NavStop() would have stopped - // the timer in the last NavLocation object. We want to tell it to continue since the - // trial is not in fact over. - if (m_vpNavCycles.size() > 0) { - NavLocation* pLastLocation = GetCurrentNavLocation(); - if ((pLastLocation != NULL) && (pLastLocation->span != NULL)) pLastLocation->span->Continue(); - } - - // NavCycle* newCycle = AddNavCycle(); - AddNavCycle(); -} - -void CUserLogTrial::StopWriting(double dBits) { - - if (!m_bWritingStart) { - m_pGlobalFileLogger->LogNormal("CUserLogTrial::StopWriting, nav already marked as stopped!"); - return; - } - - if (m_vpNavCycles.size() <= 0) { - m_pGlobalFileLogger->LogNormal("CUserLogTrial::StopWriting, vector was empty!"); - return; - } - - NavCycle* pCycle = GetCurrentNavCycle(); - if (pCycle == NULL) { - m_pGlobalFileLogger->LogNormal("CUserLogTrial::StopWriting, current cycle was NULL!"); - return; - } - - CTimeSpan* pSpan = (CTimeSpan*)pCycle->pSpan; - if (pSpan == NULL) { - m_pGlobalFileLogger->LogNormal("CUserLogTrial::StopWriting, span was NULL!"); - return; - } - - pCycle->dBits = dBits; - pSpan->Stop(); - - m_bWritingStart = false; - - // Make sure the last location timer is stopped since this could be the end of the trial and - // we want the timestamps in the location elements to match the total trial time. - NavLocation* pLastLocation = GetCurrentNavLocation(); - if ((pLastLocation != NULL) && (pLastLocation->span != NULL)) pLastLocation->span->Continue(); - - // Could be the last event of the trial - if (m_pSpan != NULL) m_pSpan->Stop(); - - // We want to use the UserTrial info from the navigation period in Dasher. We'll update - // this everytime the user stops, this should make sure we get the right bit. - GetUserTrialInfo(); -} - -// The user has entered one or more new symbols. UserLog object will -// pass us the pointer to the current alphabet that is being used. -void CUserLogTrial::AddSymbols(Dasher::VECTOR_SYMBOL_PROB* vpNewSymbolProbs, eUserLogEventType iEvent) { - // Add the symbols to our running total of symbols. - - // ACL: Old comment said "We track the symbols and not the display text - // since we may need to delete symbols later and - // a given symbol might take up multiple chars." - // - yet stored the display text???? - - // We also keep the probability around so we can - // calculate the average bits of the history. - m_vHistory.insert(m_vHistory.end(), vpNewSymbolProbs->begin(), vpNewSymbolProbs->end()); - - StopPreviousTimer(); - - // Create the new NavLocation struct that record the data about this addition - NavLocation* pLocation = NULL; - pLocation = new NavLocation; - - if (pLocation == NULL) { - m_pGlobalFileLogger->LogNormal("CUserLogTrial::AddSymbols, failed to create location!"); - return; - } - - pLocation->strHistory = GetHistoryDisplay(); - pLocation->span = new CTimeSpan("Time", false); - pLocation->avgBits = GetHistoryAvgBits(); - pLocation->event = iEvent; - pLocation->numDeleted = 0; - pLocation->pVectorAdded = new std::vector(*vpNewSymbolProbs); - - NavCycle* pCycle = GetCurrentNavCycle(); - if (pCycle != NULL) - pCycle->vectorNavLocations.push_back(pLocation); - else - m_pGlobalFileLogger->LogNormal("CUserLogTrial::AddSymbols, cycle was NULL!"); -} - -void CUserLogTrial::DeleteSymbols(int iNumToDelete, eUserLogEventType iEvent) { - - if (iNumToDelete <= 0) return; - - // Be careful not to pop more things than we have (this will hork the - // memory up on linux but not windows). - int iActualNumToDelete = std::min((int)m_vHistory.size(), iNumToDelete); - - for (int i = 0; i < iActualNumToDelete; i++) { - // Remove the request number of symbols from our - // ongoing list. - m_vHistory.pop_back(); - } - - StopPreviousTimer(); - - // Create the new NavLocation struct that record the data about this addition - NavLocation* pLocation = NULL; - pLocation = new NavLocation; - - if (pLocation == NULL) { - m_pGlobalFileLogger->LogNormal("CUserLogTrial::DeleteSymbols, failed to create location!"); - return; - } - - pLocation->strHistory = GetHistoryDisplay(); - pLocation->span = new CTimeSpan("Time", false); - pLocation->avgBits = GetHistoryAvgBits(); - pLocation->event = iEvent; - pLocation->numDeleted = iNumToDelete; - pLocation->pVectorAdded = NULL; - - NavCycle* pCycle = GetCurrentNavCycle(); - if (pCycle != NULL) - pCycle->vectorNavLocations.push_back(pLocation); - else - m_pGlobalFileLogger->LogNormal("CUserLogTrial::DeleteSymbols, cycle was NULL!"); -} - -// Called by UserLog object whenever we move on to the next trial. This lets -// our trial object finalize any timers. -void CUserLogTrial::Done() { - - StopPreviousTimer(); - - // Stop the time span that tracks the total trial time (if not already stopped) - if ((m_pSpan != NULL) && (!m_pSpan->IsStopped())) m_pSpan->Stop(); -} - -void CUserLogTrial::AddMouseLocation(int iX, int iY, float dNats) { - - CUserLocation* pLocation = new CUserLocation(iX, iY, dNats); - - if (pLocation != NULL) { - NavCycle* pCycle = GetCurrentNavCycle(); - - if (pCycle != NULL) - pCycle->vectorMouseLocations.push_back(pLocation); - else { - m_pGlobalFileLogger->LogNormal("CUserLogTrial::AddLocation, cycle was NULL!"); - delete pLocation; - } - } else - m_pGlobalFileLogger->LogNormal("CUserLogTrial::AddLocation, location was NULL!"); -} - -// Adds a normalized version of our mouse coordinates based on the size -// of the window. Can optionally be told to store both representations. -void CUserLogTrial::AddMouseLocationNormalized(int iX, int iY, bool bStoreIntegerRep, float dNats) { - - CUserLocation* pLocation = NULL; - - if ((m_sCanvasCoordinates.bottom == 0) && (m_sCanvasCoordinates.left == 0) && (m_sCanvasCoordinates.right == 0) && - (m_sCanvasCoordinates.top == 0)) - m_pGlobalFileLogger->LogNormal("CUserLogTrial::AddMouseLocationNormalized, called before AddCanvasSize()?"); - - pLocation = new CUserLocation(iX, iY, m_sCanvasCoordinates.top, m_sCanvasCoordinates.left, - m_sCanvasCoordinates.bottom, m_sCanvasCoordinates.right, bStoreIntegerRep, dNats); - - if (pLocation != NULL) { - NavCycle* pCycle = GetCurrentNavCycle(); - - if (pCycle != NULL) - pCycle->vectorMouseLocations.push_back(pLocation); - else { - m_pGlobalFileLogger->LogNormal("CUserLogTrial::AddMouseLocationNormalized, cycle was NULL!"); - delete pLocation; - } - } else - m_pGlobalFileLogger->LogNormal("CUserLogTrial::AddLocation, location was NULL!"); -} - -void CUserLogTrial::AddKeyDown(Dasher::Keys::VirtualKey Key, int iType, int iEffect) { - CUserButton* pButton = new CUserButton(Key, iType, iEffect); - - if (pButton) { - NavCycle* pCycle = GetCurrentNavCycle(); - - if (pCycle) - pCycle->vectorButtons.push_back(pButton); - else { - m_pGlobalFileLogger->LogNormal("CUserLogTrial::AddLocation, cycle was NULL!"); - delete pButton; - } - } else - m_pGlobalFileLogger->LogNormal("CUserLogTrial::AddLocation, location was NULL!"); -} - -// Sets the current window size, this includes area for the menu bar, -// sliders, canvas, etc. -void CUserLogTrial::AddWindowSize(int iTop, int iLeft, int iBottom, int iRight) { - m_sWindowCoordinates.top = iTop; - m_sWindowCoordinates.left = iLeft; - m_sWindowCoordinates.bottom = iBottom; - m_sWindowCoordinates.right = iRight; -} - -// Sets the current canvas size -void CUserLogTrial::AddCanvasSize(int iTop, int iLeft, int iBottom, int iRight) { - m_sCanvasCoordinates.top = iTop; - m_sCanvasCoordinates.left = iLeft; - m_sCanvasCoordinates.bottom = iBottom; - m_sCanvasCoordinates.right = iRight; -} - -// Are we currently navigating? -bool CUserLogTrial::IsWriting() { - return m_bWritingStart; -} - -////////////////////////////////////////// private methods //////////////////////////////////////////////// - -// Obtain information that is being passed in from the UserTrial standalone application. -// This information tell us what the user is actually trying to enter. -void CUserLogTrial::GetUserTrialInfo() { - m_strCurrentTrial = ""; - - if (m_strCurrentTrialFilename.length() > 0) { - // We want ios::nocreate, but not available in .NET 2003, arrgh - std::fstream fin(m_strCurrentTrialFilename.c_str(), std::ios::in); - - // Make sure we successfully opened before we start reading it - if (fin.is_open()) { - while (!fin.eof()) { - fin.getline(m_szTempBuffer, TEMP_BUFFER_SIZE); - if (strlen(m_szTempBuffer) > 0) { - m_strCurrentTrial += "\t\t\t"; - m_strCurrentTrial += m_szTempBuffer; - m_strCurrentTrial += "\n"; - } - } - fin.close(); - } - } -} - -// Returns the concatenation of all our symbol history using -// the display text that the alphabet at the time of the -// symbol being added gave us. -std::string CUserLogTrial::GetHistoryDisplay() { - std::string strResult = ""; - - for (unsigned int i = 0; i < m_vHistory.size(); i++) { - Dasher::SymbolProb sItem = (Dasher::SymbolProb)m_vHistory[i]; - strResult += sItem.strDisplay; - } - - return strResult; -} - -double CUserLogTrial::GetHistoryAvgBits() { - double dResult = 0.0; - - if (m_vHistory.size() > 0) { - for (unsigned int i = 0; i < m_vHistory.size(); i++) { - Dasher::SymbolProb sItem = (Dasher::SymbolProb)m_vHistory[i]; - - dResult += log(sItem.prob); - } - dResult = dResult * -1.0; - dResult = dResult / log(2.0); - dResult = dResult / m_vHistory.size(); - } - - return dResult; -} - -void CUserLogTrial::StopPreviousTimer() { - // Make sure the previous time span (if any) has had its timer stopped - if (m_vpNavCycles.size() > 0) { - NavLocation* pLastLocation = GetCurrentNavLocation(); - if ((pLastLocation != NULL) && (pLastLocation->span != NULL)) pLastLocation->span->Stop(); - } -} - -// Gets XML string for a given NavLocation struct -std::string CUserLogTrial::GetLocationXML(NavLocation* pLocation, const std::string& strPrefix) { - std::string strResult = ""; - if (pLocation == NULL) { - m_pGlobalFileLogger->LogNormal("CUserLogTrial::GetLocationXML, location was NULL!"); - return strResult; - } - - strResult += strPrefix; - strResult += "\n"; - - strResult += strPrefix; - strResult += "\t"; - strResult += pLocation->strHistory; - strResult += "\n"; - - strResult += strPrefix; - strResult += "\t"; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "%0.6f", pLocation->avgBits); - strResult += m_szTempBuffer; - strResult += "\n"; - - // Only output the event if it is interesting type, not normal mouse navigation - if (pLocation->event != userLogEventMouse) { - strResult += strPrefix; - strResult += "\t\t"; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "%d", (int)pLocation->event); - strResult += m_szTempBuffer; - strResult += "\n"; - } - - if ((pLocation->pVectorAdded != NULL) && (pLocation->pVectorAdded->size() > 0)) { - strResult += strPrefix; - strResult += "\t"; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "%zu", pLocation->pVectorAdded->size()); - strResult += m_szTempBuffer; - strResult += "\n"; - - Dasher::VECTOR_SYMBOL_PROB* pVectorAdded = pLocation->pVectorAdded; - - if (pVectorAdded != NULL) { - // Output the details of each add - for (unsigned int j = 0; j < pVectorAdded->size(); j++) { - Dasher::SymbolProb sItem = (Dasher::SymbolProb)(*pVectorAdded)[j]; - - strResult += strPrefix; - strResult += "\t\n"; - - strResult += strPrefix; - strResult += "\t\t"; - strResult += sItem.strDisplay; - strResult += "\n"; - - strResult += strPrefix; - strResult += "\t\t"; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "%0.6f", sItem.prob); - strResult += m_szTempBuffer; - strResult += "\n"; - - strResult += strPrefix; - strResult += "\t\n"; - } - } - } - - if (pLocation->numDeleted > 0) { - strResult += strPrefix; - strResult += "\t"; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "%d", pLocation->numDeleted); - strResult += m_szTempBuffer; - strResult += "\n"; - } - - if (pLocation->span != NULL) { - std::string strPrefixTabTabTab = strPrefix; - strPrefixTabTabTab += "\t"; - - strResult += pLocation->span->GetXML(strPrefixTabTabTab); - } - - strResult += strPrefix; - strResult += "\n"; - - return strResult; -} - -// Output the XML for the summary section of XML -std::string CUserLogTrial::GetSummaryXML(const std::string& strPrefix) { - std::string strResult = ""; - - strResult += strPrefix; - strResult += "\t\n"; - - // Figure out what the user ended up writing and how fast they did it - std::string strText = ""; - double dAvgBits = 0.0; - - NavLocation* pLocation = GetCurrentNavLocation(); - if (pLocation != NULL) { - strText = GetHistoryDisplay(); - dAvgBits = pLocation->avgBits; - } - - int iButtonCount = GetButtonCount(); - double dTotalBits = GetTotalBits(); - - strResult += GetStatsXML(strPrefix, strText, m_pSpan, dAvgBits, iButtonCount, dTotalBits); - - strResult += strPrefix; - strResult += "\t\n"; - - return strResult; -} - -// Calculates the various summary stats we output -std::string CUserLogTrial::GetStatsXML(const std::string& strPrefix, const std::string& strText, CTimeSpan* pSpan, - double dAvgBits, int iButtonCount, double dTotalBits) { - std::string strResult = ""; - - if (pSpan == NULL) { - m_pGlobalFileLogger->LogNormal("CUserLogTrial::GetStatsXML, pSpan = NULL!"); - return strResult; - } - - strResult += strPrefix; - strResult += "\t\t"; - strResult += strText; - strResult += "\n"; - - // Average number of bits along the path to the final string - strResult += strPrefix; - strResult += "\t\t"; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "%0.6f", dAvgBits); - strResult += m_szTempBuffer; - strResult += "\n"; - - strResult += strPrefix; - strResult += "\t\t"; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "%0.6f", dTotalBits); - strResult += m_szTempBuffer; - strResult += "\n"; - - strResult += strPrefix; - strResult += "\t\t"; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "%d", iButtonCount); - strResult += m_szTempBuffer; - strResult += "\n"; - - // Calculate the number of words and characters - strResult += strPrefix; - strResult += "\t\t"; - - // We want the number of symbols which might differ - // from the actual length of the text history. - int iNumChars = static_cast(m_vHistory.size()); - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "%d", iNumChars); - strResult += m_szTempBuffer; - strResult += "\n"; - - strResult += strPrefix; - strResult += "\t\t"; - double dNumWords = (double)iNumChars / (double)5; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "%0.2f", dNumWords); - strResult += m_szTempBuffer; - strResult += "\n"; - - double dWPM = 0.0; - double dCPM = 0.0; - - if (m_pSpan != NULL) { - dWPM = (double)dNumWords / (m_pSpan->GetElapsed() / 60.0); - dCPM = (double)iNumChars / (m_pSpan->GetElapsed() / 60.0); - } - - strResult += strPrefix; - strResult += "\t\t"; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "%0.3f", dWPM); - strResult += m_szTempBuffer; - strResult += "\n"; - - strResult += strPrefix; - strResult += "\t\t"; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "%0.3f", dCPM); - strResult += m_szTempBuffer; - strResult += "\n"; - - std::string strPrefixTabTab = strPrefix; - strPrefixTabTab += "\t\t"; - - if (m_pSpan != NULL) strResult += m_pSpan->GetXML(strPrefixTabTab); - - return strResult; -} - -std::string CUserLogTrial::GetWindowCanvasXML(const std::string& strPrefix) { - std::string strResult = ""; - - // Log the window location and size that was last used during this trial - strResult += strPrefix; - strResult += "\t\n"; - - strResult += strPrefix; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "\t\t%d\n", m_sWindowCoordinates.top); - strResult += m_szTempBuffer; - - strResult += strPrefix; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "\t\t%d\n", m_sWindowCoordinates.bottom); - strResult += m_szTempBuffer; - - strResult += strPrefix; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "\t\t%d\n", m_sWindowCoordinates.left); - strResult += m_szTempBuffer; - - strResult += strPrefix; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "\t\t%d\n", m_sWindowCoordinates.right); - strResult += m_szTempBuffer; - - strResult += strPrefix; - strResult += "\t\n"; - - // Log the canvas location and size that was last used during this trial - strResult += strPrefix; - strResult += "\t\n"; - - strResult += strPrefix; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "\t\t%d\n", m_sCanvasCoordinates.top); - strResult += m_szTempBuffer; - - strResult += strPrefix; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "\t\t%d\n", m_sCanvasCoordinates.bottom); - strResult += m_szTempBuffer; - - strResult += strPrefix; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "\t\t%d\n", m_sCanvasCoordinates.left); - strResult += m_szTempBuffer; - - strResult += strPrefix; - snprintf(m_szTempBuffer, TEMP_BUFFER_SIZE, "\t\t%d\n", m_sCanvasCoordinates.right); - strResult += m_szTempBuffer; - - strResult += strPrefix; - strResult += "\t\n"; - - return strResult; -} - -std::string CUserLogTrial::GetParamsXML(const std::string& strPrefix) { - std::string strResult = ""; - - if (m_vpParams.size() > 0) { - // Make parameters with the same name appear near each other in the results - sort(m_vpParams.begin(), m_vpParams.end(), CUserLogParam::ComparePtr); - - strResult += strPrefix; - strResult += "\t\n"; - - std::string strPrefixPlusTabTab = strPrefix; - strPrefixPlusTabTab += "\t\t"; - - for (unsigned int i = 0; i < m_vpParams.size(); i++) { - CUserLogParam* pParam = (CUserLogParam*)m_vpParams[i]; - - strResult += GetParamXML(pParam, strPrefixPlusTabTab); - } - - strResult += strPrefix; - strResult += "\t\n"; - } - - return strResult; -} - -int CUserLogTrial::GetButtonCount() { - int iCount(0); - - for (VECTOR_NAV_CYCLE_PTR::iterator it(m_vpNavCycles.begin()); it != m_vpNavCycles.end(); ++it) - for (VECTOR_USER_BUTTON_PTR::iterator it2((*it)->vectorButtons.begin()); it2 != (*it)->vectorButtons.end(); - ++it2) - iCount += (*it2)->GetCount(); - - return iCount; -} - -double CUserLogTrial::GetTotalBits() { - double dBits(0.0); - - for (VECTOR_NAV_CYCLE_PTR::iterator it(m_vpNavCycles.begin()); it != m_vpNavCycles.end(); ++it) - dBits += (*it)->dBits; - - return dBits; -} - -// Parameters can optionally be specified to be added to the Trial objects. -// This allows us to easily see what a certain parameter value was used -// in a given trial. -void CUserLogTrial::AddParam(const std::string& strName, const std::string& strValue, int iOptionMask) { - bool bTrackMultiple = false; - - if (iOptionMask & userLogParamTrackMultiple) bTrackMultiple = true; - - // See if this matches an existing parameter value that we may want to - // overwrite. But only if we aren't suppose to keep track of multiple changes. - if (!bTrackMultiple) { - for (unsigned int i = 0; i < m_vpParams.size(); i++) { - CUserLogParam* pParam = (CUserLogParam*)m_vpParams[i]; - - if (pParam != NULL) { - if (pParam->strName.compare(strName) == 0) { - pParam->strValue = strValue; - return; - } - } - } - } - // We need to add a new param - CUserLogParam* pNewParam = new CUserLogParam; - if (pNewParam == NULL) { - m_pGlobalFileLogger->LogNormal("CUserLogTrial::AddParam, newParam was NULL!"); - return; - } - - pNewParam->strName = strName; - pNewParam->strValue = strValue; - pNewParam->strTimeStamp = ""; - - // Parameters that can have multiple values logged will also log when they were changed - if (bTrackMultiple) pNewParam->strTimeStamp = CTimeSpan::GetTimeStamp(); - - m_vpParams.push_back(pNewParam); -} - -// Static method that generates the XML representation of a -// single param name value set. Used both to output params -// for a trial and for the parent UserLog object. -std::string CUserLogTrial::GetParamXML(CUserLogParam* pParam, const std::string& strPrefix) { - std::string strResult = ""; - - if (pParam != NULL) { - strResult += strPrefix; - strResult += "<"; - strResult += pParam->strName; - strResult += ">"; - - if (pParam->strTimeStamp.length() > 0) { - strResult += "\n"; - strResult += strPrefix; - strResult += "\t"; - strResult += pParam->strValue; - strResult += "\n"; - - strResult += strPrefix; - strResult += "\t\n"; - - strResult += strPrefix; - } else { - strResult += pParam->strValue; - } - - strResult += "strName; - strResult += ">\n"; - } - - return strResult; -} - -// Returns a pointer to the currently active navigation cycle -NavCycle* CUserLogTrial::GetCurrentNavCycle() { - if (m_vpNavCycles.size() <= 0) return NULL; - return m_vpNavCycles[m_vpNavCycles.size() - 1]; -} - -// Gets a pointer to the last NavLocation object -// in the current navication cycle. -NavLocation* CUserLogTrial::GetCurrentNavLocation() { - // NavCycle* pCycle = GetCurrentNavCycle(); - - // if (pCycle == NULL) - // return NULL; - - // if (pCycle->vectorNavLocations.size() <= 0) - // return NULL; - - // return (NavLocation*) pCycle->vectorNavLocations[pCycle->vectorNavLocations.size() - 1]; - - // New version - reverse iterate through the list and find the last nav cycle which has any locations - - for (VECTOR_NAV_CYCLE_PTR::reverse_iterator it(m_vpNavCycles.rbegin()); it != m_vpNavCycles.rend(); ++it) { - if ((*it)->vectorNavLocations.size() > 0) - return (NavLocation*)(*it)->vectorNavLocations[(*it)->vectorNavLocations.size() - 1]; - } - - return NULL; -} - -// Adds a new navgiation cycle to our collection -NavCycle* CUserLogTrial::AddNavCycle() { - NavCycle* pNewCycle = new NavCycle; - if (pNewCycle == NULL) { - m_pGlobalFileLogger->LogNormal("CUserLogTrial::AddNavCycle, failed to create NavCycle!"); - return NULL; - } - - pNewCycle->pSpan = new CTimeSpan("Time", false); - - m_vpNavCycles.push_back(pNewCycle); - return pNewCycle; -} - -std::string CUserLogTrial::GetNavCyclesXML(const std::string& strPrefix) { - std::string strResult = ""; - - std::string strPrefixTab = strPrefix; - strPrefixTab += "\t"; - - std::string strPrefixTabTab = strPrefixTab; - strPrefixTabTab += "\t"; - - std::string strPrefixTabTabTab = strPrefixTabTab; - strPrefixTabTabTab += "\t"; - - std::string strPrefixTabTabTabTab = strPrefixTabTabTab; - strPrefixTabTabTabTab += "\t"; - - strResult += strPrefixTab; - strResult += "\n"; - - for (unsigned int i = 0; i < m_vpNavCycles.size(); i++) { - NavCycle* pCycle = (NavCycle*)m_vpNavCycles[i]; - - if (pCycle != NULL) { - strResult += strPrefixTabTab; - strResult += "\n"; - } - } - - strResult += strPrefixTab; - strResult += "\n"; - - return strResult; -} - -// Construct based on some XML, second parameter is just to make signature -// different from the normal constructor. -CUserLogTrial::CUserLogTrial(const std::string& strXML, int iIgnored) { - VECTOR_STRING vNavs; - - std::string strParams = XMLUtil::GetElementString("Params", strXML, true); - std::string strWindow = XMLUtil::GetElementString("WindowCoordinates", strXML, true); - std::string strCanvas = XMLUtil::GetElementString("CanvasCoordinates", strXML, true); - std::string strNavs = XMLUtil::GetElementString("Navs", strXML, true); - std::string strSummary = XMLUtil::GetElementString("Summary", strXML, true); - std::string strSummaryTime = XMLUtil::GetElementString("Time", strSummary, true); - vNavs = XMLUtil::GetElementStrings("Nav", strNavs, true); - - std::string strCurrentTrial = XMLUtil::GetElementString("CurrentTrial", strXML, false); - if (strCurrentTrial.length() > 0) { - // We copied the XML string directly into the member variable - // including the start/end tag, so we need to reproduce the - // tags ourselves. - m_strCurrentTrial = "\t\t\t\n"; - m_strCurrentTrial += strCurrentTrial; - m_strCurrentTrial += "\n"; - } - - m_vpParams = ParseParamsXML(strParams); - m_sWindowCoordinates = ParseWindowXML(strWindow); - m_sCanvasCoordinates = ParseWindowXML(strCanvas); - m_pSpan = new CTimeSpan("Time", strSummaryTime); - - // Process each