diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index a07701382..f0acdfb6b 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -644,18 +644,18 @@ endif() option(DFLASH27B_TESTS "Build numerics tests" ON) option(DFLASH27B_SERVER "Build dflash_server + backend_ipc_daemon" ON) if(DFLASH27B_TESTS) - if((DFLASH27B_GPU_BACKEND STREQUAL "cuda" OR DFLASH27B_GPU_BACKEND STREQUAL "hip") + set(_raw_unit_test_targets) + enable_testing() + if(DFLASH27B_GPU_BACKEND STREQUAL "hip" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_cuda_pool_shutdown.cpp") add_executable(test_cuda_pool_shutdown test/test_cuda_pool_shutdown.cpp) - if(DFLASH27B_GPU_BACKEND STREQUAL "hip") - set_source_files_properties(test/test_cuda_pool_shutdown.cpp PROPERTIES LANGUAGE HIP) - set_target_properties(test_cuda_pool_shutdown PROPERTIES HIP_ARCHITECTURES "${_dflash_archs}") - endif() + set_source_files_properties(test/test_cuda_pool_shutdown.cpp PROPERTIES LANGUAGE HIP) + set_target_properties(test_cuda_pool_shutdown PROPERTIES HIP_ARCHITECTURES "${_dflash_archs}") target_include_directories(test_cuda_pool_shutdown PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/include) target_link_libraries(test_cuda_pool_shutdown PRIVATE ${DFLASH27B_GGML_BACKEND_TARGET} ggml ggml-base) - add_test(NAME cuda_pool_shutdown COMMAND test_cuda_pool_shutdown) + list(APPEND _raw_unit_test_targets test_cuda_pool_shutdown) endif() add_executable(test_rocmfp4 deps/llama.cpp/ggml/rocmfp4/test_rocmfp4.c) @@ -663,14 +663,14 @@ if(DFLASH27B_TESTS) if(UNIX) target_link_libraries(test_rocmfp4 PRIVATE m) endif() - add_test(NAME rocmfp4_reference COMMAND test_rocmfp4) + list(APPEND _raw_unit_test_targets test_rocmfp4) add_executable(test_rocmfpx deps/llama.cpp/ggml/rocmfpx/test_rocmfpx.c) target_link_libraries(test_rocmfpx PRIVATE ggml-base) if(UNIX) target_link_libraries(test_rocmfpx PRIVATE m) endif() - add_test(NAME rocmfpx_reference COMMAND test_rocmfpx) + list(APPEND _raw_unit_test_targets test_rocmfpx) if(DFLASH27B_GPU_BACKEND STREQUAL "hip") add_executable(test_rocmfp4_hip_tail test/test_rocmfp4_hip_tail.cpp) @@ -684,7 +684,7 @@ if(DFLASH27B_TESTS) ggml-base ${DFLASH27B_GGML_BACKEND_TARGET} hip::host) - add_test(NAME rocmfp4_hip_tail COMMAND test_rocmfp4_hip_tail) + list(APPEND _raw_unit_test_targets test_rocmfp4_hip_tail) add_executable(test_rocmfpx_mmq test/test_rocmfpx_mmq.cpp) set_source_files_properties(test/test_rocmfpx_mmq.cpp PROPERTIES LANGUAGE HIP) @@ -698,7 +698,7 @@ if(DFLASH27B_TESTS) ggml-base ${DFLASH27B_GGML_BACKEND_TARGET} hip::host) - add_test(NAME rocmfpx_mmq COMMAND test_rocmfpx_mmq) + list(APPEND _raw_unit_test_targets test_rocmfpx_mmq) endif() if(DFLASH27B_GPU_BACKEND STREQUAL "cuda" AND _dflash_cuda_min_sm GREATER_EQUAL 80 AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_flashprefill_kernels.cpp") @@ -734,7 +734,7 @@ if(DFLASH27B_TESTS) ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/hip_compat) target_link_libraries(test_rms_norm_hip PRIVATE dflash_common ${DFLASH27B_GGML_BACKEND_TARGET}) - add_test(NAME rms_norm_hip COMMAND test_rms_norm_hip) + list(APPEND _raw_unit_test_targets test_rms_norm_hip) endif() # DS4 Hierarchical-Controller pre-mix kernels vs CPU reference. HIP only: # deepseek4_hc_cuda.cu is compiled into dflash_common on this backend. Validates @@ -747,17 +747,10 @@ if(DFLASH27B_TESTS) ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/hip_compat) target_link_libraries(test_deepseek4_hc_cuda PRIVATE dflash_common ${DFLASH27B_GGML_BACKEND_TARGET}) - add_test(NAME deepseek4_hc_cuda COMMAND test_deepseek4_hc_cuda) + list(APPEND _raw_unit_test_targets test_deepseek4_hc_cuda) endif() - # GPU draft top-K kernel vs CPU reference (extract_draft_topk). Built on both - # backends: geometric_draft_topk_cuda.cu is compiled into dflash_common on the - # cuda backend directly and on hip via LANGUAGE HIP + the hip_compat shim. - if(DFLASH27B_GPU_BACKEND STREQUAL "cuda" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_draft_topk_cuda.cpp") - add_executable(test_draft_topk_cuda test/test_draft_topk_cuda.cpp) - target_include_directories(test_draft_topk_cuda PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) - target_link_libraries(test_draft_topk_cuda PRIVATE dflash_common CUDA::cudart) - add_test(NAME draft_topk_cuda COMMAND test_draft_topk_cuda) - elseif(DFLASH27B_GPU_BACKEND STREQUAL "hip" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_draft_topk_cuda.cpp") + # HIP-only standalone build; CUDA backend is covered by aggregated test_server_unit. + if(DFLASH27B_GPU_BACKEND STREQUAL "hip" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_draft_topk_cuda.cpp") # HIP build of the same GPU-vs-CPU parity test. The test source uses CUDA # spellings ( + cudaMalloc/cudaMemcpy/...); the hip_compat # shim maps them onto HIP, exactly as the kernel TU does. Lets the draft @@ -769,126 +762,18 @@ if(DFLASH27B_TESTS) ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/hip_compat) target_link_libraries(test_draft_topk_cuda PRIVATE dflash_common ${DFLASH27B_GGML_BACKEND_TARGET}) - add_test(NAME draft_topk_cuda COMMAND test_draft_topk_cuda) - endif() - # GPU port of the sample_logits chain vs the CPU reference. CUDA only: - # geometric_sampler_cuda.cu is compiled into dflash_common solely on the cuda backend. - if(DFLASH27B_GPU_BACKEND STREQUAL "cuda" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_gpu_sampler_cuda.cpp") - add_executable(test_gpu_sampler_cuda test/test_gpu_sampler_cuda.cpp) - target_include_directories(test_gpu_sampler_cuda PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) - target_link_libraries(test_gpu_sampler_cuda PRIVATE dflash_common CUDA::cudart) - add_test(NAME gpu_sampler_cuda COMMAND test_gpu_sampler_cuda) - endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_kv_quant.cpp") - add_executable(test_kv_quant test/test_kv_quant.cpp) - target_include_directories(test_kv_quant PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) - target_link_libraries(test_kv_quant PRIVATE dflash_common) - endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_gguf_mmap.cpp") - add_executable(test_gguf_mmap test/test_gguf_mmap.cpp) - target_include_directories(test_gguf_mmap PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) - target_link_libraries(test_gguf_mmap PRIVATE dflash_common) - endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_adaptive_keep_ratio.cpp") - add_executable(test_adaptive_keep_ratio test/test_adaptive_keep_ratio.cpp) - target_include_directories(test_adaptive_keep_ratio PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) - target_link_libraries(test_adaptive_keep_ratio PRIVATE dflash_common) - add_test(NAME adaptive_keep COMMAND test_adaptive_keep_ratio) - endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_derived_scalars.cpp") - add_executable(test_derived_scalars test/test_derived_scalars.cpp) - target_include_directories(test_derived_scalars PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) - add_test(NAME derived_scalars COMMAND test_derived_scalars) - endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_chain_rollback_policy.cpp") - add_executable(test_chain_rollback_policy test/test_chain_rollback_policy.cpp) - target_include_directories(test_chain_rollback_policy PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src/common) - add_test(NAME chain_rollback_policy COMMAND test_chain_rollback_policy) - endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_kvflash_placement.cpp") - add_executable(test_kvflash_placement test/test_kvflash_placement.cpp) - target_include_directories(test_kvflash_placement PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) - add_test(NAME kvflash_placement COMMAND test_kvflash_placement) - endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_bandit_integration.cpp") - add_executable(test_bandit_integration test/test_bandit_integration.cpp) - target_include_directories(test_bandit_integration PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) - target_link_libraries(test_bandit_integration PRIVATE dflash_common) - add_test(NAME bandit_integration COMMAND test_bandit_integration) - endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_anchor_params.cpp") - add_executable(test_anchor_params test/test_anchor_params.cpp) - target_include_directories(test_anchor_params PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) - target_link_libraries(test_anchor_params PRIVATE dflash_common) - add_test(NAME anchor_params COMMAND test_anchor_params) - endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_drafter_early_exit_score_range.cpp") - add_executable(test_drafter_early_exit_score_range - test/test_drafter_early_exit_score_range.cpp) - target_include_directories(test_drafter_early_exit_score_range PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src/common) - add_test(NAME test_drafter_early_exit_score_range - COMMAND test_drafter_early_exit_score_range) - endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_anchor_transitive.cpp") - add_executable(test_anchor_transitive - test/test_anchor_transitive.cpp - src/qwen3/anchor_scan.cpp) - target_include_directories(test_anchor_transitive PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src/qwen3) - add_test(NAME test_anchor_transitive - COMMAND test_anchor_transitive) - endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_drafter_warm_path_regression.cpp") - add_executable(test_drafter_warm_path_regression - test/test_drafter_warm_path_regression.cpp) - target_include_directories(test_drafter_warm_path_regression PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src/common) - add_test(NAME test_drafter_warm_path_regression - COMMAND test_drafter_warm_path_regression) + list(APPEND _raw_unit_test_targets test_draft_topk_cuda) endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_drafter_tail_capture_guard.cpp") - # GREEN phase: built with TAIL_GUARD_USE_NEW_FORMULA — must pass after Bug #42 fix. - add_executable(test_drafter_tail_capture_guard - test/test_drafter_tail_capture_guard.cpp) - target_compile_definitions(test_drafter_tail_capture_guard PRIVATE - TAIL_GUARD_USE_NEW_FORMULA) - add_test(NAME test_drafter_tail_capture_guard - COMMAND test_drafter_tail_capture_guard) # RED phase binary: same source WITHOUT the fix flag — documents the bug. add_executable(test_drafter_tail_capture_guard_red + test/test_unit_main.cpp test/test_drafter_tail_capture_guard.cpp) - # No TAIL_GUARD_USE_NEW_FORMULA — uses old (buggy) guard, expected to FAIL. endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_draft_vs_reference.cpp") add_executable(test_draft_vs_reference test/test_draft_vs_reference.cpp) target_link_libraries(test_draft_vs_reference PRIVATE dflash_common) endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_qwen35moe_routing_stats.cpp") - add_executable(test_qwen35moe_routing_stats test/test_qwen35moe_routing_stats.cpp) - target_include_directories(test_qwen35moe_routing_stats PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) - if(DFLASH27B_GPU_BACKEND STREQUAL "cuda") - target_include_directories(test_qwen35moe_routing_stats PRIVATE ${CUDAToolkit_INCLUDE_DIRS}) - endif() - target_link_libraries(test_qwen35moe_routing_stats PRIVATE dflash_common) - endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_qwen35moe_expert_placement.cpp") - add_executable(test_qwen35moe_expert_placement test/test_qwen35moe_expert_placement.cpp) - target_include_directories(test_qwen35moe_expert_placement PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) - if(DFLASH27B_GPU_BACKEND STREQUAL "cuda") - target_include_directories(test_qwen35moe_expert_placement PRIVATE ${CUDAToolkit_INCLUDE_DIRS}) - endif() - target_link_libraries(test_qwen35moe_expert_placement PRIVATE dflash_common) - endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_qwen35moe_swap_manager.cpp") - add_executable(test_qwen35moe_swap_manager test/test_qwen35moe_swap_manager.cpp) - target_include_directories(test_qwen35moe_swap_manager PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) - if(DFLASH27B_GPU_BACKEND STREQUAL "cuda") - target_include_directories(test_qwen35moe_swap_manager PRIVATE ${CUDAToolkit_INCLUDE_DIRS}) - endif() - target_link_libraries(test_qwen35moe_swap_manager PRIVATE dflash_common) - endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_deepseek4_unit.cpp") add_executable(test_deepseek4_unit test/test_deepseek4_unit.cpp) target_include_directories(test_deepseek4_unit PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}/include) @@ -906,7 +791,7 @@ if(DFLASH27B_TESTS) else() target_link_libraries(test_deepseek4_unit PRIVATE hip::host) endif() - add_test(NAME deepseek4_unit COMMAND test_deepseek4_unit) + list(APPEND _raw_unit_test_targets test_deepseek4_unit) endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_ds4_dspark_load.cpp") add_executable(test_ds4_dspark_load test/test_ds4_dspark_load.cpp) @@ -984,11 +869,6 @@ if(DFLASH27B_TESTS) target_include_directories(bench_laguna_spark PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) target_link_libraries(bench_laguna_spark PRIVATE dflash_common ggml ${DFLASH27B_GGML_BACKEND_TARGET}) endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/bench_moe_stream.cpp" AND DFLASH27B_GPU_BACKEND STREQUAL "cuda") - add_executable(bench_moe_stream test/bench_moe_stream.cpp) - target_include_directories(bench_moe_stream PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS} ${CUDAToolkit_INCLUDE_DIRS}) - target_link_libraries(bench_moe_stream PRIVATE dflash_common ggml ${DFLASH27B_GGML_BACKEND_TARGET} CUDA::cudart) - endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_laguna_daemon.cpp") add_executable(test_laguna_daemon test/test_laguna_daemon.cpp) target_include_directories(test_laguna_daemon PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) @@ -1017,24 +897,6 @@ if(DFLASH27B_TESTS) target_link_libraries(test_kvflash PRIVATE CUDA::cudart) endif() endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_kvflash_qk.cpp") - # Pure-math unit test (KVFLASH_QK_PURE_ONLY): no ggml, no GPU. - add_executable(test_kvflash_qk test/test_kvflash_qk.cpp) - target_include_directories(test_kvflash_qk PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) - endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_kvflash_pool_sizing.cpp") - # Pure unit test for kvflash_pool_from_env: ggml headers only, no ggml link, no GPU. - add_executable(test_kvflash_pool_sizing test/test_kvflash_pool_sizing.cpp) - target_include_directories(test_kvflash_pool_sizing PRIVATE - ${DFLASH27B_SRC_INCLUDE_DIRS} - ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/include - ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/src) - add_test(NAME kvflash_pool_sizing COMMAND test_kvflash_pool_sizing) - endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_restore_delta.cpp") - add_executable(test_restore_delta test/test_restore_delta.cpp) - target_include_directories(test_restore_delta PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) - endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_dflash.cpp") add_executable(test_dflash test/test_dflash.cpp) target_include_directories(test_dflash PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) @@ -1082,15 +944,48 @@ if(DFLASH27B_TESTS) endif() # ─── Unit tests (no GPU, no model files) ──────────────────────────── - enable_testing() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_server_unit.cpp") - add_executable(test_server_unit test/test_server_unit.cpp) + set(_server_unit_sources + test/test_unit_main.cpp + test/test_server_unit.cpp + test/test_anchor_params.cpp + test/test_derived_scalars.cpp + test/test_adaptive_keep_ratio.cpp + test/test_skip_park_guard.cpp + test/test_bandit_integration.cpp + test/test_admission.cpp + test/test_restore_delta.cpp + test/test_chain_rollback_policy.cpp + test/test_anchor_transitive.cpp + test/test_drafter_early_exit_score_range.cpp + test/test_drafter_tail_capture_guard.cpp + test/test_drafter_warm_path_regression.cpp + test/test_gguf_mmap.cpp + test/test_kv_quant.cpp + test/test_kvflash_placement.cpp + test/test_kvflash_pool_sizing.cpp + test/test_kvflash_qk.cpp + test/test_qwen35moe_routing_stats.cpp + test/test_qwen35moe_expert_placement.cpp + test/test_qwen35moe_swap_manager.cpp) + if(DFLASH27B_GPU_BACKEND STREQUAL "cuda") + list(APPEND _server_unit_sources + test/test_gpu_sampler_cuda.cpp + test/test_cuda_pool_shutdown.cpp + test/test_draft_topk_cuda.cpp + test/test_moe_stream.cpp) + endif() + add_executable(test_server_unit ${_server_unit_sources}) target_sources(test_server_unit PRIVATE src/server/http_server.cpp src/server/model_card.cpp - src/server/prompt_normalize.cpp) - target_include_directories(test_server_unit PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) + src/server/prompt_normalize.cpp + src/qwen3/anchor_scan.cpp) + set_source_files_properties(test/test_drafter_tail_capture_guard.cpp + PROPERTIES COMPILE_DEFINITIONS TAIL_GUARD_USE_NEW_FORMULA) + target_include_directories(test_server_unit PRIVATE + ${DFLASH27B_SRC_INCLUDE_DIRS} + ${CMAKE_CURRENT_SOURCE_DIR}/src/common) if(DFLASH27B_GPU_BACKEND STREQUAL "hip") target_compile_definitions(test_server_unit PRIVATE DFLASH27B_BACKEND_HIP=1 GGML_USE_HIP) else() @@ -1111,26 +1006,50 @@ if(DFLASH27B_TESTS) else() target_link_libraries(test_server_unit PRIVATE hip::host) endif() - add_test(NAME server_unit COMMAND test_server_unit) + set(_server_unit_ctest_generated + "${CMAKE_CURRENT_BINARY_DIR}/test_server_unit_discovered_tests.cmake") + set(_server_unit_ctest_include + "${CMAKE_CURRENT_BINARY_DIR}/test_server_unit_discovered_tests_include.cmake") + file(GENERATE OUTPUT "${_server_unit_ctest_include}" + CONTENT "include([==[${_server_unit_ctest_generated}]==] OPTIONAL)\n") + set_property(DIRECTORY APPEND PROPERTY TEST_INCLUDE_FILES + "${_server_unit_ctest_include}") + add_custom_command(TARGET test_server_unit POST_BUILD + COMMAND ${CMAKE_COMMAND} + -DTEST_EXECUTABLE=$ + -DTEST_WORKING_DIR=${CMAKE_CURRENT_BINARY_DIR} + -DCTEST_FILE=${_server_unit_ctest_generated} + -DTEST_PREFIX=test_server_unit. + -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/DiscoverCppUnitTests.cmake + VERBATIM + COMMENT "Discovering CppUnitTestFramework tests for test_server_unit") + unset(_server_unit_ctest_generated) + unset(_server_unit_ctest_include) + unset(_server_unit_sources) + list(APPEND _raw_unit_test_targets test_server_unit) endif() - # 'make check' — builds test targets then runs ctest - if(TARGET test_server_unit) - set(_check_deps test_server_unit) - if(TARGET test_deepseek4_unit) - list(APPEND _check_deps test_deepseek4_unit) - endif() + if(_raw_unit_test_targets) + foreach(_unit_target IN LISTS _raw_unit_test_targets) + if(NOT _unit_target STREQUAL "test_server_unit") + add_test(NAME "${_unit_target}" COMMAND ${_unit_target}) + endif() + endforeach() + endif() + + # 'make check' — build unit-test targets then run ctest. + if(_raw_unit_test_targets) add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure - DEPENDS ${_check_deps} + DEPENDS ${_raw_unit_test_targets} COMMENT "Building and running unit tests" ) else() add_custom_target(check - COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure - COMMENT "Building and running unit tests (server unit tests skipped — CURL not found)" + COMMENT "No unit-test binaries are enabled in this configuration" ) endif() + unset(_raw_unit_test_targets) if(DFLASH27B_GPU_BACKEND STREQUAL "cuda") # internal.h includes when GGML_USE_CUDA is set; link diff --git a/server/cmake/DiscoverCppUnitTests.cmake b/server/cmake/DiscoverCppUnitTests.cmake new file mode 100644 index 000000000..9017696b3 --- /dev/null +++ b/server/cmake/DiscoverCppUnitTests.cmake @@ -0,0 +1,68 @@ +if(NOT DEFINED TEST_EXECUTABLE OR TEST_EXECUTABLE STREQUAL "") + message(FATAL_ERROR "TEST_EXECUTABLE is required") +endif() + +if(NOT DEFINED TEST_WORKING_DIR OR TEST_WORKING_DIR STREQUAL "") + message(FATAL_ERROR "TEST_WORKING_DIR is required") +endif() + +if(NOT DEFINED CTEST_FILE OR CTEST_FILE STREQUAL "") + message(FATAL_ERROR "CTEST_FILE is required") +endif() + +if(NOT DEFINED TEST_PREFIX) + set(TEST_PREFIX "") +endif() + +execute_process( + COMMAND "${TEST_EXECUTABLE}" --discover_tests --adapter_info + WORKING_DIRECTORY "${TEST_WORKING_DIR}" + RESULT_VARIABLE _discover_result + OUTPUT_VARIABLE _discover_output + ERROR_VARIABLE _discover_error + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +if(NOT _discover_result EQUAL 0) + string(JOIN "\n" _discover_failure + "Failed to discover tests from ${TEST_EXECUTABLE}" + "Exit code: ${_discover_result}" + "stderr:" + "${_discover_error}") + message(FATAL_ERROR "${_discover_failure}") +endif() + +string(REPLACE "\r\n" "\n" _discover_output "${_discover_output}") +string(REPLACE "\r" "\n" _discover_output "${_discover_output}") + +file(WRITE "${CTEST_FILE}" + "# Generated by DiscoverCppUnitTests.cmake. Do not edit.\n") + +if(_discover_output) + string(REPLACE "\n" ";" _discover_lines "${_discover_output}") + set(_discovered_ctest_names) + foreach(_discover_line IN LISTS _discover_lines) + if(_discover_line STREQUAL "") + continue() + endif() + + if(NOT _discover_line MATCHES "^([^,]+),(.*),([0-9]+)$") + message(FATAL_ERROR + "Unexpected test discovery output from ${TEST_EXECUTABLE}: ${_discover_line}") + endif() + + set(_test_keyword "${CMAKE_MATCH_1}") + string(REPLACE "::" "." _test_suffix "${_test_keyword}") + set(_ctest_name "${TEST_PREFIX}${_test_suffix}") + + list(FIND _discovered_ctest_names "${_ctest_name}" _existing_index) + if(NOT _existing_index EQUAL -1) + message(FATAL_ERROR "Duplicate discovered test name: ${_ctest_name}") + endif() + + file(APPEND "${CTEST_FILE}" + "add_test([==[${_ctest_name}]==] [==[${TEST_EXECUTABLE}]==] [==[${_test_keyword}]==])\n" + "set_tests_properties([==[${_ctest_name}]==] PROPERTIES WORKING_DIRECTORY [==[${TEST_WORKING_DIR}]==])\n") + list(APPEND _discovered_ctest_names "${_ctest_name}") + endforeach() +endif() diff --git a/server/test/CppUnitTestFramework.hpp b/server/test/CppUnitTestFramework.hpp new file mode 100644 index 000000000..61676aac3 --- /dev/null +++ b/server/test/CppUnitTestFramework.hpp @@ -0,0 +1,846 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace CppUnitTestFramework { + + struct AssertLocation { + std::string_view SourceFile; + size_t LineNumber; + }; + + enum class AssertType { + Throw, + Continue + }; + + struct AssertException : + std::exception + { + AssertException(std::string message) + : m_message(std::move(message)) + {} + + const char* what() const noexcept override { + return m_message.data(); + } + + private: + std::string m_message; + }; + + //-------------------------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------------------------- + + struct RunOptions { + bool Verbose = false; + bool DiscoveryMode = false; + bool AdapterInfo = false; + std::vector Keywords; + + bool ParseCommandLine(int argc, const char* argv[]) { + for (int index = 1; index < argc; ++index) { + auto arg = argv[index]; + + if (arg[0] != '-') { + Keywords.push_back(arg); + continue; + } + + std::string option_name{ &arg[1] }; + if (option_name == "h" || option_name == "-help" || option_name == "?") { + std::cout << "Usage: [] [keyword1] [keyword2] ..." << std::endl; + std::cout << " -h, --help, -?: Displays this message" << std::endl; + std::cout << " -v, --verbose: Show verbose output" << std::endl; + std::cout << " --discover_tests: Output test details" << std::endl; + std::cout << " --adapter_info: Output additional details for test adapters" << std::endl; + return false; + } + + if (option_name == "v" || option_name == "-verbose") { + Verbose = true; + continue; + } + + if (option_name == "-discover_tests") { + DiscoveryMode = true; + continue; + } + + if (option_name == "-adapter_info") { + AdapterInfo = true; + continue; + } + + // Unknown option + std::cerr << "Unknown option: " << option_name << std::endl; + return false; + } + + return true; + } + }; + + //-------------------------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------------------------- + + struct ILogger { + virtual ~ILogger() = default; + + virtual void BeginRun(size_t test_count) = 0; + virtual void EndRun(size_t pass_count, size_t fail_count, size_t skip_count) = 0; + + virtual void SkipTest(const std::string_view& name) = 0; + virtual void EnterTest(const std::string_view& name) = 0; + virtual void ExitTest(bool failed) = 0; + + virtual void SkipSection(const std::string_view& name) = 0; + virtual void PushSection(const std::string_view& name) = 0; + virtual void PopSection() = 0; + + virtual void AssertFailed( + AssertType type, + const AssertLocation& location, + const std::string_view& message + ) = 0; + virtual void UnhandledException(const std::string_view& message) = 0; + }; + using ILoggerPtr = std::shared_ptr; + + //-------------------------------------------------------------------------------------------------------- + + struct ConsoleLogger : + ILogger + { + static ILoggerPtr Create(const RunOptions* options) { + return std::unique_ptr{ new ConsoleLogger(options) }; + } + + virtual ~ConsoleLogger() = default; + + void BeginRun(size_t test_count) override { + std::cout << "Running " << test_count << " test cases..." << std::endl; + } + void EndRun(size_t pass_count, size_t fail_count, size_t skip_count) override { + std::cout << "Complete." << std::endl; + std::cout << " Passed: " << pass_count << std::endl; + std::cout << " Failed: " << fail_count << std::endl; + std::cout << " Skipped: " << skip_count << std::endl; + } + + void SkipTest(const std::string_view& name) override { + m_test_log.clear(); + m_test_log << "Skip: " << name.data() << std::endl; + + if (m_run_options->Verbose) { + FlushLog(); + } + } + void EnterTest(const std::string_view& name) override { + m_test_log = std::stringstream(); + m_test_log << "Test: " << name.data() << std::endl; + m_indent_level++; + + if (m_run_options->Verbose) { + FlushLog(); + } + } + void ExitTest(bool failed) override { + m_indent_level = 0; + + if (m_run_options->AdapterInfo) { + m_test_log << "Test Complete: "; + if (failed) { + m_test_log << "failed" << std::endl; + } else { + m_test_log << "passed" << std::endl; + } + } + + if (failed || m_run_options->Verbose) { + FlushLog(); + } + } + + void SkipSection(const std::string_view& name) override { + Indent() << "[Skipped] " << name.data() << std::endl; + + if (m_run_options->Verbose) { + FlushLog(); + } + } + void PushSection(const std::string_view& name) override { + Indent() << name.data() << std::endl; + m_indent_level++; + + if (m_run_options->Verbose) { + FlushLog(); + } + } + void PopSection() override { + if (m_indent_level > 0) { + m_indent_level--; + } + } + + void AssertFailed( + AssertType type, + const AssertLocation& location, + const std::string_view& message + ) override { + auto& log = Indent(); + + log << "@" << location.LineNumber << " "; + + switch (type) { + case AssertType::Throw: log << "REQUIRE"; break; + case AssertType::Continue: log << "CHECK"; break; + } + + log << ": " << message.data() << std::endl; + + if (m_run_options->Verbose) { + FlushLog(); + } + } + void UnhandledException(const std::string_view& message) override { + Indent() << "Fail: " << message.data() << std::endl; + + if (m_run_options->Verbose) { + FlushLog(); + } + } + + private: + ConsoleLogger(const RunOptions* run_options) + : m_run_options(run_options) + {} + + std::ostream& Indent() { + for (size_t i = 0; i != m_indent_level; ++i) { + m_test_log << " "; + } + return m_test_log; + } + + void FlushLog() { + std::cout << m_test_log.str().c_str(); + m_test_log = std::stringstream(); + } + + private: + const RunOptions*const m_run_options; + size_t m_indent_level = 0; + std::stringstream m_test_log; + }; + + //-------------------------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------------------------- + + struct TestRegistry { + private: + using TestCallback = std::function; + struct TestDetails { + std::string_view Name; + std::string_view SourceFile; + size_t SourceLine; + std::vector Tags; + TestCallback Callback; + }; + + public: + template + struct AutoReg { + AutoReg() { + TestRegistry::Add(); + } + }; + + public: + template + static void Add() { + TestDetails details; + details.Name = TTestCase::Name; + details.SourceFile = TTestCase::SourceFile; + details.SourceLine = TTestCase::SourceLine; + details.Tags.assign(std::begin(TTestCase::Tags), std::end(TTestCase::Tags)); + details.Callback = [](const auto&... args) -> bool { + TTestCase test_case(args...); + test_case.Run(); + return test_case.HaveChecksFailed(); + }; + + GetTestVector().push_back(std::move(details)); + } + + static bool Run(const RunOptions* options, const ILoggerPtr& logger) { + const auto& all_test_cases = GetTestVector(); + + if (options->DiscoveryMode) { + for (auto& test_case : all_test_cases) { + // Output the test name. + std::cout << test_case.Name; + + if (options->AdapterInfo) { + // Output the source file and line number. + std::cout << "," << test_case.SourceFile << "," << test_case.SourceLine; + } + + std::cout << std::endl; + } + return true; + } + + logger->BeginRun(all_test_cases.size()); + + size_t pass_count = 0; + size_t fail_count = 0; + size_t skip_count = 0; + + for (auto& test_case : all_test_cases) { + if (!ShouldRunTest(options, test_case.Name, test_case.Tags)) { + logger->SkipTest(test_case.Name); + skip_count++; + continue; + } + + logger->EnterTest(test_case.Name); + + bool test_failed = true; + try { + test_failed = test_case.Callback(logger); + } catch (const AssertException&) { + // REQUIRE* statement failed. No need to do anything else. + } catch (const std::exception& e) { + logger->UnhandledException(e.what()); + } catch (...) { + logger->UnhandledException(""); + } + + logger->ExitTest(test_failed); + + if (test_failed) { + fail_count++; + } else { + pass_count++; + } + } + + logger->EndRun(pass_count, fail_count, skip_count); + + return (fail_count == 0); + } + + private: + static std::vector& GetTestVector() { + static std::vector s_test_vector; + return s_test_vector; + } + + static bool ShouldRunTest( + const RunOptions* options, + const std::string_view& test_name, + const std::vector test_tags + ) { + if (options->Keywords.empty()) { + // No keywords. All tests match. + return true; + } + + for (auto& keyword : options->Keywords) { + if (test_name.find(keyword) != std::string_view::npos) { + return true; + } + + for (auto& tag : test_tags) { + if (tag == keyword) { + return true; + } + } + } + + return false; + } + }; + + //-------------------------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------------------------- + + namespace Ext { + template + std::string ToString([[maybe_unused]] const T& value) { + if constexpr (std::is_null_pointer_v) { + // std::nullptr_t + return "nullptr"; + + } else if constexpr (std::is_same_v) { + // std::nullopt + return "?"; + + } else if constexpr (std::is_constructible_v) { + // std::string(const T&) + return std::string(value); + + } else if constexpr (std::is_pointer_v) { + // -> Hex address + std::ostringstream ss; + ss << "0x" << std::hex << std::setfill('0') << std::setw(sizeof(size_t) * 2) + << reinterpret_cast(value); + return ss.str(); + + } else if constexpr (std::is_enum_v) { + // Enum -> [] + std::ostringstream ss; + if constexpr (sizeof(std::underlying_type_t) == sizeof(char)) { + ss << "[" << typeid(T).name() << "] " << static_cast(value); + } else { + ss << "[" << typeid(T).name() << "] " << static_cast>(value); + } + return ss.str(); + + } else if constexpr (std::is_floating_point_v) { + // float|double -> + std::ostringstream ss; + ss << value; + return ss.str(); + + } else { + // std::to_string(const T&) + return std::to_string(value); + } + } + + //---------------------------------------------------------------------------------------------------- + + template + std::string ToString(const std::optional& value) { + if (!value.has_value()) { + return ToString(std::nullopt); + } + + return ToString(value.value()); + } + } + + //-------------------------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------------------------- + + namespace Assert { + template + std::optional AreEqual(TLeft&& left, TRight&& right) { + bool equal = static_cast(left == right); + if (equal) { + return std::nullopt; + } + + auto msg = "[" + Ext::ToString(left) + "] == [" + Ext::ToString(right) + "]"; + return AssertException(msg.c_str()); + } + + //---------------------------------------------------------------------------------------------------- + + inline std::optional AreEqual(const char* left, const char* right) { + bool equal = (std::strcmp(left, right) == 0); + if (equal) { + return std::nullopt; + } + + auto msg = "[" + Ext::ToString(left) + "] == [" + Ext::ToString(right) + "]"; + return AssertException(msg.c_str()); + } + + //---------------------------------------------------------------------------------------------------- + + template + std::optional IsNull(const T& value, const char* expression) { + if (value == nullptr) { + return std::nullopt; + } + + std::ostringstream ss; + ss << "IsNull(" << expression << ")"; + return AssertException(ss.str()); + } + + //---------------------------------------------------------------------------------------------------- + + template + std::optional IsNotNull(const T& value, const char* expression) { + if (value != nullptr) { + return std::nullopt; + } + + std::ostringstream ss; + ss << "IsNotNull(" << expression << ")"; + return AssertException(ss.str()); + } + + //---------------------------------------------------------------------------------------------------- + + inline std::optional IsTrue(bool value, const char* expression) { + if (!value) { + std::ostringstream ss; + ss << "IsTrue(" << expression << ")"; + return AssertException(ss.str()); + } + return std::nullopt; + } + + //---------------------------------------------------------------------------------------------------- + + inline std::optional IsFalse(bool value, const char* expression) { + if (value) { + std::ostringstream ss; + ss << "IsFalse(" << expression << ")"; + return AssertException(ss.str()); + } + return std::nullopt; + } + + //---------------------------------------------------------------------------------------------------- + + template + inline std::optional Throws(const Callback& callback) { + try { + callback(); + } catch (const TException&) { + return std::nullopt; + } catch (const std::exception& e) { + std::ostringstream ss; + ss << "Expected exception [" << typeid(TException).name() << "] but caught [" + << typeid(e).name() << ": " << e.what() << "]"; + return AssertException(ss.str()); + } catch (...) { + std::ostringstream ss; + ss << "Expected exception [" << typeid(TException).name() << "] but caught another"; + return AssertException(ss.str()); + } + + std::ostringstream ss; + ss << "Expected exception [" << typeid(TException).name() << "] but none was thrown"; + return AssertException(ss.str()); + } + + //---------------------------------------------------------------------------------------------------- + + template + inline std::optional NoThrow(const Callback& callback) { + try { + callback(); + } catch (std::exception& e) { + std::ostringstream ss; + ss << "Expected no exception but caught [" << typeid(e).name() << ": " << e.what() << "]"; + return AssertException(ss.str()); + } catch (...) { + return AssertException("Expected no exception but one occurred"); + } + + return std::nullopt; + } + + //---------------------------------------------------------------------------------------------------- + + inline std::optional Close(float left, float right, float percentage_tolerance) { + // Code based on BOOST_CHECK_CLOSE + auto safe_div = [](float a, float b) -> float { + // Avoid overflow. + if ((b < 1.0f) && (a > b*std::numeric_limits::max())) { + return std::numeric_limits::max(); + } + + // Avoid underflow. + if ((std::abs(a) <= std::numeric_limits::min()) || + ((b > 1.0f) && (a < b*std::numeric_limits::min())) + ) { + return 0.0f; + } + + return a / b; + }; + + float diff = std::abs(left - right); + float percentage_left = safe_div(diff, std::abs(left)); + float percentage_right = safe_div(diff, std::abs(right)); + + if (percentage_left <= percentage_tolerance && percentage_right <= percentage_tolerance) { + return std::nullopt; + } + + std::ostringstream ss; + ss << "[" + Ext::ToString(left) + "] == [" + Ext::ToString(right) + "]: "; + ss << "[" << diff << "] exceeds " << percentage_tolerance << "%"; + return AssertException(ss.str()); + } + + //---------------------------------------------------------------------------------------------------- + + inline std::optional Close(double left, double right, double percentage_tolerance) { + // Code based on BOOST_CHECK_CLOSE + auto safe_div = [](double a, double b) -> double { + // Avoid overflow. + if ((b < 1.0) && (a > b*std::numeric_limits::max())) { + return std::numeric_limits::max(); + } + + // Avoid underflow. + if ((std::abs(a) <= std::numeric_limits::min()) || + ((b > 1.0) && (a < b*std::numeric_limits::min())) + ) { + return 0.0; + } + + return a / b; + }; + + double diff = std::abs(left - right); + double percentage_left = safe_div(diff, std::abs(left)); + double percentage_right = safe_div(diff, std::abs(right)); + + if (percentage_left <= percentage_tolerance && percentage_right <= percentage_tolerance) { + return std::nullopt; + } + + std::ostringstream ss; + ss << "[" + Ext::ToString(left) + "] == [" + Ext::ToString(right) + "]: "; + ss << "[" << diff << "] exceeds " << percentage_tolerance << "%"; + return AssertException(ss.str()); + } + + //---------------------------------------------------------------------------------------------------- + + inline std::optional CloseFraction(float left, float right, float fraction) { + float diff = std::abs(left - right); + if (diff <= fraction) { + return std::nullopt; + } + + std::ostringstream ss; + ss << "[" + Ext::ToString(left) + "] == [" + Ext::ToString(right) + "]: "; + ss << "[" << diff << "] exceeds " << fraction; + return AssertException(ss.str()); + } + + //---------------------------------------------------------------------------------------------------- + + inline std::optional CloseFraction(double left, double right, double fraction) { + double diff = std::abs(left - right); + if (diff <= fraction) { + return std::nullopt; + } + + std::ostringstream ss; + ss << "[" + Ext::ToString(left) + "] == [" + Ext::ToString(right) + "]: "; + ss << "[" << diff << "] exceeds " << fraction; + return AssertException(ss.str()); + } + + } + + //-------------------------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------------------------- + + struct CommonFixture; + struct SectionLock { + ~SectionLock() { + if (m_logger00) { + m_logger00->PopSection(); + m_logger00 = nullptr; + } + } + + private: + SectionLock(const std::string_view& text, ILoggerPtr logger) + : m_logger00(std::move(logger)) + { + m_logger00->PushSection(text); + } + + SectionLock(SectionLock&& other) noexcept + : m_logger00(std::move(other.m_logger00)) + {} + + SectionLock& operator = (SectionLock&& other) noexcept { + if (m_logger00) { + m_logger00->PopSection(); + } + m_logger00 = std::move(other.m_logger00); + return *this; + } + + ILoggerPtr m_logger00; + + friend struct CommonFixture; + }; + + //-------------------------------------------------------------------------------------------------------- + + struct CommonFixture + { + CommonFixture(ILoggerPtr logger) + : m_logger(std::move(logger)) + {} + + bool HaveChecksFailed() const { + return m_check_has_failed; + } + + protected: + SectionLock EnterSection(std::string text) { + return SectionLock(text, m_logger); + } + + void HandleAssert( + AssertType behavior, + const AssertLocation& location, + const std::optional& exception00 + ) { + if (!exception00) { + // Nothing happened. + return; + } + + m_logger->AssertFailed(behavior, location, exception00->what()); + + if (behavior == AssertType::Throw) { + throw *exception00; + } else { + m_check_has_failed = true; + } + } + + // In leiu of std::make_array() + template + static auto make_tags_array(TArgs&&... tags) { + return std::vector { + std::forward(tags)... + }; + } + + private: + bool m_check_has_failed = false; + ILoggerPtr m_logger; + }; + + //-------------------------------------------------------------------------------------------------------- + + template + struct TestFixtureBaseImpl : CommonFixture, T { + using CommonFixture::CommonFixture; + }; + + template + using TestFixtureBase = std::conditional_t< + std::is_base_of_v, + T, + TestFixtureBaseImpl + >; + +} + +//------------------------------------------------------------------------------------------------------------ + +#define _CPPUTF_COMBINE_NAME_PARTS2(a, b) a##b +#define _CPPUTF_COMBINE_NAME_PARTS(a, b) _CPPUTF_COMBINE_NAME_PARTS2(a, b) +#define _CPPUTF_NEXT_REGISTRAR_NAME _CPPUTF_COMBINE_NAME_PARTS(s_test_registrar_, __COUNTER__) +#define _CPPUTF_NEXT_SECTION_LOCK_NAME _CPPUTF_COMBINE_NAME_PARTS(section_lock_, __COUNTER__) +#define _CPPUTF_NEXT_EXCEPTION_NAME _CPPUTF_COMBINE_NAME_PARTS(exception_, __COUNTER__) +#define _CPPUTF_NEXT_MAYBEUNUSED_NAME _CPPUTF_COMBINE_NAME_PARTS(unused_, __COUNTER__) + +//------------------------------------------------------------------------------------------------------------ + +#define TEST_CASE_WITH_TAGS(TestFixture, TestName, ...) namespace { \ + struct TestCase_##TestName : CppUnitTestFramework::TestFixtureBase { \ + using CppUnitTestFramework::TestFixtureBase::TestFixtureBase; \ + static constexpr std::string_view SourceFile = __FILE__; \ + static constexpr size_t SourceLine = __LINE__; \ + static constexpr std::string_view Name = #TestFixture "::" #TestName; \ + static std::vector Tags; \ + void Run(); \ + }; \ + std::vector TestCase_##TestName::Tags = make_tags_array(__VA_ARGS__); \ + CppUnitTestFramework::TestRegistry::AutoReg _CPPUTF_NEXT_REGISTRAR_NAME; \ +} \ +void TestCase_##TestName::Run() + +#define TEST_CASE(TestFixture, TestName) TEST_CASE_WITH_TAGS(TestFixture, TestName, ) + +//------------------------------------------------------------------------------------------------------------ + +#define SECTION(Text) if (auto _CPPUTF_NEXT_SECTION_LOCK_NAME = EnterSection("Section: " Text); true) +#define SCENARIO(Text) if (auto _CPPUTF_NEXT_SECTION_LOCK_NAME = EnterSection("Scenario: " Text); true) +#define GIVEN(Text) if (auto _CPPUTF_NEXT_SECTION_LOCK_NAME = EnterSection("Given: " Text); true) +#define AND(Text) if (auto _CPPUTF_NEXT_SECTION_LOCK_NAME = EnterSection("And: " Text); true) +#define WHEN(Text) if (auto _CPPUTF_NEXT_SECTION_LOCK_NAME = EnterSection("When: " Text); true) +#define THEN(Text) if (auto _CPPUTF_NEXT_SECTION_LOCK_NAME = EnterSection("Then: " Text); true) + +//------------------------------------------------------------------------------------------------------------ + +#define _CPPUTF_ASSERT_LOCATION CppUnitTestFramework::AssertLocation{ __FILE__, __LINE__ } + +#define REQUIRE(Expression) CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Throw, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::IsTrue(static_cast(Expression), #Expression)) +#define REQUIRE_TRUE(Expression) CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Throw, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::IsTrue(static_cast(Expression), #Expression)) +#define REQUIRE_FALSE(Expression) CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Throw, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::IsFalse(static_cast(Expression), #Expression)) +#define REQUIRE_EQUAL(Left, Right) CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Throw, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::AreEqual((Left), (Right))) +#define REQUIRE_NULL(Expression) CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Throw, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::IsNull((Expression), #Expression)) +#define REQUIRE_NOT_NULL(Expression) CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Throw, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::IsNotNull((Expression), #Expression)) +#define REQUIRE_THROW(ExceptionType, Expression) \ + CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Throw, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::Throws([&] { Expression; })) +#define REQUIRE_NO_THROW(Expression) \ + CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Throw, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::NoThrow([&] { Expression; })) +#define REQUIRE_CLOSE(Left, Right, Percentage) \ + CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Throw, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::Close((Left), (Right), (Percentage))) +#define REQUIRE_CLOSE_FRACTION(Left, Right, Fraction) \ + CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Throw, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::CloseFraction((Left), (Right), (Fraction))) + +#define CHECK(Expression) CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Continue, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::IsTrue(static_cast(Expression), #Expression)) +#define CHECK_TRUE(Expression) CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Continue, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::IsTrue(static_cast(Expression), #Expression)) +#define CHECK_FALSE(Expression) CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Continue, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::IsFalse(static_cast(Expression), #Expression)) +#define CHECK_EQUAL(Left, Right) CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Continue, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::AreEqual((Left), (Right))) +#define CHECK_NULL(Expression) CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Continue, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::IsNull((Expression), #Expression)) +#define CHECK_NOT_NULL(Expression) CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Continue, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::IsNotNull((Expression), #Expression)) +#define CHECK_THROW(ExceptionType, Expression) \ + CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Continue, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::Throws([&] { Expression; })) +#define CHECK_NO_THROW(Expression) \ + CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Continue, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::NoThrow([&] { Expression; })) +#define CHECK_CLOSE(Left, Right, Percentage) \ + CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Continue, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::Close((Left), (Right), (Percentage))) +#define CHECK_CLOSE_FRACTION(Left, Right, Fraction) \ + CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Continue, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::CloseFraction((Left), (Right), (Fraction))) + +//------------------------------------------------------------------------------------------------------------ + +#define UNUSED_RETURN(Expression) [[maybe_unused]] auto _CPPUTF_NEXT_MAYBEUNUSED_NAME = Expression + +//------------------------------------------------------------------------------------------------------------ + +#ifdef GENERATE_UNIT_TEST_MAIN +int main(int argc, const char* argv[]) { + CppUnitTestFramework::RunOptions options; + if (!options.ParseCommandLine(argc, argv)) { + return 2; + } + + bool success = CppUnitTestFramework::TestRegistry::Run( + &options, + CppUnitTestFramework::ConsoleLogger::Create(&options) + ); + + return success ? 0 : 1; +} +#endif \ No newline at end of file diff --git a/server/test/test_adaptive_keep_ratio.cpp b/server/test/test_adaptive_keep_ratio.cpp index b4e8d59f7..495fc28ee 100644 --- a/server/test/test_adaptive_keep_ratio.cpp +++ b/server/test/test_adaptive_keep_ratio.cpp @@ -1,123 +1,84 @@ // Unit tests for AdaptiveKeepRatioState + HttpServerSessions — no GPU, no model files. -// -// Build: cmake --build build --target test_adaptive_keep_ratio -j -// Run: cd build && ctest -R adaptive_keep --output-on-failure +#include "CppUnitTestFramework.hpp" #include "server/adaptive_keep_ratio.h" #include -#include #include using namespace dflash::common; -// ─── Test framework (ds4 style) ─────────────────────────────────────────────── - -static int test_failures = 0; -static int test_count = 0; - -#define TEST_ASSERT(expr) do { \ - test_count++; \ - if (!(expr)) { \ - test_failures++; \ - std::fprintf(stderr, " FAIL: %s:%d: %s\n", __FILE__, __LINE__, #expr); \ - } \ -} while (0) - -#define TEST_ASSERT_MSG(expr, msg) do { \ - test_count++; \ - if (!(expr)) { \ - test_failures++; \ - std::fprintf(stderr, " FAIL: %s:%d: %s -- %s\n", __FILE__, __LINE__, #expr, msg); \ - } \ -} while (0) - -#define RUN_TEST(fn) do { \ - std::fprintf(stderr, " %s ...", #fn); \ - int before = test_failures; \ - fn(); \ - if (test_failures == before) std::fprintf(stderr, " ok\n"); \ - else std::fprintf(stderr, "\n"); \ -} while (0) +namespace { +struct AdaptiveKeepRatioFixture {}; +} static inline bool approx_eq(float a, float b, float eps = 1e-5f) { return std::fabs(a - b) < eps; } -// ─── Tests ──────────────────────────────────────────────────────────────────── - -static void default_construction() { +TEST_CASE(AdaptiveKeepRatioFixture, default_construction) { AdaptiveKeepRatioState s{}; - TEST_ASSERT(approx_eq(s.ema, 0.0f)); - TEST_ASSERT(approx_eq(s.last_keep, 0.10f)); - TEST_ASSERT(s.turn_count == 0); + CHECK(approx_eq(s.ema, 0.0f)); + CHECK(approx_eq(s.last_keep, 0.10f)); + CHECK(s.turn_count == 0); } -static void first_turn_sets_ema_to_observed() { +TEST_CASE(AdaptiveKeepRatioFixture, first_turn_sets_ema_to_observed) { AdaptiveKeepRatioState s{}; - // turn_count == 0 => no smoothing, ema = observed directly auto next = step_adaptive_keep_ratio(s, 0.82f); - TEST_ASSERT_MSG(approx_eq(next.ema, 0.82f), "first-turn EMA must equal observed"); - TEST_ASSERT(next.turn_count == 1); + CHECK(approx_eq(next.ema, 0.82f)); + CHECK(next.turn_count == 1); } -static void high_accept_decreases_keep() { - // observed > kBanditTargetHi (0.85) => keep should decrease +TEST_CASE(AdaptiveKeepRatioFixture, high_accept_decreases_keep) { AdaptiveKeepRatioState s{}; s.turn_count = 1; - s.ema = 0.88f; - s.last_keep = 0.10f; + s.ema = 0.88f; + s.last_keep = 0.10f; auto next = step_adaptive_keep_ratio(s, 0.88f); - TEST_ASSERT_MSG(next.last_keep < s.last_keep, "high accept must decrease keep"); + CHECK(next.last_keep < s.last_keep); } -static void low_accept_increases_keep() { - // observed < kBanditTargetLo (0.75) => keep should increase +TEST_CASE(AdaptiveKeepRatioFixture, low_accept_increases_keep) { AdaptiveKeepRatioState s{}; s.turn_count = 1; - s.ema = 0.65f; - s.last_keep = 0.10f; + s.ema = 0.65f; + s.last_keep = 0.10f; auto next = step_adaptive_keep_ratio(s, 0.65f); - TEST_ASSERT_MSG(next.last_keep > s.last_keep, "low accept must increase keep"); + CHECK(next.last_keep > s.last_keep); } -static void in_band_no_change() { - // 0.75 <= ema <= 0.85 => keep unchanged +TEST_CASE(AdaptiveKeepRatioFixture, in_band_no_change) { AdaptiveKeepRatioState s{}; s.turn_count = 1; - s.ema = 0.80f; - s.last_keep = 0.10f; + s.ema = 0.80f; + s.last_keep = 0.10f; auto next = step_adaptive_keep_ratio(s, 0.80f); - TEST_ASSERT_MSG(approx_eq(next.last_keep, s.last_keep), "in-band keep must be unchanged"); + CHECK(approx_eq(next.last_keep, s.last_keep)); } -static void respects_lower_bound() { - // already at minimum; high accept must not push it below kBanditKeepMin +TEST_CASE(AdaptiveKeepRatioFixture, respects_lower_bound) { AdaptiveKeepRatioState s{}; s.turn_count = 5; - s.ema = 0.95f; - s.last_keep = kBanditKeepMin; + s.ema = 0.95f; + s.last_keep = kBanditKeepMin; auto next = step_adaptive_keep_ratio(s, 0.99f); - TEST_ASSERT_MSG(approx_eq(next.last_keep, kBanditKeepMin), - "keep must not go below kBanditKeepMin"); + CHECK(approx_eq(next.last_keep, kBanditKeepMin)); } -static void respects_upper_bound() { - // already at maximum; low accept must not push it above kBanditKeepMax +TEST_CASE(AdaptiveKeepRatioFixture, respects_upper_bound) { AdaptiveKeepRatioState s{}; s.turn_count = 5; - s.ema = 0.40f; - s.last_keep = kBanditKeepMax; + s.ema = 0.40f; + s.last_keep = kBanditKeepMax; auto next = step_adaptive_keep_ratio(s, 0.40f); - TEST_ASSERT_MSG(approx_eq(next.last_keep, kBanditKeepMax), - "keep must not go above kBanditKeepMax"); + CHECK(approx_eq(next.last_keep, kBanditKeepMax)); } -static void ten_turn_convergence_high_accept() { - // Feeding accept=0.90 ten turns => keep monotonically decreases +TEST_CASE(AdaptiveKeepRatioFixture, ten_turn_convergence_high_accept) { AdaptiveKeepRatioState s{}; float prev_keep = s.last_keep; - bool monotone = true; + bool monotone = true; for (int i = 0; i < 10; ++i) { s = step_adaptive_keep_ratio(s, 0.90f); if (s.last_keep > prev_keep + 1e-6f) { @@ -126,114 +87,70 @@ static void ten_turn_convergence_high_accept() { } prev_keep = s.last_keep; } - TEST_ASSERT_MSG(monotone, "keep must monotonically decrease under persistent high accept"); - TEST_ASSERT_MSG(s.last_keep < 0.10f, "keep must have decreased after 10 high-accept turns"); + CHECK(monotone); + CHECK(s.last_keep < 0.10f); } -static void escalation_far_outside_band() { - // ema > kBanditEscalateHi (0.90) => step is large (0.01), not small (0.005) +TEST_CASE(AdaptiveKeepRatioFixture, escalation_far_outside_band) { AdaptiveKeepRatioState s{}; s.turn_count = 1; - s.ema = 0.92f; - s.last_keep = 0.10f; + s.ema = 0.92f; + s.last_keep = 0.10f; auto next = step_adaptive_keep_ratio(s, 0.92f); float drop = s.last_keep - next.last_keep; - TEST_ASSERT_MSG(approx_eq(drop, kBanditStepLarge, 1e-4f), - "far-above-band must use large step"); + CHECK(approx_eq(drop, kBanditStepLarge, 1e-4f)); } -static void sessions_isolated() { +TEST_CASE(AdaptiveKeepRatioFixture, sessions_isolated) { HttpServerSessions mgr; - // s1 sees high accept => keep decreases mgr.update("s1", 0.90f); - // s2 sees low accept => keep increases mgr.update("s2", 0.50f); float k1 = mgr.get_keep_ratio("s1"); float k2 = mgr.get_keep_ratio("s2"); - TEST_ASSERT_MSG(k1 < k2, - "session with high accept must end up with lower keep than low-accept session"); - TEST_ASSERT(mgr.turn_count("s1") == 1); - TEST_ASSERT(mgr.turn_count("s2") == 1); - TEST_ASSERT(mgr.size() == 2); + CHECK(k1 < k2); + CHECK(mgr.turn_count("s1") == 1); + CHECK(mgr.turn_count("s2") == 1); + CHECK(mgr.size() == 2); } -static void unknown_session_returns_default() { +TEST_CASE(AdaptiveKeepRatioFixture, unknown_session_returns_default) { HttpServerSessions mgr; float k = mgr.get_keep_ratio("no-such-session"); - TEST_ASSERT_MSG(approx_eq(k, AdaptiveKeepRatioState{}.last_keep), - "unknown session must return default keep_ratio"); - TEST_ASSERT(mgr.turn_count("no-such-session") == 0); + CHECK(approx_eq(k, AdaptiveKeepRatioState{}.last_keep)); + CHECK(mgr.turn_count("no-such-session") == 0); } -static void get_ema_reflects_post_update_value() { +TEST_CASE(AdaptiveKeepRatioFixture, get_ema_reflects_post_update_value) { HttpServerSessions mgr; - TEST_ASSERT_MSG(approx_eq(mgr.get_ema("s1"), 0.0f), "unknown session ema is 0"); - // First turn: ema seeds to observed directly + CHECK(approx_eq(mgr.get_ema("s1"), 0.0f)); mgr.update("s1", 0.80f); - TEST_ASSERT_MSG(approx_eq(mgr.get_ema("s1"), 0.80f), "first-turn ema == observed"); - // Second turn: ema = alpha*prev + (1-alpha)*observed + CHECK(approx_eq(mgr.get_ema("s1"), 0.80f)); mgr.update("s1", 0.60f); float expected = kBanditEmaAlpha * 0.80f + (1.0f - kBanditEmaAlpha) * 0.60f; - TEST_ASSERT_MSG(approx_eq(mgr.get_ema("s1"), expected), "second-turn ema correct"); + CHECK(approx_eq(mgr.get_ema("s1"), expected)); } -static void lru_eviction_bounds_map_size() { +TEST_CASE(AdaptiveKeepRatioFixture, lru_eviction_bounds_map_size) { HttpServerSessions mgr; - // Insert kMaxSessions + 100 distinct sessions const std::size_t over = kMaxSessions + 100; for (std::size_t i = 0; i < over; ++i) { mgr.update("sess-" + std::to_string(i), 0.80f); } + CHECK(mgr.size() <= kMaxSessions); - // Map must stay at or below the cap - TEST_ASSERT_MSG(mgr.size() <= kMaxSessions, - "map size must not exceed kMaxSessions after overflow inserts"); - - // The OLDEST sessions (low indices, never touched after insert) must be gone. - // The most recent kMaxSessions inserts are the high-index ones. - // Verify the very first session is evicted. float k0 = mgr.get_keep_ratio("sess-0"); - // A session evicted returns the default keep; one still present returns a - // stepped-down keep (we fed accept=0.80 which is inside the band → keep unchanged). - // We just assert size is bounded; eviction of the oldest is implied by LRU. - TEST_ASSERT_MSG(mgr.size() <= kMaxSessions, - "size still bounded after get_keep_ratio accesses"); - (void)k0; // value used above; suppress unused-variable warning - - // Touch only a few sessions to make them "recently used", then overflow again. - // Those touched sessions must survive a second wave. - const std::string pinned = "sess-" + std::to_string(over - 1); - for (int t = 0; t < 3; ++t) mgr.update(pinned, 0.80f); + CHECK(mgr.size() <= kMaxSessions); + (void) k0; + const std::string pinned = "sess-" + std::to_string(over - 1); + for (int t = 0; t < 3; ++t) { + mgr.update(pinned, 0.80f); + } for (std::size_t i = over; i < over + 200; ++i) { mgr.update("wave2-" + std::to_string(i), 0.80f); } - TEST_ASSERT_MSG(mgr.size() <= kMaxSessions, "size bounded after second wave"); - TEST_ASSERT_MSG(mgr.turn_count(pinned) >= 3, - "recently-used pinned session must survive eviction waves"); -} - -// ─── main ───────────────────────────────────────────────────────────────────── - -int main() { - std::fprintf(stderr, "=== test_adaptive_keep_ratio ===\n"); - - RUN_TEST(default_construction); - RUN_TEST(first_turn_sets_ema_to_observed); - RUN_TEST(high_accept_decreases_keep); - RUN_TEST(low_accept_increases_keep); - RUN_TEST(in_band_no_change); - RUN_TEST(respects_lower_bound); - RUN_TEST(respects_upper_bound); - RUN_TEST(ten_turn_convergence_high_accept); - RUN_TEST(escalation_far_outside_band); - RUN_TEST(sessions_isolated); - RUN_TEST(unknown_session_returns_default); - RUN_TEST(get_ema_reflects_post_update_value); - RUN_TEST(lru_eviction_bounds_map_size); - - std::fprintf(stderr, "\n%d tests, %d failures\n", test_count, test_failures); - return (test_failures == 0) ? 0 : 1; + CHECK(mgr.size() <= kMaxSessions); + CHECK(mgr.turn_count(pinned) >= 3); } diff --git a/server/test/test_admission.cpp b/server/test/test_admission.cpp index b42a512e7..6ee33d789 100644 --- a/server/test/test_admission.cpp +++ b/server/test/test_admission.cpp @@ -1,117 +1,57 @@ // Unit tests for should_reject_oversized — pure, GPU-free. -// Reject iff prompt+max_output > max_ctx AND compression is NOT enabled. -// Build: /usr/bin/g++-11 -std=gnu++17 -O0 -I server/src -o /tmp/test_admission server/test/test_admission.cpp && /tmp/test_admission -#include "server/admission.h" - -#include - -static int test_failures = 0; -static int test_count = 0; -#define TEST_ASSERT(expr) do { \ - test_count++; \ - if (!(expr)) { \ - test_failures++; \ - std::fprintf(stderr, " FAIL: %s:%d: %s\n", \ - __FILE__, __LINE__, #expr); \ - } \ -} while (0) - -#define RUN_TEST(fn) do { \ - std::fprintf(stderr, " %s ...", #fn); \ - int before = test_failures; \ - fn(); \ - std::fprintf(stderr, (test_failures == before) ? " ok\n" : "\n"); \ -} while (0) +#include "CppUnitTestFramework.hpp" +#include "server/admission.h" -// 100+100 <= 1024, no compression -> accept -static void test_small_prompt_no_compression_accepts() { - TEST_ASSERT(!should_reject_oversized(100, 100, 1024, false)); +namespace { +struct AdmissionFixture {}; } -// 900+200 > 1024, no compression -> reject (hard gate preserved) -static void test_oversized_no_compression_rejects() { - TEST_ASSERT(should_reject_oversized(900, 200, 1024, false)); +TEST_CASE(AdmissionFixture, test_small_prompt_no_compression_accepts) { + CHECK(!should_reject_oversized(100, 100, 1024, false)); } -// 167000+2048 > 65536, compression enabled -> accept (post-compress check is gate) -static void test_oversized_with_compression_accepts() { - TEST_ASSERT(!should_reject_oversized(167000, 2048, 65536, true)); +TEST_CASE(AdmissionFixture, test_oversized_no_compression_rejects) { + CHECK(should_reject_oversized(900, 200, 1024, false)); } -// prompt+max_output == max_ctx is not oversized -> accept -static void test_exactly_at_limit_accepts() { - TEST_ASSERT(!should_reject_oversized(1024, 0, 1024, false)); - TEST_ASSERT(!should_reject_oversized(512, 512, 1024, false)); +TEST_CASE(AdmissionFixture, test_oversized_with_compression_accepts) { + CHECK(!should_reject_oversized(167000, 2048, 65536, true)); } -// 1025 > 1024, no compression -> reject -static void test_one_over_limit_no_compression_rejects() { - TEST_ASSERT(should_reject_oversized(1025, 0, 1024, false)); +TEST_CASE(AdmissionFixture, test_exactly_at_limit_accepts) { + CHECK(!should_reject_oversized(1024, 0, 1024, false)); + CHECK(!should_reject_oversized(512, 512, 1024, false)); } -// 1025 > 1024, compression enabled -> accept -static void test_one_over_limit_with_compression_accepts() { - TEST_ASSERT(!should_reject_oversized(1025, 0, 1024, true)); +TEST_CASE(AdmissionFixture, test_one_over_limit_no_compression_rejects) { + CHECK(should_reject_oversized(1025, 0, 1024, false)); } -// ── effective_prompt_overflows tests ─────────────────────────────────────── - -// (a) FlowKV-compressed request, effective_tokens already within budget → no reject. -static void test_effective_overflows_compressed_within_budget() { - // raw=50000, after FlowKV effective=5000, max_output=2048, max_ctx=65536 - TEST_ASSERT(!effective_prompt_overflows(5000, 0, 2048, 65536)); +TEST_CASE(AdmissionFixture, test_one_over_limit_with_compression_accepts) { + CHECK(!should_reject_oversized(1025, 0, 1024, true)); } -// (b) BUG-B: raw-oversized request that is a pFlash full-cache hit (served_from_cache -// tokens=800 which fits) must NOT be rejected. -// This is THE BUG: current code uses effective_tokens (raw=70000) and rejects. -static void test_effective_overflows_full_cache_hit_uses_served_size() { - // raw prompt = 70000 tokens, but full-cache hit stores only 800 compressed tokens. - // max_output=2048, max_ctx=65536. - // Served size 800 + 2048 = 2848 <= 65536 → must NOT overflow. - // BUG: current implementation ignores served size → returns true (false reject). - TEST_ASSERT(!effective_prompt_overflows(70000, 800, 2048, 65536)); +TEST_CASE(AdmissionFixture, test_effective_overflows_compressed_within_budget) { + CHECK(!effective_prompt_overflows(5000, 0, 2048, 65536)); } -// (c) Genuinely oversized post-compress, no cache hit (-1 sentinel) → reject. -static void test_effective_overflows_post_compress_genuinely_oversized() { - // effective=60000, max_output=10000, max_ctx=65536 → 70000 > 65536 → reject. - TEST_ASSERT(effective_prompt_overflows(60000, -1, 10000, 65536)); +TEST_CASE(AdmissionFixture, test_effective_overflows_full_cache_hit_uses_served_size) { + CHECK(!effective_prompt_overflows(70000, 800, 2048, 65536)); } -// (d) Verbatim turn-1 within budget → no reject. -static void test_effective_overflows_verbatim_within_budget() { - // effective=1000, no cache, max_output=2048, max_ctx=65536 → accept. - TEST_ASSERT(!effective_prompt_overflows(1000, -1, 2048, 65536)); +TEST_CASE(AdmissionFixture, test_effective_overflows_post_compress_genuinely_oversized) { + CHECK(effective_prompt_overflows(60000, -1, 10000, 65536)); } -// (f) Degenerate zero-length cache hit must be treated as a hit, not as no-hit. -static void test_effective_overflows_zero_length_hit_is_a_hit() { - // served=0 (valid hit), max_output=2048 → 2048 <= 65536 → accept. - TEST_ASSERT(!effective_prompt_overflows(70000, 0, 2048, 65536)); +TEST_CASE(AdmissionFixture, test_effective_overflows_verbatim_within_budget) { + CHECK(!effective_prompt_overflows(1000, -1, 2048, 65536)); } -// (e) Full-cache hit but served size + max_output itself overflows → reject. -static void test_effective_overflows_full_cache_hit_still_too_large() { - // served=60000, max_output=10000, max_ctx=65536 → 70000 > 65536 → reject. - TEST_ASSERT(effective_prompt_overflows(200000, 60000, 10000, 65536)); +TEST_CASE(AdmissionFixture, test_effective_overflows_zero_length_hit_is_a_hit) { + CHECK(!effective_prompt_overflows(70000, 0, 2048, 65536)); } -int main() { - std::fprintf(stderr, "=== test_admission ===\n"); - RUN_TEST(test_small_prompt_no_compression_accepts); - RUN_TEST(test_oversized_no_compression_rejects); - RUN_TEST(test_oversized_with_compression_accepts); - RUN_TEST(test_exactly_at_limit_accepts); - RUN_TEST(test_one_over_limit_no_compression_rejects); - RUN_TEST(test_one_over_limit_with_compression_accepts); - RUN_TEST(test_effective_overflows_compressed_within_budget); - RUN_TEST(test_effective_overflows_full_cache_hit_uses_served_size); - RUN_TEST(test_effective_overflows_post_compress_genuinely_oversized); - RUN_TEST(test_effective_overflows_verbatim_within_budget); - RUN_TEST(test_effective_overflows_full_cache_hit_still_too_large); - RUN_TEST(test_effective_overflows_zero_length_hit_is_a_hit); - std::fprintf(stderr, "\n%d tests, %d failures\n", test_count, test_failures); - return (test_failures == 0) ? 0 : 1; +TEST_CASE(AdmissionFixture, test_effective_overflows_full_cache_hit_still_too_large) { + CHECK(effective_prompt_overflows(200000, 60000, 10000, 65536)); } diff --git a/server/test/test_anchor_params.cpp b/server/test/test_anchor_params.cpp index 01d76febd..285d945a0 100644 --- a/server/test/test_anchor_params.cpp +++ b/server/test/test_anchor_params.cpp @@ -1,71 +1,46 @@ // Unit tests for resolve_anchor_params() — no GPU, no model files. -// -// Build: cmake --build build --target test_anchor_params -j -// Run: ./build/test_anchor_params +#include "CppUnitTestFramework.hpp" #include "qwen3/anchor_params.h" -#include - using namespace dflash::common; -static int failures = 0; -static int checks = 0; - -#define CHECK_EQ(a, b) do { \ - checks++; \ - if ((a) != (b)) { \ - failures++; \ - std::fprintf(stderr, " FAIL %s:%d: %d != %d\n", __FILE__, __LINE__, (a), (b)); \ - } \ -} while (0) +namespace { +struct AnchorParamsFixture {}; +} -static void test_tier_boundaries() { +TEST_CASE(AnchorParamsFixture, test_tier_boundaries) { // tier 0: n_chunks < 1024 -> {2,8} - { auto p = resolve_anchor_params(0, -1,-1, -1,-1); CHECK_EQ(p.radius,2); CHECK_EQ(p.max_hits,8); } - { auto p = resolve_anchor_params(1, -1,-1, -1,-1); CHECK_EQ(p.radius,2); CHECK_EQ(p.max_hits,8); } - { auto p = resolve_anchor_params(1023, -1,-1, -1,-1); CHECK_EQ(p.radius,2); CHECK_EQ(p.max_hits,8); } + { auto p = resolve_anchor_params(0, -1,-1, -1,-1); CHECK_EQUAL(p.radius, 2); CHECK_EQUAL(p.max_hits, 8); } + { auto p = resolve_anchor_params(1, -1,-1, -1,-1); CHECK_EQUAL(p.radius, 2); CHECK_EQUAL(p.max_hits, 8); } + { auto p = resolve_anchor_params(1023, -1,-1, -1,-1); CHECK_EQUAL(p.radius, 2); CHECK_EQUAL(p.max_hits, 8); } // tier 1: 1024 <= n_chunks < 2048 -> {4,16} - { auto p = resolve_anchor_params(1024, -1,-1, -1,-1); CHECK_EQ(p.radius,4); CHECK_EQ(p.max_hits,16); } - { auto p = resolve_anchor_params(2047, -1,-1, -1,-1); CHECK_EQ(p.radius,4); CHECK_EQ(p.max_hits,16); } + { auto p = resolve_anchor_params(1024, -1,-1, -1,-1); CHECK_EQUAL(p.radius, 4); CHECK_EQUAL(p.max_hits, 16); } + { auto p = resolve_anchor_params(2047, -1,-1, -1,-1); CHECK_EQUAL(p.radius, 4); CHECK_EQUAL(p.max_hits, 16); } // tier 2: n_chunks >= 2048 -> {8,32} - { auto p = resolve_anchor_params(2048, -1,-1, -1,-1); CHECK_EQ(p.radius,8); CHECK_EQ(p.max_hits,32); } - { auto p = resolve_anchor_params(4096, -1,-1, -1,-1); CHECK_EQ(p.radius,8); CHECK_EQ(p.max_hits,32); } + { auto p = resolve_anchor_params(2048, -1,-1, -1,-1); CHECK_EQUAL(p.radius, 8); CHECK_EQUAL(p.max_hits, 32); } + { auto p = resolve_anchor_params(4096, -1,-1, -1,-1); CHECK_EQUAL(p.radius, 8); CHECK_EQUAL(p.max_hits, 32); } } -static void test_legacy_env_overrides_tier() { +TEST_CASE(AnchorParamsFixture, test_legacy_env_overrides_tier) { // legacy (DFLASH) env wins over tier default - { auto p = resolve_anchor_params(500, -1,-1, 5, 20); CHECK_EQ(p.radius,5); CHECK_EQ(p.max_hits,20); } - { auto p = resolve_anchor_params(2048, -1,-1, 1, 4); CHECK_EQ(p.radius,1); CHECK_EQ(p.max_hits,4); } + { auto p = resolve_anchor_params(500, -1,-1, 5, 20); CHECK_EQUAL(p.radius, 5); CHECK_EQUAL(p.max_hits, 20); } + { auto p = resolve_anchor_params(2048, -1,-1, 1, 4); CHECK_EQUAL(p.radius, 1); CHECK_EQUAL(p.max_hits, 4); } } -static void test_pflash_env_wins_over_legacy() { +TEST_CASE(AnchorParamsFixture, test_pflash_env_wins_over_legacy) { // PFLASH env (env_*) wins over legacy (legacy_*) which wins over tier - { auto p = resolve_anchor_params(500, 10, 40, 5, 20); CHECK_EQ(p.radius,10); CHECK_EQ(p.max_hits,40); } - { auto p = resolve_anchor_params(2048, 3, 12, 1, 4); CHECK_EQ(p.radius,3); CHECK_EQ(p.max_hits,12); } + { auto p = resolve_anchor_params(500, 10, 40, 5, 20); CHECK_EQUAL(p.radius, 10); CHECK_EQUAL(p.max_hits, 40); } + { auto p = resolve_anchor_params(2048, 3, 12, 1, 4); CHECK_EQUAL(p.radius, 3); CHECK_EQUAL(p.max_hits, 12); } } -static void test_sentinel_minus1_falls_through_to_tier() { +TEST_CASE(AnchorParamsFixture, test_sentinel_minus1_falls_through_to_tier) { // -1 means unset; both sentinels -> tier - { auto p = resolve_anchor_params(1023, -1,-1, -1,-1); CHECK_EQ(p.radius,2); CHECK_EQ(p.max_hits,8); } - { auto p = resolve_anchor_params(1024, -1,-1, -1,-1); CHECK_EQ(p.radius,4); CHECK_EQ(p.max_hits,16); } + { auto p = resolve_anchor_params(1023, -1,-1, -1,-1); CHECK_EQUAL(p.radius, 2); CHECK_EQUAL(p.max_hits, 8); } + { auto p = resolve_anchor_params(1024, -1,-1, -1,-1); CHECK_EQUAL(p.radius, 4); CHECK_EQUAL(p.max_hits, 16); } } -static void test_zero_is_valid_override() { +TEST_CASE(AnchorParamsFixture, test_zero_is_valid_override) { // 0 is a valid override (>= 0), not sentinel - { auto p = resolve_anchor_params(2048, 0, 0, -1,-1); CHECK_EQ(p.radius,0); CHECK_EQ(p.max_hits,0); } -} - -int main() { - std::fprintf(stderr, "test_anchor_params\n"); - test_tier_boundaries(); - test_legacy_env_overrides_tier(); - test_pflash_env_wins_over_legacy(); - test_sentinel_minus1_falls_through_to_tier(); - test_zero_is_valid_override(); - if (failures == 0) - std::fprintf(stdout, "PASS %d checks\n", checks); - else - std::fprintf(stdout, "FAIL %d/%d checks failed\n", failures, checks); - return failures ? 1 : 0; + { auto p = resolve_anchor_params(2048, 0, 0, -1,-1); CHECK_EQUAL(p.radius, 0); CHECK_EQUAL(p.max_hits, 0); } } diff --git a/server/test/test_anchor_transitive.cpp b/server/test/test_anchor_transitive.cpp index dc87b24c5..01090ad02 100644 --- a/server/test/test_anchor_transitive.cpp +++ b/server/test/test_anchor_transitive.cpp @@ -1,6 +1,7 @@ // TDD: anchor transitive multi-pass. Pure CPU — no GPU, no model load. // T1: single-pass match; T2: single-pass misses hops; T3: transitive rescues all hops. +#include "CppUnitTestFramework.hpp" #include "../src/qwen3/anchor_scan.h" #include @@ -8,11 +9,18 @@ #include #include -#define REQUIRE(cond) \ - do { if (!(cond)) { \ - std::fprintf(stderr, "FAIL: %s line %d: %s\n", __FILE__, __LINE__, #cond); \ - std::exit(1); \ - } } while (0) +#define TEST_ASSERT(cond) do { \ + auto _cpputf_exception = CppUnitTestFramework::Assert::IsTrue(static_cast(cond), #cond); \ + if (_cpputf_exception) { \ + throw *_cpputf_exception; \ + } \ +} while (0) +#undef REQUIRE +#define REQUIRE(cond) TEST_ASSERT(cond) + +namespace { +struct AnchorTransitiveFixture {}; +} static constexpr int32_t FILLER = 1; static constexpr int32_t M1 = 1001, M2 = 1002, M3 = 1003; @@ -338,7 +346,7 @@ static void t6_hard_cap_prevents_runaway() { total_forced); } -int main() { +TEST_CASE(AnchorTransitiveFixture, transitive_anchor_suite) { t1_single_pass_match(); t2_single_pass_misses_hops(); t3_transitive_rescues_all(); @@ -346,5 +354,4 @@ int main() { t5_gate_closes_when_pass1_finds_many(); t6_hard_cap_prevents_runaway(); std::printf("\nAll anchor_transitive tests passed.\n"); - return 0; } diff --git a/server/test/test_bandit_integration.cpp b/server/test/test_bandit_integration.cpp index 7a8911ab2..0453db70c 100644 --- a/server/test/test_bandit_integration.cpp +++ b/server/test/test_bandit_integration.cpp @@ -1,200 +1,103 @@ // Integration tests: adaptive bandit wired into HttpServer request path. -// No GPU, no model files — uses a synchronous MockBackend that returns -// a configurable accept_rate. -// -// Build: cmake --build dflash/build --target test_bandit_integration -j -// Run: cd dflash/build && ./test_bandit_integration +#include "CppUnitTestFramework.hpp" #include "server/http_server.h" #include "server/adaptive_keep_ratio.h" #include -#include #include using namespace dflash::common; -// ─── Test framework (ds4 style) ────────────────────────────────────────────── - -static int test_failures = 0; -static int test_count = 0; - -#define TEST_ASSERT(expr) do { \ - test_count++; \ - if (!(expr)) { \ - test_failures++; \ - std::fprintf(stderr, " FAIL: %s:%d: %s\n", __FILE__, __LINE__, #expr); \ - } \ -} while (0) - -#define TEST_ASSERT_MSG(expr, msg) do { \ - test_count++; \ - if (!(expr)) { \ - test_failures++; \ - std::fprintf(stderr, " FAIL: %s:%d: %s -- %s\n", __FILE__, __LINE__, #expr, msg); \ - } \ -} while (0) - -#define RUN_TEST(fn) do { \ - std::fprintf(stderr, " %s ...", #fn); \ - int before = test_failures; \ - fn(); \ - if (test_failures == before) std::fprintf(stderr, " ok\n"); \ - else std::fprintf(stderr, "\n"); \ -} while (0) +namespace { +struct BanditIntegrationFixture {}; +} static inline bool approx_eq(float a, float b, float eps = 1e-5f) { return std::fabs(a - b) < eps; } -// ─── Tests for HttpServerSessions (the integration contract) ───────────────── - -// Test 1: Three-turn session with high accept_rate should decrease keep_ratio. -// This mirrors "three_turn_session_evolves_keep_ratio". -static void three_turn_session_evolves_keep_ratio() { +TEST_CASE(BanditIntegrationFixture, three_turn_session_evolves_keep_ratio) { HttpServerSessions sessions; - - // Initial keep ratio (default prior = 0.10) float k0 = sessions.get_keep_ratio("s1"); - TEST_ASSERT_MSG(approx_eq(k0, AdaptiveKeepRatioState{}.last_keep), - "initial keep should be the default prior"); + CHECK(approx_eq(k0, AdaptiveKeepRatioState{}.last_keep)); - // Turn 1: high accept => next keep should drop sessions.update("s1", 0.95f); float k1 = sessions.get_keep_ratio("s1"); - - // Turn 2: same high accept => keep drops further sessions.update("s1", 0.95f); float k2 = sessions.get_keep_ratio("s1"); - - // Turn 3: same sessions.update("s1", 0.95f); float k3 = sessions.get_keep_ratio("s1"); - TEST_ASSERT_MSG(k1 < k0, "turn 1 keep must be less than initial for high accept"); - TEST_ASSERT_MSG(k2 <= k1, "turn 2 keep must not exceed turn 1 under high accept"); - TEST_ASSERT_MSG(k3 <= k2, "turn 3 keep must not exceed turn 2 under high accept"); - TEST_ASSERT(sessions.turn_count("s1") == 3); + CHECK(k1 < k0); + CHECK(k2 <= k1); + CHECK(k3 <= k2); + CHECK(sessions.turn_count("s1") == 3); } -// Test 2: Request without session_id uses config default (no bandit mutation). -// We verify that the sessions map stays empty when no session_id is used. -static void no_session_id_uses_static_default() { +TEST_CASE(BanditIntegrationFixture, no_session_id_uses_static_default) { HttpServerSessions sessions; - - // Never call update with empty key — this simulates the "no session_id" path. - // The server code guards: if (session_id.empty()) skip bandit. - // So sessions stays empty and get_keep_ratio("") returns the default. - TEST_ASSERT(sessions.size() == 0); - // If someone queries with empty string (shouldn't happen), they get default. + CHECK(sessions.size() == 0); float k = sessions.get_keep_ratio(""); - TEST_ASSERT_MSG(approx_eq(k, AdaptiveKeepRatioState{}.last_keep), - "empty session_id must return default keep_ratio"); + CHECK(approx_eq(k, AdaptiveKeepRatioState{}.last_keep)); } -// Test 3: Two sessions with different accept rates stay isolated. -// High-accept session ends up with lower keep than low-accept session. -static void isolated_sessions() { +TEST_CASE(BanditIntegrationFixture, isolated_sessions) { HttpServerSessions sessions; - - // Session A: accept = 0.95 (high) → keep should decrease sessions.update("high_accept", 0.95f); - - // Session B: accept = 0.50 (low) → keep should increase sessions.update("low_accept", 0.50f); float k_high = sessions.get_keep_ratio("high_accept"); - float k_low = sessions.get_keep_ratio("low_accept"); - - TEST_ASSERT_MSG(k_high < k_low, - "session with high accept must have lower keep than low-accept session"); - TEST_ASSERT(sessions.turn_count("high_accept") == 1); - TEST_ASSERT(sessions.turn_count("low_accept") == 1); - TEST_ASSERT(sessions.size() == 2); + float k_low = sessions.get_keep_ratio("low_accept"); + CHECK(k_high < k_low); + CHECK(sessions.turn_count("high_accept") == 1); + CHECK(sessions.turn_count("low_accept") == 1); + CHECK(sessions.size() == 2); } -// Test 4: Multi-turn convergence — with persistent high accept the ratio -// reaches the lower bound and stays there. -static void multi_turn_reaches_lower_bound() { +TEST_CASE(BanditIntegrationFixture, multi_turn_reaches_lower_bound) { HttpServerSessions sessions; - - // Drive 100 turns with accept=1.0 for (int i = 0; i < 100; ++i) { sessions.update("s_hi", 1.0f); } float k = sessions.get_keep_ratio("s_hi"); - TEST_ASSERT_MSG(k >= kBanditKeepMin - 1e-5f, - "keep must not fall below kBanditKeepMin"); + CHECK(k >= kBanditKeepMin - 1e-5f); } -// Test 5: Multi-turn convergence with low accept reaches the upper bound. -static void multi_turn_reaches_upper_bound() { +TEST_CASE(BanditIntegrationFixture, multi_turn_reaches_upper_bound) { HttpServerSessions sessions; - for (int i = 0; i < 100; ++i) { sessions.update("s_lo", 0.0f); } float k = sessions.get_keep_ratio("s_lo"); - TEST_ASSERT_MSG(k <= kBanditKeepMax + 1e-5f, - "keep must not exceed kBanditKeepMax"); + CHECK(k <= kBanditKeepMax + 1e-5f); } -// Test 6: Zero accept_rate with spec_decode_ran=true MUST update the bandit. -// Previously, the guard was accept_rate>0, which silently skipped 0-accept -// sessions — exactly the case where the bandit most needs to act (push keep up). -// The fix uses spec_decode_ran as the gate; this test exercises the session layer -// directly: update() with 0.0 must drive keep_ratio toward kBanditKeepMax. -static void zero_accept_drives_keep_up() { +TEST_CASE(BanditIntegrationFixture, zero_accept_drives_keep_up) { HttpServerSessions sessions; - float k0 = sessions.get_keep_ratio("s1"); - // Simulate server calling update() because spec_decode_ran==true, accept==0 sessions.update("s1", 0.0f); float k1 = sessions.get_keep_ratio("s1"); - TEST_ASSERT(k1 >= kBanditKeepMin && k1 <= kBanditKeepMax); - TEST_ASSERT_MSG(k1 > k0, "zero accept must increase keep_ratio"); - TEST_ASSERT(sessions.turn_count("s1") == 1); + CHECK(k1 >= kBanditKeepMin && k1 <= kBanditKeepMax); + CHECK(k1 > k0); + CHECK(sessions.turn_count("s1") == 1); } -// ─── Tests for parse_session_id_from_body (non-string guard) ───────────────── - -// Test 7: session_id as integer in extra_body => empty (no type_error) -static void non_string_session_id_integer_extra_body() { +TEST_CASE(BanditIntegrationFixture, non_string_session_id_integer_extra_body) { json body = {{"extra_body", {{"session_id", 42}}}}; std::string sid = parse_session_id_from_body(body); - TEST_ASSERT_MSG(sid.empty(), "integer session_id in extra_body must yield empty string"); + CHECK(sid.empty()); } -// Test 8: session_id as null at top level => empty (no type_error) -static void non_string_session_id_null_top_level() { +TEST_CASE(BanditIntegrationFixture, non_string_session_id_null_top_level) { json body = {{"session_id", nullptr}}; std::string sid = parse_session_id_from_body(body); - TEST_ASSERT_MSG(sid.empty(), "null session_id at top level must yield empty string"); + CHECK(sid.empty()); } -// Test 9: session_id as array in extra_body => empty (no type_error) -static void non_string_session_id_array_extra_body() { +TEST_CASE(BanditIntegrationFixture, non_string_session_id_array_extra_body) { json body = {{"extra_body", {{"session_id", json::array({"a", "b"})}}}}; std::string sid = parse_session_id_from_body(body); - TEST_ASSERT_MSG(sid.empty(), "array session_id in extra_body must yield empty string"); -} - -// ─── main ──────────────────────────────────────────────────────────────────── - -int main() { - std::fprintf(stderr, "=== test_bandit_integration ===\n"); - - RUN_TEST(three_turn_session_evolves_keep_ratio); - RUN_TEST(no_session_id_uses_static_default); - RUN_TEST(isolated_sessions); - RUN_TEST(multi_turn_reaches_lower_bound); - RUN_TEST(multi_turn_reaches_upper_bound); - RUN_TEST(zero_accept_drives_keep_up); - RUN_TEST(non_string_session_id_integer_extra_body); - RUN_TEST(non_string_session_id_null_top_level); - RUN_TEST(non_string_session_id_array_extra_body); - - std::fprintf(stderr, "\n%d tests, %d failures\n", test_count, test_failures); - return (test_failures == 0) ? 0 : 1; + CHECK(sid.empty()); } diff --git a/server/test/test_chain_rollback_policy.cpp b/server/test/test_chain_rollback_policy.cpp index fad6e7e4f..5c679e526 100644 --- a/server/test/test_chain_rollback_policy.cpp +++ b/server/test/test_chain_rollback_policy.cpp @@ -1,3 +1,4 @@ +#include "CppUnitTestFramework.hpp" #include "chain_rollback_policy.h" #include @@ -8,14 +9,9 @@ using dflash::common::resolve_chain_rollback_policy; using dflash::common::RollbackDiag; -static int failures = 0; - -#define CHECK(expr) do { \ - if (!(expr)) { \ - std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #expr); \ - ++failures; \ - } \ -} while (0) +namespace { +struct ChainRollbackPolicyFixture {}; +} static void clear_policy_env() { unsetenv("DFLASH_SINGLE_CHAIN_CHECKPOINT_F32"); @@ -23,14 +19,13 @@ static void clear_policy_env() { unsetenv("DFLASH_SINGLE_CHAIN_ROLLBACK_DIAG"); } -int main() { +TEST_CASE(ChainRollbackPolicyFixture, policy_defaults_and_env_parsing) { clear_policy_env(); auto policy = resolve_chain_rollback_policy(); CHECK(!policy.checkpoint_f32); CHECK(policy.fast_rollback_threshold == 5); CHECK(!policy.diagnostics); - // A threshold flag alone must not alter the established F16 policy. setenv("DFLASH_FAST_ROLLBACK_THRESHOLD", "2", 1); policy = resolve_chain_rollback_policy(); CHECK(!policy.checkpoint_f32); @@ -41,7 +36,6 @@ int main() { CHECK(policy.checkpoint_f32); CHECK(policy.fast_rollback_threshold == 2); - // Boolean flags follow the project's non-empty, non-"0" convention. setenv("DFLASH_SINGLE_CHAIN_CHECKPOINT_F32", "true", 1); CHECK(resolve_chain_rollback_policy().checkpoint_f32); setenv("DFLASH_SINGLE_CHAIN_CHECKPOINT_F32", "yes", 1); @@ -52,7 +46,6 @@ int main() { CHECK(!resolve_chain_rollback_policy().checkpoint_f32); setenv("DFLASH_SINGLE_CHAIN_CHECKPOINT_F32", "1", 1); - // Invalid values degrade safely to the default threshold. setenv("DFLASH_FAST_ROLLBACK_THRESHOLD", "0", 1); CHECK(resolve_chain_rollback_policy().fast_rollback_threshold == 5); setenv("DFLASH_FAST_ROLLBACK_THRESHOLD", "6", 1); @@ -62,63 +55,60 @@ int main() { setenv("DFLASH_SINGLE_CHAIN_ROLLBACK_DIAG", "1", 1); CHECK(resolve_chain_rollback_policy().diagnostics); + clear_policy_env(); +} - // Shared diagnostics accumulator used by both spec-decode loops. - { - RollbackDiag diag; - diag.record_accept(1); - diag.record_accept(3); - diag.record_accept(7); - diag.record_accept(40); // clamps into the 16+ bucket - diag.record_fast_rollback(3); // below the F16 breakeven - diag.record_fast_rollback(7); // at/above the breakeven - diag.record_legacy_replay(); - diag.record_failed_fallback(); - CHECK(diag.accept_hist[1] == 1); - CHECK(diag.accept_hist[3] == 1); - CHECK(diag.accept_hist[7] == 1); - CHECK(diag.accept_hist[16] == 1); - CHECK(diag.fast_low == 1); - CHECK(diag.fast_high == 1); - CHECK(diag.legacy_replay == 1); - CHECK(diag.failed_fallback == 1); - - auto print_to_string = [](const RollbackDiag & d) { - std::string text; - std::FILE * f = tmpfile(); - if (!f) return text; - const auto policy = resolve_chain_rollback_policy(); - d.print(policy, f); - long n = std::ftell(f); - std::rewind(f); - text.resize(n > 0 ? (size_t)n : 0); - if (n > 0 && std::fread(&text[0], 1, (size_t)n, f) != (size_t)n) text.clear(); - std::fclose(f); +TEST_CASE(ChainRollbackPolicyFixture, diagnostics_accumulator_and_print_contract) { + clear_policy_env(); + setenv("DFLASH_SINGLE_CHAIN_ROLLBACK_DIAG", "1", 1); + + RollbackDiag diag; + diag.record_accept(1); + diag.record_accept(3); + diag.record_accept(7); + diag.record_accept(40); + diag.record_fast_rollback(3); + diag.record_fast_rollback(7); + diag.record_legacy_replay(); + diag.record_failed_fallback(); + CHECK(diag.accept_hist[1] == 1); + CHECK(diag.accept_hist[3] == 1); + CHECK(diag.accept_hist[7] == 1); + CHECK(diag.accept_hist[16] == 1); + CHECK(diag.fast_low == 1); + CHECK(diag.fast_high == 1); + CHECK(diag.legacy_replay == 1); + CHECK(diag.failed_fallback == 1); + + auto print_to_string = [](const RollbackDiag & d) { + std::string text; + std::FILE * f = tmpfile(); + if (!f) { return text; - }; - - // Diagnostics enabled above: the shared print emits the exact line - // format the validation harness greps for. - setenv("DFLASH_SINGLE_CHAIN_CHECKPOINT_F32", "1", 1); - setenv("DFLASH_FAST_ROLLBACK_THRESHOLD", "1", 1); - const std::string line = print_to_string(diag); - CHECK(line == - "[chain-rollback-policy] checkpoint=F32 threshold=1 fast_low=1 fast_high=1 " - "legacy_replay=1 failed_fallback=1 " - "accept_hist=1:1,2:0,3:1,4:0,5:0,6:0,7:1,8:0,9:0,10:0,11:0,12:0,13:0,14:0,15:0,16+:1\n"); - - // A failed fopen/tmpfile may provide a null stream. Diagnostics must - // remain a no-op rather than forwarding nullptr to std::fprintf. - diag.print(resolve_chain_rollback_policy(), nullptr); - - // print() is a no-op when diagnostics are disabled. - unsetenv("DFLASH_SINGLE_CHAIN_ROLLBACK_DIAG"); - CHECK(print_to_string(diag).empty()); - setenv("DFLASH_SINGLE_CHAIN_ROLLBACK_DIAG", "1", 1); - } + } + const auto policy = resolve_chain_rollback_policy(); + d.print(policy, f); + long n = std::ftell(f); + std::rewind(f); + text.resize(n > 0 ? (size_t) n : 0); + if (n > 0 && std::fread(&text[0], 1, (size_t) n, f) != (size_t) n) { + text.clear(); + } + std::fclose(f); + return text; + }; + + setenv("DFLASH_SINGLE_CHAIN_CHECKPOINT_F32", "1", 1); + setenv("DFLASH_FAST_ROLLBACK_THRESHOLD", "1", 1); + const std::string line = print_to_string(diag); + CHECK(line == + "[chain-rollback-policy] checkpoint=F32 threshold=1 fast_low=1 fast_high=1 " + "legacy_replay=1 failed_fallback=1 " + "accept_hist=1:1,2:0,3:1,4:0,5:0,6:0,7:1,8:0,9:0,10:0,11:0,12:0,13:0,14:0,15:0,16+:1\n"); + diag.print(resolve_chain_rollback_policy(), nullptr); + + unsetenv("DFLASH_SINGLE_CHAIN_ROLLBACK_DIAG"); + CHECK(print_to_string(diag).empty()); clear_policy_env(); - if (failures != 0) return 1; - std::printf("chain rollback policy tests passed\n"); - return 0; } diff --git a/server/test/test_cuda_pool_shutdown.cpp b/server/test/test_cuda_pool_shutdown.cpp index 20ec4423c..cf112cfd6 100644 --- a/server/test/test_cuda_pool_shutdown.cpp +++ b/server/test/test_cuda_pool_shutdown.cpp @@ -1,3 +1,4 @@ +#include "CppUnitTestFramework.hpp" #include "ggml-backend.h" #include "ggml-cuda.h" #include "ggml.h" @@ -7,7 +8,18 @@ #include #include -int main() { +namespace { +struct CudaPoolShutdownFixture {}; +} + +#define TEST_ASSERT(cond) do { \ + auto _cpputf_exception = CppUnitTestFramework::Assert::IsTrue(static_cast(cond), #cond); \ + if (_cpputf_exception) { \ + throw *_cpputf_exception; \ + } \ +} while (0) + +TEST_CASE(CudaPoolShutdownFixture, backend_pool_shutdown) { #if defined(_WIN32) _putenv_s("LUCE_Q8_MEMO", "1"); #else @@ -17,7 +29,7 @@ int main() { ggml_backend_t backend = ggml_backend_cuda_init(0); if (!backend) { std::fprintf(stderr, "failed to initialize CUDA/HIP backend\n"); - return 1; + TEST_ASSERT(false); } ggml_init_params params{}; @@ -26,7 +38,7 @@ int main() { ggml_context * ctx = ggml_init(params); if (!ctx) { ggml_backend_free(backend); - return 1; + TEST_ASSERT(false); } constexpr int64_t k = 256; @@ -44,7 +56,7 @@ int main() { if (!buffer) { ggml_free(ctx); ggml_backend_free(backend); - return 1; + TEST_ASSERT(false); } std::vector weights_data(ggml_nbytes(weights), 0); @@ -57,11 +69,11 @@ int main() { ggml_free(ctx); if (status != GGML_STATUS_SUCCESS) { ggml_backend_free(backend); - return 1; + TEST_ASSERT(false); } // LUCE_Q8_MEMO intentionally retains the pool allocation after compute. // Backend teardown must release that allocation before destroying its pool. ggml_backend_free(backend); - return 0; + TEST_ASSERT(true); } diff --git a/server/test/test_derived_scalars.cpp b/server/test/test_derived_scalars.cpp index c0e4ae07a..cc962ff5d 100644 --- a/server/test/test_derived_scalars.cpp +++ b/server/test/test_derived_scalars.cpp @@ -1,98 +1,67 @@ // Unit tests for dflash::common::verify_derived_scalars — no GPU, no model files. -// -// Build: cmake --build build --target test_derived_scalars -j -// Run: cd build && ctest -R derived_scalars --output-on-failure +#include "CppUnitTestFramework.hpp" #include "common/derived_scalars.h" -#include #include using namespace dflash::common; -// ─── Minimal test framework ──────────────────────────────────────────────────── - -static int test_failures = 0; -static int test_count = 0; - -#define TEST_ASSERT(expr) do { \ - test_count++; \ - if (!(expr)) { \ - test_failures++; \ - std::fprintf(stderr, " FAIL: %s:%d: %s\n", __FILE__, __LINE__, #expr); \ - } \ -} while (0) - -#define RUN_TEST(fn) do { \ - std::fprintf(stderr, " %s ...", #fn); \ - int before = test_failures; \ - fn(); \ - if (test_failures == before) std::fprintf(stderr, " ok\n"); \ - else std::fprintf(stderr, "\n"); \ -} while (0) - -// ─── Tests ───────────────────────────────────────────────────────────────────── +namespace { +struct DerivedScalarsFixture {}; +} -// All three dims match: returns true, err untouched. -static void match_returns_true() { +TEST_CASE(DerivedScalarsFixture, match_returns_true) { std::string err; bool ok = verify_derived_scalars( /*wq_ne1*/ 4096, /*wk_ne1*/ 512, /*wq_ne0*/ 5120, /*exp_q_dim*/ 4096, /*exp_kv_dim*/ 512, /*exp_n_embd*/ 5120, "blk.0", err); - TEST_ASSERT(ok); - TEST_ASSERT(err.empty()); + CHECK(ok); + CHECK(err.empty()); } -// wq_ne1 != expected_q_dim: returns false and err non-empty. -static void mismatch_q_dim_returns_false() { +TEST_CASE(DerivedScalarsFixture, mismatch_q_dim_returns_false) { std::string err; bool ok = verify_derived_scalars( - /*wq_ne1*/ 4096 + 1, /*wk_ne1*/ 512, /*wq_ne0*/ 5120, + /*wq_ne1*/ 4097, /*wk_ne1*/ 512, /*wq_ne0*/ 5120, /*exp_q_dim*/ 4096, /*exp_kv_dim*/ 512, /*exp_n_embd*/ 5120, "blk.0", err); - TEST_ASSERT(!ok); - TEST_ASSERT(!err.empty()); + CHECK(!ok); + CHECK(!err.empty()); } -// wk_ne1 != expected_kv_dim: returns false. -static void mismatch_kv_dim_returns_false() { +TEST_CASE(DerivedScalarsFixture, mismatch_kv_dim_returns_false) { std::string err; bool ok = verify_derived_scalars( - /*wq_ne1*/ 4096, /*wk_ne1*/ 512 + 1, /*wq_ne0*/ 5120, + /*wq_ne1*/ 4096, /*wk_ne1*/ 513, /*wq_ne0*/ 5120, /*exp_q_dim*/ 4096, /*exp_kv_dim*/ 512, /*exp_n_embd*/ 5120, "blk.0", err); - TEST_ASSERT(!ok); - TEST_ASSERT(!err.empty()); + CHECK(!ok); + CHECK(!err.empty()); } -// wq_ne0 != expected_n_embd: returns false. -static void mismatch_n_embd_returns_false() { +TEST_CASE(DerivedScalarsFixture, mismatch_n_embd_returns_false) { std::string err; bool ok = verify_derived_scalars( - /*wq_ne1*/ 4096, /*wk_ne1*/ 512, /*wq_ne0*/ 5120 + 64, + /*wq_ne1*/ 4096, /*wk_ne1*/ 512, /*wq_ne0*/ 5184, /*exp_q_dim*/ 4096, /*exp_kv_dim*/ 512, /*exp_n_embd*/ 5120, "blk.0", err); - TEST_ASSERT(!ok); - TEST_ASSERT(!err.empty()); + CHECK(!ok); + CHECK(!err.empty()); } -// Typical draft model dims: n_head=32, head_dim=128, n_head_kv=8, n_embd=5120. -// expected_q_dim=32*128=4096, expected_kv_dim=8*128=1024. -static void draft_dims_match() { +TEST_CASE(DerivedScalarsFixture, draft_dims_match) { std::string err; bool ok = verify_derived_scalars( 4096, 1024, 5120, (int64_t)32 * 128, (int64_t)8 * 128, 5120, "blk.0", err); - TEST_ASSERT(ok); - TEST_ASSERT(err.empty()); + CHECK(ok); + CHECK(err.empty()); } -// Typical qwen35 target layer: n_head=24, n_embd_head_k=256, n_head_kv=4. -// expected_q_dim = 24*256*2 = 12288 (Q+gate packed). -// expected_kv_dim = 4*256 = 1024. -static void qwen35_target_dims_match() { +TEST_CASE(DerivedScalarsFixture, qwen35_target_dims_match) { std::string err; bool ok = verify_derived_scalars( /*wq_ne1*/ 12288, /*wk_ne1*/ 1024, /*wq_ne0*/ 5120, @@ -100,33 +69,12 @@ static void qwen35_target_dims_match() { /*exp_kv_dim*/ (int64_t)4 * 256, /*exp_n_embd*/ 5120, "blk.3", err); - TEST_ASSERT(ok); - TEST_ASSERT(err.empty()); + CHECK(ok); + CHECK(err.empty()); } -// Error message must contain the layer tag for easy diagnosis. -static void err_contains_layer_tag() { +TEST_CASE(DerivedScalarsFixture, err_contains_layer_tag) { std::string err; - verify_derived_scalars( - 4097, 1024, 5120, - 4096, 1024, 5120, - "blk.15", err); - TEST_ASSERT(err.find("blk.15") != std::string::npos); -} - -// ─── main ────────────────────────────────────────────────────────────────────── - -int main() { - std::fprintf(stderr, "=== test_derived_scalars ===\n"); - - RUN_TEST(match_returns_true); - RUN_TEST(mismatch_q_dim_returns_false); - RUN_TEST(mismatch_kv_dim_returns_false); - RUN_TEST(mismatch_n_embd_returns_false); - RUN_TEST(draft_dims_match); - RUN_TEST(qwen35_target_dims_match); - RUN_TEST(err_contains_layer_tag); - - std::fprintf(stderr, "\n%d tests, %d failures\n", test_count, test_failures); - return (test_failures == 0) ? 0 : 1; + verify_derived_scalars(4097, 1024, 5120, 4096, 1024, 5120, "blk.15", err); + CHECK(err.find("blk.15") != std::string::npos); } diff --git a/server/test/test_draft_topk_cuda.cpp b/server/test/test_draft_topk_cuda.cpp index bf734665e..a1defe7c7 100644 --- a/server/test/test_draft_topk_cuda.cpp +++ b/server/test/test_draft_topk_cuda.cpp @@ -16,6 +16,7 @@ // CUDA and HIP backends (the HIP build compiles this via the hip_compat // shim). Run: ./test_draft_topk_cuda (0 = pass). +#include "CppUnitTestFramework.hpp" #include "../src/common/geometric_draft_topk_cuda.h" #include "../src/common/ddtree.h" @@ -119,11 +120,15 @@ bool run_case(const Case & c, unsigned seed) { } // namespace -int main() { +namespace { +struct DraftTopkCudaFixture {}; +} + +TEST_CASE(DraftTopkCudaFixture, draft_topk_cuda_suite) { int dev_count = 0; if (cudaGetDeviceCount(&dev_count) != cudaSuccess || dev_count == 0) { printf("SKIP: no CUDA device available\n"); - return 0; + return; } // The kernel supports K up to kMaxK (=8 in geometric_draft_topk_cuda.cu); larger K is @@ -172,8 +177,7 @@ int main() { if (failures) { printf("\nFAILED: %d/%d cases\n", failures, idx); - return 1; } + REQUIRE(failures == 0); printf("\nALL PASS: %d/%d cases\n", idx, idx); - return 0; } diff --git a/server/test/test_drafter_early_exit_score_range.cpp b/server/test/test_drafter_early_exit_score_range.cpp index 9fabc155f..047e7e604 100644 --- a/server/test/test_drafter_early_exit_score_range.cpp +++ b/server/test/test_drafter_early_exit_score_range.cpp @@ -1,96 +1,84 @@ -// Unit tests for dflash::common::compute_score_range(). Plain int main(), no frameworks. +// Unit tests for dflash::common::compute_score_range(). // SCORE_LAYERS is relative to fwd_layer_limit: ee7+sl7 → [0,7), not phantom-empty [7,7). +#include "CppUnitTestFramework.hpp" #include "score_range.h" #include #include -// REQUIRE survives -DNDEBUG (bare assert does not). -#define REQUIRE(cond) \ - do { if (!(cond)) { \ - std::fprintf(stderr, "FAIL: %s line %d: %s\n", __FILE__, __LINE__, #cond); \ - std::exit(1); \ - } } while (0) - using dflash::common::ScoreRange; using dflash::common::compute_score_range; -// T1 — The exact bug scenario: early_exit_n=7, score_layers=7, n_layer=28. -// OLD code: start = min(28-7, 7) = 7, end = 7 → empty loop. -// NEW code: effective_n=7, want=min(7,7)=7, start=7-7=0, end=7 → [0,7). -static void t1_bug_scenario() { - ScoreRange r = compute_score_range(/*n_layer=*/28, - /*score_layers=*/7, - /*fwd_layer_limit=*/7); - REQUIRE(r.start == 0 && "score_layer_start must be 0"); - REQUIRE(r.end == 7 && "score_layer_end must equal fwd_layer_limit"); - REQUIRE(!r.empty() && "range must be non-empty"); - REQUIRE(r.count() == 7); - printf("T1 pass: early_exit_n=7 score_layers=7 n_layer=28 -> [%d,%d)\n", - r.start, r.end); -} +namespace { +struct DrafterEarlyExitScoreRangeFixture : CppUnitTestFramework::CommonFixture { + using CppUnitTestFramework::CommonFixture::CommonFixture; -// T2 — No early exit (fwd_layer_limit == n_layer). -// score_layers=7 should pick the last 7 layers [21,28). -static void t2_no_early_exit() { - ScoreRange r = compute_score_range(28, 7, 28); - REQUIRE(r.start == 21); - REQUIRE(r.end == 28); - REQUIRE(!r.empty()); - REQUIRE(r.count() == 7); - printf("T2 pass: no early exit score_layers=7 -> [%d,%d)\n", r.start, r.end); -} + void t1_bug_scenario() { + ScoreRange r = compute_score_range(/*n_layer=*/28, + /*score_layers=*/7, + /*fwd_layer_limit=*/7); + REQUIRE(r.start == 0 && "score_layer_start must be 0"); + REQUIRE(r.end == 7 && "score_layer_end must equal fwd_layer_limit"); + REQUIRE(!r.empty() && "range must be non-empty"); + REQUIRE(r.count() == 7); + printf("T1 pass: early_exit_n=7 score_layers=7 n_layer=28 -> [%d,%d)\n", + r.start, r.end); + } -// T3 — score_layers == -1 (all layers) with no early exit. -static void t3_all_layers_no_exit() { - ScoreRange r = compute_score_range(28, -1, 28); - REQUIRE(r.start == 0); - REQUIRE(r.end == 28); - REQUIRE(!r.empty()); - printf("T3 pass: score_layers=-1 no exit -> [%d,%d)\n", r.start, r.end); -} + void t2_no_early_exit() { + ScoreRange r = compute_score_range(28, 7, 28); + REQUIRE(r.start == 21); + REQUIRE(r.end == 28); + REQUIRE(!r.empty()); + REQUIRE(r.count() == 7); + printf("T2 pass: no early exit score_layers=7 -> [%d,%d)\n", r.start, r.end); + } -// T4 — All layers, with early exit at 14. -static void t4_all_layers_with_exit() { - ScoreRange r = compute_score_range(28, -1, 14); - REQUIRE(r.start == 0); - REQUIRE(r.end == 14); - REQUIRE(!r.empty()); - printf("T4 pass: score_layers=-1 early_exit=14 -> [%d,%d)\n", r.start, r.end); -} + void t3_all_layers_no_exit() { + ScoreRange r = compute_score_range(28, -1, 28); + REQUIRE(r.start == 0); + REQUIRE(r.end == 28); + REQUIRE(!r.empty()); + printf("T3 pass: score_layers=-1 no exit -> [%d,%d)\n", r.start, r.end); + } -// T5 — SCORE_LAYERS larger than fwd_layer_limit: clamp to [0, fwd_layer_limit). -static void t5_score_layers_exceeds_exit() { - // score_layers=14 but only 7 computed: want = min(14,7) = 7, start=0 - ScoreRange r = compute_score_range(28, 14, 7); - REQUIRE(r.start == 0); - REQUIRE(r.end == 7); - REQUIRE(!r.empty()); - printf("T5 pass: score_layers=14 early_exit=7 -> [%d,%d)\n", r.start, r.end); -} + void t4_all_layers_with_exit() { + ScoreRange r = compute_score_range(28, -1, 14); + REQUIRE(r.start == 0); + REQUIRE(r.end == 14); + REQUIRE(!r.empty()); + printf("T4 pass: score_layers=-1 early_exit=14 -> [%d,%d)\n", r.start, r.end); + } -// T6 — SCORE_LAYERS == n_layer (all layers) with no early exit. -static void t6_score_layers_equals_n_layer() { - ScoreRange r = compute_score_range(28, 28, 28); - // score_layers == n_layer → condition (score_layers < n_layer) is false → start=0 - REQUIRE(r.start == 0); - REQUIRE(r.end == 28); - REQUIRE(!r.empty()); - printf("T6 pass: score_layers=n_layer=28 -> [%d,%d)\n", r.start, r.end); -} + void t5_score_layers_exceeds_exit() { + ScoreRange r = compute_score_range(28, 14, 7); + REQUIRE(r.start == 0); + REQUIRE(r.end == 7); + REQUIRE(!r.empty()); + printf("T5 pass: score_layers=14 early_exit=7 -> [%d,%d)\n", r.start, r.end); + } + + void t6_score_layers_equals_n_layer() { + ScoreRange r = compute_score_range(28, 28, 28); + REQUIRE(r.start == 0); + REQUIRE(r.end == 28); + REQUIRE(!r.empty()); + printf("T6 pass: score_layers=n_layer=28 -> [%d,%d)\n", r.start, r.end); + } -// T7 — early_exit_n == 14, score_layers == 7: should produce [7,14). -static void t7_partial_exit_partial_score() { - ScoreRange r = compute_score_range(28, 7, 14); - REQUIRE(r.start == 7); - REQUIRE(r.end == 14); - REQUIRE(!r.empty()); - REQUIRE(r.count() == 7); - printf("T7 pass: early_exit=14 score_layers=7 -> [%d,%d)\n", r.start, r.end); + void t7_partial_exit_partial_score() { + ScoreRange r = compute_score_range(28, 7, 14); + REQUIRE(r.start == 7); + REQUIRE(r.end == 14); + REQUIRE(!r.empty()); + REQUIRE(r.count() == 7); + printf("T7 pass: early_exit=14 score_layers=7 -> [%d,%d)\n", r.start, r.end); + } +}; } -int main() { +TEST_CASE(DrafterEarlyExitScoreRangeFixture, score_range_suite) { t1_bug_scenario(); t2_no_early_exit(); t3_all_layers_no_exit(); @@ -99,5 +87,4 @@ int main() { t6_score_layers_equals_n_layer(); t7_partial_exit_partial_score(); printf("\nAll score_range tests passed.\n"); - return 0; } diff --git a/server/test/test_drafter_tail_capture_guard.cpp b/server/test/test_drafter_tail_capture_guard.cpp index a00763e3e..1ce9d176f 100644 --- a/server/test/test_drafter_tail_capture_guard.cpp +++ b/server/test/test_drafter_tail_capture_guard.cpp @@ -21,108 +21,98 @@ // GREEN (after patch): TAIL_GUARD_USE_NEW_FORMULA defined via compiler flag → test PASSES. // The patch to qwen3_graph.cpp changes the same 2 lines as this toggle. +#include "CppUnitTestFramework.hpp" + #include #include -#define REQUIRE(cond) \ - do { if (!(cond)) { \ - std::fprintf(stderr, "FAIL: %s line %d: %s\n", __FILE__, __LINE__, #cond); \ - std::exit(1); \ - } } while (0) +static bool tail_fits(int tail_lo, int cs, int cl, int n_lookahead); -// The guard being tested — toggled by compile-time flag to reproduce RED/GREEN. -#ifdef TAIL_GUARD_USE_NEW_FORMULA -static bool tail_fits(int tail_lo, int cs, int cl, int n_lookahead) { - return tail_lo >= cs && tail_lo + n_lookahead <= cs + cl; // NEW (fix) -} -#else -static bool tail_fits(int tail_lo, int cs, int cl, int n_lookahead) { - (void)n_lookahead; - return tail_lo >= cs && tail_lo < cs + cl; // OLD (Bug #42) -} -#endif +namespace { +struct DrafterTailCaptureGuardFixture : CppUnitTestFramework::CommonFixture { + using CppUnitTestFramework::CommonFixture::CommonFixture; + + void t1_straddling_tail_must_be_skipped() { + const int chunk_size = 4096, n_lookahead = 8; + const int cs = 0, cl = chunk_size; -// T1: First chunk (cs=0, cl=4096), S = chunk_size + r for r ∈ {1..7}. -// Tail straddles the chunk boundary: tail_lo ∈ [4089..4095], needs 8 tokens -// → runs 1..7 tokens past the end → view must be SKIPPED. -// CORRECT answer: false. Old guard returns true → BUG → RED test FAILS. -static void t1_straddling_tail_must_be_skipped() { - const int chunk_size = 4096, n_lookahead = 8; - const int cs = 0, cl = chunk_size; // first chunk + for (int r = 1; r <= 7; r++) { + const int S = chunk_size + r; + const int tail_lo = S - n_lookahead; + + const bool result = tail_fits(tail_lo, cs, cl, n_lookahead); + std::printf("T1 r=%d S=%d tail_lo=%d tail_hi=%d chunk=[%d,%d): fits=%d (expect 0)\n", + r, S, tail_lo, tail_lo + n_lookahead, cs, cs + cl, (int)result); + REQUIRE(!result && "tail overruns chunk boundary — guard must return false"); + } + } - for (int r = 1; r <= 7; r++) { - const int S = chunk_size + r; - const int tail_lo = S - n_lookahead; // = 4088 + r ∈ [4089..4095] + void t2_tail_fits_exactly_at_chunk_end() { + const int chunk_size = 4096, n_lookahead = 8; + const int cs = 0, cl = chunk_size; + const int S = chunk_size; + const int tail_lo = S - n_lookahead; const bool result = tail_fits(tail_lo, cs, cl, n_lookahead); - std::printf("T1 r=%d S=%d tail_lo=%d tail_hi=%d chunk=[%d,%d): fits=%d (expect 0)\n", - r, S, tail_lo, tail_lo + n_lookahead, cs, cs + cl, (int)result); - REQUIRE(!result && "tail overruns chunk boundary — guard must return false"); + std::printf("T2 r=0 S=%d tail_lo=%d: fits=%d (expect 1)\n", S, tail_lo, (int)result); + REQUIRE(result && "tail fits exactly at chunk end — must return true"); } -} -// T2: r=0 (S == chunk_size exactly). tail_lo=4088, tail_hi=4096=chunk end. Fits exactly. -// Both old and new guards agree: true. -static void t2_tail_fits_exactly_at_chunk_end() { - const int chunk_size = 4096, n_lookahead = 8; - const int cs = 0, cl = chunk_size; - const int S = chunk_size; - const int tail_lo = S - n_lookahead; // 4088 - - const bool result = tail_fits(tail_lo, cs, cl, n_lookahead); - std::printf("T2 r=0 S=%d tail_lo=%d: fits=%d (expect 1)\n", S, tail_lo, (int)result); - REQUIRE(result && "tail fits exactly at chunk end — must return true"); -} + void t3_tail_starts_outside_chunk() { + const int chunk_size = 4096, n_lookahead = 8; + const int cs = 0, cl = chunk_size; + const int S = chunk_size + 8; + const int tail_lo = S - n_lookahead; -// T3: r=8 (S = chunk_size + 8). tail_lo=4096 — at cs+cl boundary, outside chunk. -// Both guards agree: false. -static void t3_tail_starts_outside_chunk() { - const int chunk_size = 4096, n_lookahead = 8; - const int cs = 0, cl = chunk_size; - const int S = chunk_size + 8; - const int tail_lo = S - n_lookahead; // 4096 - - const bool result = tail_fits(tail_lo, cs, cl, n_lookahead); - std::printf("T3 r=8 S=%d tail_lo=%d: fits=%d (expect 0)\n", S, tail_lo, (int)result); - REQUIRE(!result && "tail starts at next chunk — must return false"); -} + const bool result = tail_fits(tail_lo, cs, cl, n_lookahead); + std::printf("T3 r=8 S=%d tail_lo=%d: fits=%d (expect 0)\n", S, tail_lo, (int)result); + REQUIRE(!result && "tail starts at next chunk — must return false"); + } + + void t4_second_chunk_tail_fits_exactly() { + const int chunk_size = 4096, n_lookahead = 8; + const int cs = chunk_size, cl = chunk_size; + const int S = 2 * chunk_size; + const int tail_lo = S - n_lookahead; + + const bool result = tail_fits(tail_lo, cs, cl, n_lookahead); + std::printf("T4 second chunk S=%d tail_lo=%d cs=%d: fits=%d (expect 1)\n", + S, tail_lo, cs, (int)result); + REQUIRE(result && "tail fits exactly in second chunk — must return true"); + } -// T4: Second chunk (cs=4096, cl=4096), S=8192, tail fully inside. -// tail_lo=8184, tail_hi=8192 == cs+cl. Both guards agree: true. -static void t4_second_chunk_tail_fits_exactly() { - const int chunk_size = 4096, n_lookahead = 8; - const int cs = chunk_size, cl = chunk_size; // second chunk - const int S = 2 * chunk_size; - const int tail_lo = S - n_lookahead; // 8184 - - const bool result = tail_fits(tail_lo, cs, cl, n_lookahead); - std::printf("T4 second chunk S=%d tail_lo=%d cs=%d: fits=%d (expect 1)\n", - S, tail_lo, cs, (int)result); - REQUIRE(result && "tail fits exactly in second chunk — must return true"); + void t5_second_chunk_straddling_tail_skipped() { + const int chunk_size = 4096, n_lookahead = 8; + const int cs = chunk_size, cl = chunk_size; + const int r = 3; + const int S = 2 * chunk_size + r; + const int tail_lo = S - n_lookahead; + + const bool result = tail_fits(tail_lo, cs, cl, n_lookahead); + std::printf("T5 second chunk r=%d S=%d tail_lo=%d: fits=%d (expect 0)\n", + r, S, tail_lo, (int)result); + REQUIRE(!result && "tail straddles end of second chunk — must return false"); + } +}; } -// T5: Second chunk, r=3. tail straddles end of second chunk. -// S = 2*4096 + 3 = 8195. tail_lo = 8187, tail_hi = 8195. cs+cl = 8192. -// New guard: 8195 <= 8192 → false. Old guard: 8187 < 8192 → true (BUG). -static void t5_second_chunk_straddling_tail_skipped() { - const int chunk_size = 4096, n_lookahead = 8; - const int cs = chunk_size, cl = chunk_size; // second chunk [4096,8192) - const int r = 3; - const int S = 2 * chunk_size + r; - const int tail_lo = S - n_lookahead; // 8187 - - const bool result = tail_fits(tail_lo, cs, cl, n_lookahead); - std::printf("T5 second chunk r=%d S=%d tail_lo=%d: fits=%d (expect 0)\n", - r, S, tail_lo, (int)result); - REQUIRE(!result && "tail straddles end of second chunk — must return false"); +// The guard being tested — toggled by compile-time flag to reproduce RED/GREEN. +#ifdef TAIL_GUARD_USE_NEW_FORMULA +static bool tail_fits(int tail_lo, int cs, int cl, int n_lookahead) { + return tail_lo >= cs && tail_lo + n_lookahead <= cs + cl; // NEW (fix) } +#else +static bool tail_fits(int tail_lo, int cs, int cl, int n_lookahead) { + (void)n_lookahead; + return tail_lo >= cs && tail_lo < cs + cl; // OLD (Bug #42) +} +#endif -int main() { +TEST_CASE(DrafterTailCaptureGuardFixture, tail_capture_guard_suite) { t1_straddling_tail_must_be_skipped(); t2_tail_fits_exactly_at_chunk_end(); t3_tail_starts_outside_chunk(); t4_second_chunk_tail_fits_exactly(); t5_second_chunk_straddling_tail_skipped(); std::printf("All tail_capture guard tests passed.\n"); - return 0; } diff --git a/server/test/test_drafter_warm_path_regression.cpp b/server/test/test_drafter_warm_path_regression.cpp index ff26937d8..b5b408a08 100644 --- a/server/test/test_drafter_warm_path_regression.cpp +++ b/server/test/test_drafter_warm_path_regression.cpp @@ -1,6 +1,7 @@ // Regression test: K_norope_v/Q_norope_v sized to n_score_layers, not n_layer. // Old code allocated 28 entries (~5.6 GB wasted at 128K); fix uses score_range.count(). +#include "CppUnitTestFramework.hpp" #include "score_range.h" #include @@ -9,6 +10,19 @@ using dflash::common::ScoreRange; using dflash::common::compute_score_range; +#define TEST_ASSERT(cond) do { \ + auto _cpputf_exception = CppUnitTestFramework::Assert::IsTrue(static_cast(cond), #cond); \ + if (_cpputf_exception) { \ + throw *_cpputf_exception; \ + } \ +} while (0) +#undef assert +#define assert(cond) TEST_ASSERT(cond) + +namespace { +struct DrafterWarmPathRegressionFixture {}; +} + // Helper: compute n_score_layers as the fixed allocator does. static int score_layer_count(int n_layer, int score_layers_env, int early_exit_env) { const int fwd_limit = (early_exit_env > 0 && early_exit_env < n_layer) @@ -142,7 +156,7 @@ static void t7_alloc_loop_upper_bound() { } } -int main() { +TEST_CASE(DrafterWarmPathRegressionFixture, warm_path_regression_suite) { t1_baseline_full_alloc(); t2_l7_trimmed_alloc(); t3_early_exit_with_score_layers(); @@ -151,5 +165,4 @@ int main() { t6_start_pre_matches_loop_start(); t7_alloc_loop_upper_bound(); printf("\nAll warm-path regression tests passed.\n"); - return 0; } diff --git a/server/test/test_gguf_mmap.cpp b/server/test/test_gguf_mmap.cpp index f5d077d84..335141920 100644 --- a/server/test/test_gguf_mmap.cpp +++ b/server/test/test_gguf_mmap.cpp @@ -1,5 +1,4 @@ // Unit tests for dflash::common::GgufMmap (RAII platform mmap wrapper). -// Plain int main(), no test frameworks. // // T1: open + read first few bytes of a known file → ok, size > 0 // T2: open the same instance twice (idempotency) → no leak @@ -7,6 +6,7 @@ // T4: explicit release() → object becomes empty, no crash // T5: RAII destructor — scope exit, no crash +#include "CppUnitTestFramework.hpp" #include "common/gguf_mmap.h" #include @@ -27,6 +27,21 @@ #define WRITE_FN write #endif +// Use a local shim because helper functions below are free functions rather than +// fixture members, so the README's direct REQUIRE/CHECK macros are not in scope. +#define TEST_ASSERT(cond) do { \ + auto _cpputf_exception = CppUnitTestFramework::Assert::IsTrue(static_cast(cond), #cond); \ + if (_cpputf_exception) { \ + throw *_cpputf_exception; \ + } \ +} while (0) +#undef assert +#define assert(cond) TEST_ASSERT(cond) + +namespace { +struct GgufMmapFixture {}; +} + // Create a small temp file with known content; returns its path. static std::string make_temp_file() { #if defined(_WIN32) @@ -150,7 +165,7 @@ static void t5_raii_destructor() { // ─── main ──────────────────────────────────────────────────────────────────── -int main() { +TEST_CASE(GgufMmapFixture, gguf_mmap_suite) { t1_open_and_read(); t2_idempotent_open(); t3_missing_file(); @@ -158,5 +173,4 @@ int main() { t5_raii_destructor(); std::puts("ALL TESTS PASS"); - return 0; } diff --git a/server/test/test_gpu_sampler_cuda.cpp b/server/test/test_gpu_sampler_cuda.cpp index 41aef3ae4..7329a54a9 100644 --- a/server/test/test_gpu_sampler_cuda.cpp +++ b/server/test/test_gpu_sampler_cuda.cpp @@ -6,6 +6,7 @@ // Build: registered in server/CMakeLists.txt under DFLASH27B_TESTS (CUDA only). // Run: ./test_gpu_sampler_cuda (exit 0 = pass, non-zero = fail) +#include "CppUnitTestFramework.hpp" #include "common/sampler.h" #include "common/geometric_sampler_cuda.h" @@ -22,26 +23,11 @@ using namespace dflash::common; -// ─── Test framework (ds4 style) ──────────────────────────────────────── - -static int test_failures = 0; -static int test_count = 0; - -#define TEST_ASSERT_MSG(expr, msg) do { \ - test_count++; \ - if (!(expr)) { \ - test_failures++; \ - std::fprintf(stderr, " FAIL: %s:%d: %s — %s\n", __FILE__, __LINE__, #expr, msg); \ - } \ -} while (0) +namespace { +struct GpuSamplerCudaFixture {}; +} -#define RUN_TEST(fn) do { \ - std::fprintf(stderr, " %s ...", #fn); \ - int before = test_failures; \ - fn(); \ - if (test_failures == before) std::fprintf(stderr, " ok\n"); \ - else std::fprintf(stderr, "\n"); \ -} while (0) +#define TEST_ASSERT_MSG(expr, msg) CHECK(expr) // ═══════════════════════════════════════════════════════════════════════ // GPU sampler (geometric_sampler_cuda.cu) — compared against the CPU chain. @@ -96,7 +82,7 @@ static std::vector cpu_top_p_dist(const std::vector & logits, flo // Greedy (temp=0): GPU must pick exactly the same token as the CPU chain, with // and without penalties active. -static void test_gpu_sampler_greedy_matches_cpu() { +TEST_CASE(GpuSamplerCudaFixture, test_gpu_sampler_greedy_matches_cpu) { if (!gpu_sampler_test_available()) { std::fprintf(stderr, " (skip: no CUDA) "); return; } const int vocab = 257; // not a multiple of the block size on purpose for (uint64_t seed = 1; seed <= 6; seed++) { @@ -111,7 +97,7 @@ static void test_gpu_sampler_greedy_matches_cpu() { } } -static void test_gpu_sampler_greedy_penalties_match_cpu() { +TEST_CASE(GpuSamplerCudaFixture, test_gpu_sampler_greedy_penalties_match_cpu) { if (!gpu_sampler_test_available()) { std::fprintf(stderr, " (skip: no CUDA) "); return; } const int vocab = 200; auto logits = gpu_test_logits(vocab, 99); @@ -135,7 +121,7 @@ static void test_gpu_sampler_greedy_penalties_match_cpu() { } // top_k>0 is intentionally CPU-only: the GPU entry must signal fallback (-1). -static void test_gpu_sampler_top_k_falls_back() { +TEST_CASE(GpuSamplerCudaFixture, test_gpu_sampler_top_k_falls_back) { if (!gpu_sampler_test_available()) { std::fprintf(stderr, " (skip: no CUDA) "); return; } const int vocab = 128; auto logits = gpu_test_logits(vocab, 5); @@ -169,7 +155,7 @@ static double gpu_empirical_l1(const std::vector & logits, const SamplerC // Temperature sampling (no truncation): the GPU draw distribution must match the // analytic softmax — i.e. the same distribution the CPU multinomial samples. -static void test_gpu_sampler_temperature_distribution() { +TEST_CASE(GpuSamplerCudaFixture, test_gpu_sampler_temperature_distribution) { if (!gpu_sampler_test_available()) { std::fprintf(stderr, " (skip: no CUDA) "); return; } const int vocab = 24; auto logits = gpu_test_logits(vocab, 42); @@ -182,7 +168,7 @@ static void test_gpu_sampler_temperature_distribution() { // top_p in (0,1) is intentionally unimplemented on the GPU (removed; see // geometric_sampler_cuda.h): the GPU entry must signal fallback (-1), same // contract as top_k>0. -static void test_gpu_sampler_top_p_falls_back() { +TEST_CASE(GpuSamplerCudaFixture, test_gpu_sampler_top_p_falls_back) { if (!gpu_sampler_test_available()) { std::fprintf(stderr, " (skip: no CUDA) "); return; } const int vocab = 128; auto logits = gpu_test_logits(vocab, 5); @@ -196,7 +182,7 @@ static void test_gpu_sampler_top_p_falls_back() { // geometric_compute_probs_cuda is the shared prefix behind the GPU-assisted // top_p path: it must return the same penalty+softmax(temp) probability vector // the CPU chain computes. Checks a plain-temp config and one with penalties. -static void test_gpu_compute_probs_matches_softmax() { +TEST_CASE(GpuSamplerCudaFixture, test_gpu_compute_probs_matches_softmax) { if (!gpu_sampler_test_available()) { std::fprintf(stderr, " (skip: no CUDA) "); return; } const int vocab = 300; // not a multiple of the block size on purpose auto logits = gpu_test_logits(vocab, 21); @@ -237,7 +223,7 @@ static void test_gpu_compute_probs_matches_softmax() { // geometric_compute_probs_cuda + the CPU nucleus_cutoff/draw. The empirical // draw distribution must match the analytic nucleus distribution — this is the // only end-to-end check of nucleus_cutoff and sample_from_gpu_probs. -static void test_gpu_sampler_top_p_distribution() { +TEST_CASE(GpuSamplerCudaFixture, test_gpu_sampler_top_p_distribution) { if (!gpu_sampler_test_available()) { std::fprintf(stderr, " (skip: no CUDA) "); return; } const int vocab = 48; auto logits = gpu_test_logits(vocab, 7); @@ -263,7 +249,7 @@ static void test_gpu_sampler_top_p_distribution() { // Direct CPU-vs-GPU agreement on the same draws is not bit-exact (different // inverse-CDF orderings), but both must agree on the *argmax* of their empirical // histograms — the most-likely token — under temperature sampling. -static void test_gpu_sampler_modal_token_matches_cpu() { +TEST_CASE(GpuSamplerCudaFixture, test_gpu_sampler_modal_token_matches_cpu) { if (!gpu_sampler_test_available()) { std::fprintf(stderr, " (skip: no CUDA) "); return; } const int vocab = 32; auto logits = gpu_test_logits(vocab, 11); @@ -344,25 +330,9 @@ static void gpu_sampler_microbench() { (void)sink; } -// ─── main ──────────────────────────────────────────────────────────── - -int main() { +TEST_CASE(GpuSamplerCudaFixture, gpu_sampler_microbench_when_enabled) { if (const char * b = std::getenv("DFLASH_SAMPLER_BENCH"); b && b[0] == '1') { gpu_sampler_microbench(); - return 0; } - - std::fprintf(stderr, "=== test_gpu_sampler_cuda ===\n"); - - RUN_TEST(test_gpu_sampler_greedy_matches_cpu); - RUN_TEST(test_gpu_sampler_greedy_penalties_match_cpu); - RUN_TEST(test_gpu_sampler_top_k_falls_back); - RUN_TEST(test_gpu_sampler_temperature_distribution); - RUN_TEST(test_gpu_sampler_top_p_falls_back); - RUN_TEST(test_gpu_compute_probs_matches_softmax); - RUN_TEST(test_gpu_sampler_top_p_distribution); - RUN_TEST(test_gpu_sampler_modal_token_matches_cpu); - - std::fprintf(stderr, "\n%d tests, %d failures\n", test_count, test_failures); - return (test_failures == 0) ? 0 : 1; + CHECK(true); } diff --git a/server/test/test_kv_quant.cpp b/server/test/test_kv_quant.cpp index 9d9f6eb70..0c9d03521 100644 --- a/server/test/test_kv_quant.cpp +++ b/server/test/test_kv_quant.cpp @@ -1,11 +1,12 @@ // Unit tests for dflash::kv_quant (parse_kv_type, resolve_kv_types, -// is_supported_kv_pair). Plain int main(), no frameworks. +// is_supported_kv_pair). // // T8 (unsupported pair aborts) is not tested in-process because std::abort() // terminates the test runner. Manual verification: // DFLASH27B_KV_K=tq3_0 DFLASH27B_KV_V=q5_0 ./dflash/build/test_kv_quant // Expected: prints "[dflash] KV pair …" message and aborts. +#include "CppUnitTestFramework.hpp" #include "kv_quant.h" #include @@ -18,6 +19,21 @@ #define unsetenv(name) _putenv_s(name, "") #endif +// Use a local shim because helper functions below are free functions rather than +// fixture members, so the README's direct REQUIRE/CHECK macros are not in scope. +#define TEST_ASSERT(cond) do { \ + auto _cpputf_exception = CppUnitTestFramework::Assert::IsTrue(static_cast(cond), #cond); \ + if (_cpputf_exception) { \ + throw *_cpputf_exception; \ + } \ +} while (0) +#undef assert +#define assert(cond) TEST_ASSERT(cond) + +namespace { +struct KvQuantFixture {}; +} + // ─── helpers ──────────────────────────────────────────────────────────────── static void clear_kv_env() { @@ -59,10 +75,10 @@ static void t1_parse_kv_type() { static void t2_resolve_kv_types() { ggml_type k, v; - // 2a: no env → default Q8_0/Q8_0 + // 2a: no env → default Q4_0/Q4_0 clear_kv_env(); dflash::resolve_kv_types(k, v); - assert(k == GGML_TYPE_Q8_0 && v == GGML_TYPE_Q8_0); + assert(k == GGML_TYPE_Q4_0 && v == GGML_TYPE_Q4_0); // 2b: F16 shorthand clear_kv_env(); @@ -191,7 +207,7 @@ static void t5_kv_reservation() { // ─── main ──────────────────────────────────────────────────────────────────── -int main() { +TEST_CASE(KvQuantFixture, kv_quant_suite) { clear_kv_env(); // start clean regardless of calling environment t1_parse_kv_type(); @@ -201,5 +217,4 @@ int main() { t5_kv_reservation(); std::puts("ALL TESTS PASS"); - return 0; } diff --git a/server/test/test_kvflash_placement.cpp b/server/test/test_kvflash_placement.cpp index 3f16bc52e..7f31258cc 100644 --- a/server/test/test_kvflash_placement.cpp +++ b/server/test/test_kvflash_placement.cpp @@ -5,6 +5,7 @@ // reservation forces experts cold even though KVFlash bounds the resident KV. // The decision also reports whether the model is all-hot with the FULL max_ctx // KV (i.e. KVFlash is redundant) so the gate can disable the pool when unneeded. +#include "CppUnitTestFramework.hpp" #include "../src/common/kvflash_placement.h" #include @@ -12,14 +13,11 @@ using namespace dflash::common; -static void expect(bool cond, const char * msg) { - if (!cond) { - std::fprintf(stderr, "FAIL: %s\n", msg); - std::exit(1); - } +namespace { +struct KvflashPlacementFixture {}; } -int main() { +TEST_CASE(KvflashPlacementFixture, kvflash_placement_decision_suite) { // qwen3.6-35B-A3B-like budget on a 24 GiB card: // ~80 KiB/token KV (5 GiB @ 65536, 10 GiB @ 131072) // experts ~13.19 GiB, core ~3.12 GiB, draft ~1.2 GiB present. @@ -37,9 +35,9 @@ int main() { { auto d = kvflash_placement_decision(kv_per_tok, 65536, /*pool=*/0, gpu, core, experts, warm, safety, draft); - expect(d.kv_ctx == 65536, "C1: no-kvflash reserves full ctx"); - expect(d.all_hot_full_kv, "C1: 65536 full KV fits all experts hot"); - expect(!d.pool_reduced, "C1: no pool reduction"); + REQUIRE(d.kv_ctx == 65536); + REQUIRE(d.all_hot_full_kv); + REQUIRE(!d.pool_reduced); } // Case 2 — max_ctx 65536 + pool 49152: full KV still fits all-hot, so KVFlash @@ -47,9 +45,9 @@ int main() { { auto d = kvflash_placement_decision(kv_per_tok, 65536, /*pool=*/49152, gpu, core, experts, warm, safety, draft); - expect(d.all_hot_full_kv, "C2: 65536 full KV still fits -> kvflash redundant"); - expect(d.kv_ctx == 65536, "C2: keeps full ctx (gate disables kvflash)"); - expect(!d.pool_reduced, "C2: no pool reduction when redundant"); + REQUIRE(d.all_hot_full_kv); + REQUIRE(d.kv_ctx == 65536); + REQUIRE(!d.pool_reduced); } // Case 3 (THE FIX) — max_ctx 131072 + pool 49152: full KV (10 GiB) forces @@ -57,27 +55,27 @@ int main() { { auto d = kvflash_placement_decision(kv_per_tok, 131072, /*pool=*/49152, gpu, core, experts, warm, safety, draft); - expect(!d.all_hot_full_kv, "C3: 131072 full KV forces experts cold"); - expect(d.kv_ctx == 49152, "C3: reserves POOL ctx, not max_ctx"); - expect(d.pool_reduced, "C3: pool reduction engaged"); - expect(d.kv_total == kv_per_tok * 49152ull, "C3: kv_total sized to pool"); - expect(d.kv_total < kv_per_tok * 131072ull, "C3: pool reservation < full"); + REQUIRE(!d.all_hot_full_kv); + REQUIRE(d.kv_ctx == 49152); + REQUIRE(d.pool_reduced); + REQUIRE(d.kv_total == kv_per_tok * 49152ull); + REQUIRE(d.kv_total < kv_per_tok * 131072ull); } // Case 4 — max_ctx 131072, NO kvflash: full ctx, cold cliff. { auto d = kvflash_placement_decision(kv_per_tok, 131072, /*pool=*/0, gpu, core, experts, warm, safety, draft); - expect(d.kv_ctx == 131072, "C4: no-kvflash reserves full ctx"); - expect(!d.all_hot_full_kv, "C4: 131072 no-kvflash -> cold cliff"); + REQUIRE(d.kv_ctx == 131072); + REQUIRE(!d.all_hot_full_kv); } // Case 5 — pool >= max_ctx: pool can't exceed ctx, no reduction. { auto d = kvflash_placement_decision(kv_per_tok, 32768, /*pool=*/49152, gpu, core, experts, warm, safety, draft); - expect(!d.pool_reduced, "C5: pool>=ctx -> no reduction"); - expect(d.kv_ctx == 32768, "C5: keeps full ctx"); + REQUIRE(!d.pool_reduced); + REQUIRE(d.kv_ctx == 32768); } // Case 6 — laguna/poolside-style no-draft placement: same pool rule, no @@ -87,11 +85,10 @@ int main() { auto d = kvflash_placement_decision(kv_per_tok, 131072, /*pool=*/16384, gpu, core, experts, warm, safety, /*draft_bytes=*/0); - expect(!d.all_hot_full_kv, "C6: full KV still forces experts cold"); - expect(d.kv_ctx == 16384, "C6: laguna reserves POOL ctx"); - expect(d.pool_reduced, "C6: laguna pool reduction engaged"); + REQUIRE(!d.all_hot_full_kv); + REQUIRE(d.kv_ctx == 16384); + REQUIRE(d.pool_reduced); } std::printf("PASS: kvflash placement decision (6 cases)\n"); - return 0; } diff --git a/server/test/test_kvflash_pool_sizing.cpp b/server/test/test_kvflash_pool_sizing.cpp index fc4dd64b3..c6fb5b01b 100644 --- a/server/test/test_kvflash_pool_sizing.cpp +++ b/server/test/test_kvflash_pool_sizing.cpp @@ -6,6 +6,7 @@ // speed-capped value. Placement then over-reserved KV and starved experts. // These asserts pin the two behaviours so a future caller can't silently // reintroduce the divergence. +#include "CppUnitTestFramework.hpp" #include "../src/common/kvflash_pager.h" #include @@ -13,21 +14,19 @@ using namespace dflash::common; -static void expect(bool cond, const char * msg) { - if (!cond) { std::fprintf(stderr, "FAIL: %s\n", msg); std::exit(1); } +namespace { +struct KvflashPoolSizingFixture {}; } -int main() { +TEST_CASE(KvflashPoolSizingFixture, kvflash_pool_sizing_suite) { setenv("DFLASH_KVFLASH", "auto", 1); unsetenv("DFLASH_KVFLASH_MAX_POOL"); const int max_ctx = 131072; // No budget supplied -> fallback fraction of max_ctx (the buggy placement // path). scorer_expected toggles 1/2 vs 1/4. - expect(kvflash_pool_from_env(max_ctx, KvFlashConfig{}, false) == max_ctx / 2, - "no-budget fallback should be max_ctx/2 without scorer"); - expect(kvflash_pool_from_env(max_ctx, KvFlashConfig{}, true) == max_ctx / 4, - "no-budget fallback should be max_ctx/4 with scorer"); + REQUIRE(kvflash_pool_from_env(max_ctx, KvFlashConfig{}, false) == max_ctx / 2); + REQUIRE(kvflash_pool_from_env(max_ctx, KvFlashConfig{}, true) == max_ctx / 4); // Real budget with ample VRAM -> capped at the speed point (16384), far // below max_ctx/2. This is what runtime actually allocates; placement must @@ -38,16 +37,13 @@ int main() { budget.bytes_per_token = 80 * 1024; // ~qwen35moe density budget.speed_cap_tokens = 16384; const int with_budget = kvflash_pool_from_env(max_ctx, KvFlashConfig{}, true, budget); - expect(with_budget == 16384, "ample-VRAM auto pool should hit the speed cap"); - expect(with_budget < max_ctx / 2, - "budgeted pool must be smaller than the no-budget fallback " - "(the divergence that starved experts)"); + REQUIRE(with_budget == 16384); + REQUIRE(with_budget < max_ctx / 2); // Tight VRAM -> budget binds below the cap, still well under max_ctx/2. budget.free_bytes = 2LL * 1024 * 1024 * 1024; // 2 GiB free const int tight = kvflash_pool_from_env(max_ctx, KvFlashConfig{}, true, budget); - expect(tight > 0 && tight <= 16384, "tight-VRAM pool stays within the cap"); + REQUIRE(tight > 0 && tight <= 16384); std::printf("OK test_kvflash_pool_sizing\n"); - return 0; } diff --git a/server/test/test_kvflash_qk.cpp b/server/test/test_kvflash_qk.cpp index e91e63bdd..a359f2732 100644 --- a/server/test/test_kvflash_qk.cpp +++ b/server/test/test_kvflash_qk.cpp @@ -7,6 +7,8 @@ // 4. cosine: query/key magnitudes do not change scores // 5. missing pooled keys keep the missing_score sentinel +#include "CppUnitTestFramework.hpp" + #define KVFLASH_QK_PURE_ONLY #include "kvflash_qk.h" @@ -17,15 +19,13 @@ using namespace dflash::common; -static int g_fail = 0; -#define CHECK(cond, msg) do { \ - if (cond) std::printf("PASS %s\n", msg); \ - else { std::printf("FAIL %s\n", msg); g_fail++; } \ -} while (0) - static bool near(float a, float b, float eps = 1e-4f) { return std::fabs(a - b) < eps; } -int main() { +namespace { +struct KvflashQkFixture {}; +} + +TEST_CASE(KvflashQkFixture, kvflash_qk_suite) { KvFlashQkDims d; d.n_layers = 2; d.n_q_heads = 6; d.n_kv_heads = 2; d.head_dim = 8; const int group = d.n_q_heads / d.n_kv_heads; // 3 @@ -80,20 +80,20 @@ int main() { std::vector out; kvflash_qk_chunk_scores(pk, query.data(), d, out, /*missing=*/-9.0f); - CHECK(out.size() == 6, "output size matches chunk count"); - CHECK(near(out[0], 0.0f), "orthogonal chunk scores ~0"); - CHECK(near(out[1], 1.0f), "fully aligned chunk scores ~1 (cosine, layer-mean)"); - CHECK(near(out[2], 0.5f), "1-of-2-layer alignment scores ~0.5 (layer-MEAN aggregation)"); - CHECK(near(out[3], 0.5f), "alignment with one group q-head counts (max over group heads)"); - CHECK(near(out[4], -9.0f), "missing pooled keys keep missing_score"); - CHECK(out[1] > out[2] && out[2] > out[0], "ranking: aligned > partial > orthogonal"); - CHECK(near(out[5], out[1]), "identical chunks tie"); + CHECK(out.size() == 6); + CHECK(near(out[0], 0.0f)); + CHECK(near(out[1], 1.0f)); + CHECK(near(out[2], 0.5f)); + CHECK(near(out[3], 0.5f)); + CHECK(near(out[4], -9.0f)); + CHECK(out[1] > out[2] && out[2] > out[0]); + CHECK(near(out[5], out[1])); // Default missing_score is worst-case (< -1, below the cosine-mean floor) // so a missing chunk never outranks a real chunk with negative correlation. std::vector out_def; kvflash_qk_chunk_scores(pk, query.data(), d, out_def); // default missing - CHECK(out_def[4] < -1.0f, "default missing_score ranks below any real cosine"); + CHECK(out_def[4] < -1.0f); // Cosine invariance: scaling the query must not change anything. std::vector query_scaled(query); @@ -102,7 +102,7 @@ int main() { kvflash_qk_chunk_scores(pk, query_scaled.data(), d, out2, -9.0f); bool same = out2.size() == out.size(); for (size_t i = 0; same && i < out.size(); i++) same = near(out[i], out2[i]); - CHECK(same, "query magnitude invariance (cosine)"); + CHECK(same); // Mixed-direction key: pooled key = normalize(e0+e1): cos with e0-query // = 1/sqrt(2) in that layer. @@ -117,10 +117,8 @@ int main() { std::vector pk1{ mixed.data() }; std::vector o1; kvflash_qk_chunk_scores(pk1, query.data(), d, o1); - CHECK(near(o1[0], (float)(1.0 / std::sqrt(2.0))), - "fractional cosine propagates exactly"); + CHECK(near(o1[0], (float)(1.0 / std::sqrt(2.0)))); } - std::printf("%s (%d failures)\n", g_fail == 0 ? "ALL PASS" : "FAILED", g_fail); - return g_fail == 0 ? 0 : 1; + std::printf("ALL PASS\n"); } diff --git a/server/test/bench_moe_stream.cpp b/server/test/test_moe_stream.cpp similarity index 67% rename from server/test/bench_moe_stream.cpp rename to server/test/test_moe_stream.cpp index 074ed8103..9379e8774 100644 --- a/server/test/bench_moe_stream.cpp +++ b/server/test/test_moe_stream.cpp @@ -1,13 +1,7 @@ -// Synthetic microbenchmark: MoE prefill streaming vs CPU cold path. -// -// This benchmark creates fake expert data in a temporary mmap'd file, -// builds layer regions, and measures DMA + GPU compute latency at various -// batch sizes (T = number of tokens). -// -// Usage: bench_moe_stream [--n-expert 128] [--n-cold 4] [--hidden 5120] [--ffn 3584] -// -// Requires: CUDA GPU. +// Self-contained unit-style smoke for the MoE streaming path. +// Uses synthetic data only (no model files), while still printing timing. +#include "CppUnitTestFramework.hpp" #include "../src/common/moe_hybrid_stream.h" #include "../src/common/moe_hybrid_storage.h" #include "../src/common/gpu_runtime_compat.h" @@ -21,6 +15,7 @@ #include #include #include +#include #include #if !defined(_WIN32) @@ -33,6 +28,10 @@ using namespace dflash::common; using Clock = std::chrono::high_resolution_clock; +namespace { +struct BenchMoeStreamFixture {}; +} + static double elapsed_ms(Clock::time_point t0, Clock::time_point t1) { return std::chrono::duration(t1 - t0).count(); } @@ -43,21 +42,13 @@ static size_t q4km_bytes(int rows, int cols) { return (size_t)rows * (size_t)cols * 9 / 16; } -int main(int argc, char ** argv) { +static bool run_bench_moe_stream_smoke() { int n_expert = 128; int n_cold = 4; int hidden = 5120; int ffn = 3584; int n_expert_used = 8; - for (int i = 1; i < argc; ++i) { - if (std::strcmp(argv[i], "--n-expert") == 0 && i + 1 < argc) n_expert = std::atoi(argv[++i]); - else if (std::strcmp(argv[i], "--n-cold") == 0 && i + 1 < argc) n_cold = std::atoi(argv[++i]); - else if (std::strcmp(argv[i], "--hidden") == 0 && i + 1 < argc) hidden = std::atoi(argv[++i]); - else if (std::strcmp(argv[i], "--ffn") == 0 && i + 1 < argc) ffn = std::atoi(argv[++i]); - else if (std::strcmp(argv[i], "--n-expert-used") == 0 && i + 1 < argc) n_expert_used = std::atoi(argv[++i]); - } - std::printf("bench_moe_stream: n_expert=%d n_cold=%d hidden=%d ffn=%d n_expert_used=%d\n", n_expert, n_cold, hidden, ffn, n_expert_used); @@ -74,24 +65,40 @@ int main(int argc, char ** argv) { std::printf(" file_size=%.2f MiB\n", file_size / 1024.0 / 1024.0); #if defined(_WIN32) - std::fprintf(stderr, "bench_moe_stream: Windows not yet supported in this benchmark\n"); - return 1; + std::fprintf(stderr, "bench_moe_stream: Windows not yet supported in this test\n"); + return false; #else + struct FdGuard { + int fd = -1; + ~FdGuard() { if (fd >= 0) close(fd); } + } fd_guard; + struct MmapGuard { + void * addr = MAP_FAILED; + size_t size = 0; + ~MmapGuard() { if (addr != MAP_FAILED) munmap(addr, size); } + } map_guard; + struct BackendGuard { + ggml_backend_t backend = nullptr; + ~BackendGuard() { if (backend) ggml_backend_free(backend); } + } backend_guard; + MoeHybridStreamEngine engine; + // Create a temporary file with random data char tmppath[] = "/tmp/bench_moe_stream_XXXXXX"; - int fd = mkstemp(tmppath); - if (fd < 0) { perror("mkstemp"); return 1; } + fd_guard.fd = mkstemp(tmppath); + if (fd_guard.fd < 0) { perror("mkstemp"); engine.destroy(); return false; } unlink(tmppath); // auto-delete on close - if (ftruncate(fd, (off_t)file_size) != 0) { perror("ftruncate"); close(fd); return 1; } + if (ftruncate(fd_guard.fd, (off_t)file_size) != 0) { perror("ftruncate"); engine.destroy(); return false; } - void * mmap_addr = ::mmap(nullptr, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); - if (mmap_addr == MAP_FAILED) { perror("mmap"); close(fd); return 1; } + map_guard.addr = ::mmap(nullptr, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_guard.fd, 0); + map_guard.size = file_size; + if (map_guard.addr == MAP_FAILED) { perror("mmap"); engine.destroy(); return false; } // Fill with random data to ensure pages are faulted in std::printf(" filling temp file with random data...\n"); std::mt19937 rng(42); - auto * ptr = static_cast(mmap_addr); + auto * ptr = static_cast(map_guard.addr); for (size_t off = 0; off < file_size; off += 4096) { uint32_t val = rng(); std::memcpy(ptr + off, &val, sizeof(val)); @@ -108,23 +115,19 @@ int main(int argc, char ** argv) { regions.expert_bytes_down = down_bytes; // Init CUDA backend - ggml_backend_t gpu_backend = ggml_backend_cuda_init(0); - if (!gpu_backend) { + backend_guard.backend = ggml_backend_cuda_init(0); + if (!backend_guard.backend) { std::fprintf(stderr, "Failed to init CUDA backend\n"); - munmap(mmap_addr, file_size); - close(fd); - return 1; + engine.destroy(); + return false; } // Init stream engine - MoeHybridStreamEngine engine; std::string err; - if (!engine.init(gpu_backend, expert_total_bytes, &err)) { + if (!engine.init(backend_guard.backend, expert_total_bytes, &err)) { std::fprintf(stderr, "Stream engine init failed: %s\n", err.c_str()); - ggml_backend_free(gpu_backend); - munmap(mmap_addr, file_size); - close(fd); - return 1; + engine.destroy(); + return false; } std::printf(" stream engine ready: pinned=%.1f MiB scratch=%.1f MiB\n", engine.pinned_bytes() / 1024.0 / 1024.0, @@ -140,19 +143,19 @@ int main(int argc, char ** argv) { for (int T : test_T) { // Warm up - engine.prefetch_cold_experts(mmap_addr, file_size, regions, cold_ids.data(), n_cold); + engine.prefetch_cold_experts(map_guard.addr, file_size, regions, cold_ids.data(), n_cold); const int n_iter = 5; double prefetch_total = 0, stream_total = 0; for (int iter = 0; iter < n_iter; ++iter) { auto t0 = Clock::now(); - engine.prefetch_cold_experts(mmap_addr, file_size, regions, cold_ids.data(), n_cold); + engine.prefetch_cold_experts(map_guard.addr, file_size, regions, cold_ids.data(), n_cold); auto t1 = Clock::now(); // Simulate streaming all cold experts for (int ci = 0; ci < n_cold; ++ci) { - engine.stream_expert_sync(mmap_addr, file_size, regions, cold_ids[ci], gpu_backend, nullptr); + engine.stream_expert_sync(map_guard.addr, file_size, regions, cold_ids[ci], backend_guard.backend, nullptr); } auto t2 = Clock::now(); @@ -166,12 +169,12 @@ int main(int argc, char ** argv) { } // Cleanup - engine.destroy(); - ggml_backend_free(gpu_backend); - munmap(mmap_addr, file_size); - close(fd); - std::printf("\nDone.\n"); - return 0; + engine.destroy(); + return true; #endif } + +TEST_CASE(BenchMoeStreamFixture, bench_moe_stream_smoke) { + REQUIRE(run_bench_moe_stream_smoke()); +} diff --git a/server/test/test_qwen35moe_expert_placement.cpp b/server/test/test_qwen35moe_expert_placement.cpp index 05e1b92dd..459925866 100644 --- a/server/test/test_qwen35moe_expert_placement.cpp +++ b/server/test/test_qwen35moe_expert_placement.cpp @@ -1,3 +1,4 @@ +#include "CppUnitTestFramework.hpp" #include "../src/common/moe_hybrid_placement.h" #include "../src/common/moe_hybrid_routing_stats.h" @@ -8,14 +9,11 @@ using namespace dflash::common; -static void expect(bool cond, const char * msg) { - if (!cond) { - std::fprintf(stderr, "FAIL: %s\n", msg); - std::exit(1); - } +namespace { +struct Qwen35MoeExpertPlacementFixture {}; } -int main() { +TEST_CASE(Qwen35MoeExpertPlacementFixture, moe_expert_placement_suite) { MoeHybridRoutingStats stats; stats.n_layer = 2; stats.n_expert = 4; @@ -28,30 +26,29 @@ int main() { MoeHybridPlacement placement; std::string err; - expect(MoeHybridPlacement::build_from_stats(stats, /*total_hot_budget=*/4, - /*min_hot_per_layer=*/1, - placement, &err), err.c_str()); - expect(placement.n_layer == 2, "n_layer"); - expect(placement.hot_counts.size() == 2, "hot_counts size"); - expect(placement.hot_counts[0] == 3, "layer0 got extra hot slots"); - expect(placement.hot_counts[1] == 1, "layer1 kept minimum hot slot"); - expect(placement.is_hot(0, 0), "layer0 expert0 hot"); - expect(placement.is_hot(0, 1), "layer0 expert1 hot"); - expect(placement.is_hot(0, 2), "layer0 expert2 hot"); - expect(!placement.is_hot(0, 3), "layer0 expert3 cold"); - expect(placement.is_hot(1, 0), "layer1 expert0 hot"); - expect(!placement.is_hot(1, 1), "layer1 expert1 cold"); - - expect(placement.matches(2, 4, 2), "placement matches dims"); + REQUIRE(MoeHybridPlacement::build_from_stats(stats, /*total_hot_budget=*/4, + /*min_hot_per_layer=*/1, + placement, &err)); + REQUIRE(placement.n_layer == 2); + REQUIRE(placement.hot_counts.size() == 2); + REQUIRE(placement.hot_counts[0] == 3); + REQUIRE(placement.hot_counts[1] == 1); + REQUIRE(placement.is_hot(0, 0)); + REQUIRE(placement.is_hot(0, 1)); + REQUIRE(placement.is_hot(0, 2)); + REQUIRE(!placement.is_hot(0, 3)); + REQUIRE(placement.is_hot(1, 0)); + REQUIRE(!placement.is_hot(1, 1)); + + REQUIRE(placement.matches(2, 4, 2)); const auto tmp = std::filesystem::temp_directory_path() / "moe-hybrid-placement-test.json"; - expect(placement.save_json(tmp.string(), "moe_hybrid", &err), err.c_str()); + REQUIRE(placement.save_json(tmp.string(), "moe_hybrid", &err)); MoeHybridPlacement loaded; - expect(MoeHybridPlacement::load_json(tmp.string(), loaded, &err), err.c_str()); - expect(loaded.hot_counts == placement.hot_counts, "loaded hot counts"); - expect(loaded.hot_expert_ids == placement.hot_expert_ids, "loaded hot ids"); + REQUIRE(MoeHybridPlacement::load_json(tmp.string(), loaded, &err)); + REQUIRE(loaded.hot_counts == placement.hot_counts); + REQUIRE(loaded.hot_expert_ids == placement.hot_expert_ids); std::filesystem::remove(tmp); std::printf("OK\n"); - return 0; } diff --git a/server/test/test_qwen35moe_routing_stats.cpp b/server/test/test_qwen35moe_routing_stats.cpp index cfdf87f07..a69996ec9 100644 --- a/server/test/test_qwen35moe_routing_stats.cpp +++ b/server/test/test_qwen35moe_routing_stats.cpp @@ -1,3 +1,4 @@ +#include "CppUnitTestFramework.hpp" #include "../src/common/moe_hybrid_routing_stats.h" #include @@ -8,52 +9,48 @@ using namespace dflash::common; -static void expect(bool cond, const char * msg) { - if (!cond) { - std::fprintf(stderr, "FAIL: %s\n", msg); - std::exit(1); - } +namespace { +struct Qwen35MoeRoutingStatsFixture {}; } -int main() { +TEST_CASE(Qwen35MoeRoutingStatsFixture, moe_routing_stats_suite) { MoeHybridRoutingStats stats; - expect(stats.init(2, 4, 2), "init"); - expect(stats.matches(2, 4, 2), "matches after init"); + REQUIRE(stats.init(2, 4, 2)); + REQUIRE(stats.matches(2, 4, 2)); const int32_t layer0_a[] = {2, 1}; const int32_t layer0_b[] = {2, 3}; const int32_t layer1_a[] = {0, 0}; - expect(stats.observe(0, layer0_a, 2), "observe layer0_a"); - expect(stats.observe(0, layer0_b, 2), "observe layer0_b"); - expect(stats.observe(1, layer1_a, 2), "observe layer1_a"); + REQUIRE(stats.observe(0, layer0_a, 2)); + REQUIRE(stats.observe(0, layer0_b, 2)); + REQUIRE(stats.observe(1, layer1_a, 2)); - expect(stats.count(0, 2) == 2, "layer0 expert2 count"); - expect(stats.count(0, 1) == 1, "layer0 expert1 count"); - expect(stats.count(0, 3) == 1, "layer0 expert3 count"); - expect(stats.count(1, 0) == 2, "layer1 expert0 count"); - expect(stats.layer_totals[0] == 4, "layer0 total"); - expect(stats.layer_totals[1] == 2, "layer1 total"); + REQUIRE(stats.count(0, 2) == 2); + REQUIRE(stats.count(0, 1) == 1); + REQUIRE(stats.count(0, 3) == 1); + REQUIRE(stats.count(1, 0) == 2); + REQUIRE(stats.layer_totals[0] == 4); + REQUIRE(stats.layer_totals[1] == 2); std::vector ranked0 = stats.ranked_experts(0); - expect(ranked0.size() == 4, "ranked size"); - expect(ranked0[0] == 2, "ranked leader"); + REQUIRE(ranked0.size() == 4); + REQUIRE(ranked0[0] == 2); std::vector hot0 = stats.hot_experts(0, 2); - expect(hot0.size() == 2, "hot size"); - expect(hot0[0] == 2, "hot leader"); + REQUIRE(hot0.size() == 2); + REQUIRE(hot0[0] == 2); const auto tmp = std::filesystem::temp_directory_path() / "moe-hybrid-routing-stats-test.csv"; std::string err; - expect(stats.save_csv(tmp.string(), &err), err.c_str()); + REQUIRE(stats.save_csv(tmp.string(), &err)); MoeHybridRoutingStats loaded; - expect(MoeHybridRoutingStats::load_csv(tmp.string(), loaded, &err), err.c_str()); - expect(loaded.matches(2, 4, 2), "loaded matches dims"); - expect(loaded.count(0, 2) == 2, "loaded count"); - expect(loaded.layer_totals[1] == 2, "loaded total"); + REQUIRE(MoeHybridRoutingStats::load_csv(tmp.string(), loaded, &err)); + REQUIRE(loaded.matches(2, 4, 2)); + REQUIRE(loaded.count(0, 2) == 2); + REQUIRE(loaded.layer_totals[1] == 2); std::filesystem::remove(tmp); std::printf("OK\n"); - return 0; } diff --git a/server/test/test_qwen35moe_swap_manager.cpp b/server/test/test_qwen35moe_swap_manager.cpp index cbbf5e6ee..55201510c 100644 --- a/server/test/test_qwen35moe_swap_manager.cpp +++ b/server/test/test_qwen35moe_swap_manager.cpp @@ -1,3 +1,4 @@ +#include "CppUnitTestFramework.hpp" #include "../src/common/moe_hybrid_swap_manager.h" #include "../src/common/moe_hybrid_placement.h" #include "../src/common/moe_hybrid_routing_stats.h" @@ -7,14 +8,11 @@ using namespace dflash::common; -static void expect(bool cond, const char * msg) { - if (!cond) { - std::fprintf(stderr, "FAIL: %s\n", msg); - std::exit(1); - } +namespace { +struct Qwen35MoeSwapManagerFixture {}; } -int main() { +TEST_CASE(Qwen35MoeSwapManagerFixture, moe_swap_manager_suite) { MoeHybridRoutingStats stats; stats.n_layer = 2; stats.n_expert = 4; @@ -39,15 +37,14 @@ int main() { MoeHybridSwapPlan plan; std::string err; - expect(build_moe_hybrid_swap_plan(placement, stats, policy, plan, &err), err.c_str()); - expect(plan.actions.size() == 1, "one swap planned"); - expect(plan.actions[0].layer_idx == 0, "layer0 swap"); - expect(plan.actions[0].evict_expert == 1, "evict weakest hot"); - expect(plan.actions[0].promote_expert == 0, "promote best cold"); - expect(plan.next_placement.is_hot(0, 0), "layer0 expert0 hot after plan"); - expect(!plan.next_placement.is_hot(0, 1), "layer0 expert1 evicted"); - expect(plan.next_placement.is_hot(1, 0), "layer1 unchanged"); + REQUIRE(build_moe_hybrid_swap_plan(placement, stats, policy, plan, &err)); + REQUIRE(plan.actions.size() == 1); + REQUIRE(plan.actions[0].layer_idx == 0); + REQUIRE(plan.actions[0].evict_expert == 1); + REQUIRE(plan.actions[0].promote_expert == 0); + REQUIRE(plan.next_placement.is_hot(0, 0)); + REQUIRE(!plan.next_placement.is_hot(0, 1)); + REQUIRE(plan.next_placement.is_hot(1, 0)); std::printf("OK\n"); - return 0; } diff --git a/server/test/test_restore_delta.cpp b/server/test/test_restore_delta.cpp index 2b69f610a..8f3d2787e 100644 --- a/server/test/test_restore_delta.cpp +++ b/server/test/test_restore_delta.cpp @@ -1,40 +1,29 @@ +#include "CppUnitTestFramework.hpp" #include "common/restore_delta.h" -#include #include #include -static int failures = 0; - -static void check(bool ok, const char * msg) { - if (!ok) { - std::fprintf(stderr, "FAIL: %s\n", msg); - failures++; - } +namespace { +struct RestoreDeltaFixture {}; } -int main() { +TEST_CASE(RestoreDeltaFixture, restore_delta_excludes_cached_prefix) { using dflash::common::restore_prompt_delta; - - // Regression for #216: RESTORE receives the full prompt, but the backend - // must prefill only the suffix that was not covered by the cached snapshot. const std::vector prompt = {10, 11, 20, 21, 22}; const std::vector delta = restore_prompt_delta(prompt, 2); - check((delta == std::vector{20, 21, 22}), - "RESTORE delta excludes cached prefix tokens"); + CHECK(delta == std::vector({20, 21, 22})); +} +TEST_CASE(RestoreDeltaFixture, restore_delta_empty_on_full_hit) { + using dflash::common::restore_prompt_delta; + const std::vector prompt = {10, 11, 20, 21, 22}; const std::vector full_hit_delta = restore_prompt_delta(prompt, 5); - check(full_hit_delta.empty(), - "RESTORE delta is empty when prompt exactly matches cached prefix"); - - bool rejected_long_prefix = false; - try { - (void)restore_prompt_delta(prompt, 6); - } catch (const std::out_of_range &) { - rejected_long_prefix = true; - } - check(rejected_long_prefix, - "RESTORE delta rejects cached prefixes longer than the prompt"); + CHECK(full_hit_delta.empty()); +} - return failures == 0 ? 0 : 1; +TEST_CASE(RestoreDeltaFixture, restore_delta_rejects_long_prefix) { + using dflash::common::restore_prompt_delta; + const std::vector prompt = {10, 11, 20, 21, 22}; + CHECK_THROW(std::out_of_range, UNUSED_RETURN(restore_prompt_delta(prompt, 6))); } diff --git a/server/test/test_server_unit.cpp b/server/test/test_server_unit.cpp index 7a5455b44..3567a92a3 100644 --- a/server/test/test_server_unit.cpp +++ b/server/test/test_server_unit.cpp @@ -3,10 +3,11 @@ // Tests: SseEmitter, ToolParser, Reasoning, PrefixCache (hash/boundary), // UTF-8 utilities. // -// Ported from ds4_server.c's ds4_server_unit_tests_run() pattern. // Build: cmake --build . --target test_server_unit // Run: ./test_server_unit +#include "CppUnitTestFramework.hpp" + #include "server/sse_emitter.h" #include "server/tool_parser.h" #include "server/reasoning.h" @@ -68,36 +69,20 @@ std::vector normalize_chat_messages( ToolMemory & tool_memory); } -// ─── Test framework (ds4 style) ──────────────────────────────────────── - -static int test_failures = 0; -static int test_count = 0; -static const char * current_test = nullptr; +namespace { +struct ServerUnitFixture {}; +} +// This file still uses free helper functions outside fixture-member scope, so +// keep a local shim where the README's direct REQUIRE/CHECK macros are not +// available without refactoring those helpers into CommonFixture members. #define TEST_ASSERT(expr) do { \ - test_count++; \ - if (!(expr)) { \ - test_failures++; \ - std::fprintf(stderr, " FAIL: %s:%d: %s\n", __FILE__, __LINE__, #expr); \ - } \ -} while (0) - -#define TEST_ASSERT_MSG(expr, msg) do { \ - test_count++; \ - if (!(expr)) { \ - test_failures++; \ - std::fprintf(stderr, " FAIL: %s:%d: %s — %s\n", __FILE__, __LINE__, #expr, msg); \ + auto _cpputf_exception = CppUnitTestFramework::Assert::IsTrue(static_cast(expr), #expr); \ + if (_cpputf_exception) { \ + throw *_cpputf_exception; \ } \ } while (0) - -#define RUN_TEST(fn) do { \ - current_test = #fn; \ - std::fprintf(stderr, " %s ...", #fn); \ - int before = test_failures; \ - fn(); \ - if (test_failures == before) std::fprintf(stderr, " ok\n"); \ - else std::fprintf(stderr, "\n"); \ -} while (0) +#define TEST_ASSERT_MSG(expr, msg) TEST_ASSERT(expr) // ─── Helper: create an SseEmitter with minimal config ────────────────── @@ -170,21 +155,21 @@ static std::string concat(const std::vector & chunks) { // UTF-8 utility tests // ═══════════════════════════════════════════════════════════════════════ -static void test_utf8_safe_len_ascii() { +TEST_CASE(ServerUnitFixture, test_utf8_safe_len_ascii) { std::string s = "Hello, world!"; TEST_ASSERT(utf8_safe_len(s, s.size()) == s.size()); TEST_ASSERT(utf8_safe_len(s, 5) == 5); TEST_ASSERT(utf8_safe_len(s, 0) == 0); } -static void test_utf8_safe_len_partial_2byte() { +TEST_CASE(ServerUnitFixture, test_utf8_safe_len_partial_2byte) { // é = 0xC3 0xA9 std::string s = "caf\xC3\xA9!"; // "café!" TEST_ASSERT(utf8_safe_len(s, 5) == 5); // after é, ok TEST_ASSERT(utf8_safe_len(s, 4) == 3); // mid-é, snap back to before é } -static void test_utf8_safe_len_partial_3byte() { +TEST_CASE(ServerUnitFixture, test_utf8_safe_len_partial_3byte) { // ん = 0xE3 0x82 0x93 std::string s = "A\xE3\x82\x93Z"; // "AんZ" TEST_ASSERT(utf8_safe_len(s, 4) == 4); // after ん @@ -192,7 +177,7 @@ static void test_utf8_safe_len_partial_3byte() { TEST_ASSERT(utf8_safe_len(s, 2) == 1); // mid-ん, snap back to A } -static void test_utf8_safe_len_partial_4byte() { +TEST_CASE(ServerUnitFixture, test_utf8_safe_len_partial_4byte) { // 🚩 = 0xF0 0x9F 0x9A 0xA9 std::string s = "A \xF0\x9F\x9A\xA9 done"; TEST_ASSERT(utf8_safe_len(s, 6) == 6); // after 🚩 @@ -202,12 +187,12 @@ static void test_utf8_safe_len_partial_4byte() { TEST_ASSERT(utf8_safe_len(s, 3) == 2); } -static void test_utf8_sanitize_valid() { +TEST_CASE(ServerUnitFixture, test_utf8_sanitize_valid) { std::string s = "Hello, world! 🎉"; TEST_ASSERT(utf8_sanitize(s) == s); } -static void test_utf8_sanitize_replaces_invalid() { +TEST_CASE(ServerUnitFixture, test_utf8_sanitize_replaces_invalid) { // Lone continuation byte std::string s = "A\x80Z"; std::string out = utf8_sanitize(s); @@ -221,7 +206,7 @@ static void test_utf8_sanitize_replaces_invalid() { TEST_ASSERT(out2.size() > 1); // has replacement(s) } -static void test_utf8_sanitize_empty() { +TEST_CASE(ServerUnitFixture, test_utf8_sanitize_empty) { TEST_ASSERT(utf8_sanitize("") == ""); } @@ -229,20 +214,20 @@ static void test_utf8_sanitize_empty() { // Reasoning parser tests // ═══════════════════════════════════════════════════════════════════════ -static void test_reasoning_basic() { +TEST_CASE(ServerUnitFixture, test_reasoning_basic) { auto r = parse_reasoning("I need to thinkThe answer is 42"); TEST_ASSERT(r.has_reasoning); TEST_ASSERT(r.reasoning == "I need to think"); TEST_ASSERT(r.content == "The answer is 42"); } -static void test_reasoning_no_tags() { +TEST_CASE(ServerUnitFixture, test_reasoning_no_tags) { auto r = parse_reasoning("Just plain text"); TEST_ASSERT(!r.has_reasoning); TEST_ASSERT(r.content == "Just plain text"); } -static void test_reasoning_started_in_thinking() { +TEST_CASE(ServerUnitFixture, test_reasoning_started_in_thinking) { auto r = parse_reasoning("thinking bodycontent here", true, true); TEST_ASSERT(r.has_reasoning); @@ -250,7 +235,7 @@ static void test_reasoning_started_in_thinking() { TEST_ASSERT(r.content == "content here"); } -static void test_emitter_started_in_thinking_without_open_tag() { +TEST_CASE(ServerUnitFixture, test_emitter_started_in_thinking_without_open_tag) { auto em = make_emitter(ApiFormat::OPENAI_CHAT, json::array(), true); auto chunks = em.emit_token("Thinking Process: calculate 9 + 6."); auto close = em.emit_token(""); @@ -265,7 +250,7 @@ static void test_emitter_started_in_thinking_without_open_tag() { TEST_ASSERT(all.find("\"content\":\"Thinking Process") == std::string::npos); } -static void test_reasoning_unclosed_think() { +TEST_CASE(ServerUnitFixture, test_reasoning_unclosed_think) { auto r = parse_reasoning("still thinking no close", true, false); TEST_ASSERT(r.has_reasoning); @@ -273,20 +258,20 @@ static void test_reasoning_unclosed_think() { TEST_ASSERT(r.content.empty()); } -static void test_reasoning_empty_thinking() { +TEST_CASE(ServerUnitFixture, test_reasoning_empty_thinking) { auto r = parse_reasoning("answer"); TEST_ASSERT(!r.has_reasoning); // empty reasoning TEST_ASSERT(r.content == "answer"); } -static void test_reasoning_whitespace_in_think() { +TEST_CASE(ServerUnitFixture, test_reasoning_whitespace_in_think) { auto r = parse_reasoning("\n reasoning \n\ncontent"); TEST_ASSERT(r.has_reasoning); TEST_ASSERT(r.reasoning == "reasoning"); TEST_ASSERT(r.content == "content"); } -static void test_reasoning_disabled() { +TEST_CASE(ServerUnitFixture, test_reasoning_disabled) { // When thinking disabled but tags present, the parser still finds them // (the caller decides whether to use the reasoning field). auto r = parse_reasoning("ignoredcontent", @@ -299,7 +284,7 @@ static void test_reasoning_disabled() { // Tool parser tests // ═══════════════════════════════════════════════════════════════════════ -static void test_parse_tool_call_xml() { +TEST_CASE(ServerUnitFixture, test_parse_tool_call_xml) { std::string text = "Some text\n" "\n" @@ -321,7 +306,7 @@ static void test_parse_tool_call_xml() { TEST_ASSERT(result.cleaned_text.find("") == std::string::npos); } -static void test_parse_bare_function_xml() { +TEST_CASE(ServerUnitFixture, test_parse_bare_function_xml) { std::string text = "\n" "/home\n" @@ -335,7 +320,7 @@ static void test_parse_bare_function_xml() { } } -static void test_parse_bare_tool_name_xml_with_function_close() { +TEST_CASE(ServerUnitFixture, test_parse_bare_tool_name_xml_with_function_close) { std::string text = "\n\n\nLet me find the correct line range for the tests array.\n\n\n" "\n" @@ -369,7 +354,7 @@ static void test_parse_bare_tool_name_xml_with_function_close() { std::string::npos); } -static void test_parse_json_tool_call() { +TEST_CASE(ServerUnitFixture, test_parse_json_tool_call) { std::string text = "{\"name\": \"search\", \"arguments\": {\"query\": \"hello world\"}}"; auto result = parse_tool_calls(text); @@ -381,7 +366,7 @@ static void test_parse_json_tool_call() { } } -static void test_parse_single_tool_bare_json_args() { +TEST_CASE(ServerUnitFixture, test_parse_single_tool_bare_json_args) { std::string text = "{\n" " \"command\": \"git branch --show-current\"\n" @@ -396,7 +381,7 @@ static void test_parse_single_tool_bare_json_args() { TEST_ASSERT(result.cleaned_text.empty()); } -static void test_parse_single_tool_bare_json_args_allows_empty_optional_object() { +TEST_CASE(ServerUnitFixture, test_parse_single_tool_bare_json_args_allows_empty_optional_object) { auto result = parse_tool_calls("{}", optional_shell_tools()); TEST_ASSERT(result.tool_calls.size() == 1); if (!result.tool_calls.empty()) { @@ -408,28 +393,28 @@ static void test_parse_single_tool_bare_json_args_allows_empty_optional_object() TEST_ASSERT(result.cleaned_text.empty()); } -static void test_parse_single_tool_bare_json_args_rejects_prose() { +TEST_CASE(ServerUnitFixture, test_parse_single_tool_bare_json_args_rejects_prose) { std::string text = "The command is {\"command\": \"git status\"}."; auto result = parse_tool_calls(text, shell_tools()); TEST_ASSERT(result.tool_calls.empty()); TEST_ASSERT(result.cleaned_text == text); } -static void test_parse_single_tool_bare_json_args_rejects_ambiguous_tools() { +TEST_CASE(ServerUnitFixture, test_parse_single_tool_bare_json_args_rejects_ambiguous_tools) { std::string text = "{\"command\": \"git status\"}"; auto result = parse_tool_calls(text, weather_tools()); TEST_ASSERT(result.tool_calls.empty()); TEST_ASSERT(result.cleaned_text == text); } -static void test_parse_no_tools() { +TEST_CASE(ServerUnitFixture, test_parse_no_tools) { std::string text = "Just plain text without any tool calls."; auto result = parse_tool_calls(text); TEST_ASSERT(result.tool_calls.empty()); TEST_ASSERT(!result.cleaned_text.empty()); } -static void test_parse_tool_code_wrapper() { +TEST_CASE(ServerUnitFixture, test_parse_tool_code_wrapper) { std::string text = "\n" "{\"name\": \"bash\", \"arguments\": {\"command\": \"ls -la\"}}\n" @@ -441,7 +426,7 @@ static void test_parse_tool_code_wrapper() { } } -static void test_parse_tool_allowed_filter() { +TEST_CASE(ServerUnitFixture, test_parse_tool_allowed_filter) { std::string text = "\n" "1\n" @@ -463,7 +448,7 @@ static void test_parse_tool_allowed_filter() { // and the args go through coerce_relaxed_json before becoming the // argument object. -static void test_parse_call_verb_empty_args() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_empty_args) { // Bareword `call:get_weather{}` at start-of-string — sentinel // matches the leading `^` anchor; body is the empty object `{}`. auto result = parse_tool_calls("call:get_weather{}"); @@ -478,7 +463,7 @@ static void test_parse_call_verb_empty_args() { TEST_ASSERT(result.cleaned_text.find("call:get_weather") == std::string::npos); } -static void test_parse_call_verb_strict_json_args() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_strict_json_args) { // Strict JSON args go through json::parse directly in // coerce_relaxed_json's fast path. auto result = parse_tool_calls("call:get_weather{\"city\": \"NYC\"}"); @@ -490,7 +475,7 @@ static void test_parse_call_verb_strict_json_args() { } } -static void test_parse_call_verb_namespaced_verb() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_namespaced_verb) { // `ns:foo` namespaced verbs — the colon-strip logic in pattern 5 // strips everything up to the last `:` so the registered tool name // is just `foo`. @@ -503,7 +488,7 @@ static void test_parse_call_verb_namespaced_verb() { } } -static void test_parse_call_verb_whitespace_before_key() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_whitespace_before_key) { // Leading whitespace inside the brace body must not break parsing. // (Whitespace tolerance is provided by json::parse / the relaxed // fallback rewriter.) @@ -516,14 +501,14 @@ static void test_parse_call_verb_whitespace_before_key() { } } -static void test_parse_call_verb_missing_close_brace_rejected() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_missing_close_brace_rejected) { // Unbalanced opener — balanced_braces_end returns npos so pattern 5 // bails out and produces no tool call. The text leaks through. auto result = parse_tool_calls("call:get_weather{\"city\": \"NYC\""); TEST_ASSERT(result.tool_calls.empty()); } -static void test_parse_call_verb_narrative_without_body_rejected() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_narrative_without_body_rejected) { // Narrative usage with a non-balanced body — sentinel matches the // space before `call:`, but the `{` has no matching `}` so the // call is discarded. @@ -531,7 +516,7 @@ static void test_parse_call_verb_narrative_without_body_rejected() { TEST_ASSERT(result.tool_calls.empty()); } -static void test_parse_call_verb_underscore_prefix() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_underscore_prefix) { // SentencePiece artifact: `_call:` (the `_` is the literal // underscore character; sentinel char-class includes `_` for // exactly this case). @@ -544,7 +529,7 @@ static void test_parse_call_verb_underscore_prefix() { } } -static void test_parse_call_verb_nested_object_args() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_nested_object_args) { // Nested `{}` inside the args — balanced_braces_end tracks depth so // the outer close isn't consumed by the inner object. auto result = parse_tool_calls( @@ -559,7 +544,7 @@ static void test_parse_call_verb_nested_object_args() { } } -static void test_parse_call_verb_back_to_back() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_back_to_back) { // Gemma frequently emits multiple invocations back-to-back. The // sentinel char-class includes `}` so the second `call:` is found // after the first closes. @@ -576,7 +561,7 @@ static void test_parse_call_verb_back_to_back() { } } -static void test_parse_call_verb_relaxed_single_quotes() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_relaxed_single_quotes) { // Relaxed-JSON fallback: single-quoted strings + bare identifier // keys are rewritten to strict JSON before parse. auto result = parse_tool_calls("call:foo{city: 'NYC'}"); @@ -588,7 +573,7 @@ static void test_parse_call_verb_relaxed_single_quotes() { } } -static void test_parse_call_verb_glued_to_word_rejected() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_glued_to_word_rejected) { // No sentinel char before `call:` (glued to identifier) — pattern 5 // must NOT match. `_` is a deliberate exception covered by its // own test; here we use a regular letter. @@ -599,7 +584,7 @@ static void test_parse_call_verb_glued_to_word_rejected() { TEST_ASSERT(result.tool_calls.empty()); } -static void test_parse_call_verb_does_not_hijack_inner_name() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_does_not_hijack_inner_name) { // Regression: pattern 5 must run before pattern 6 so that an inner // {"name": "...", "arguments": {}} in the call's args doesn't get // hijacked into a spurious bare-JSON tool call. @@ -620,7 +605,7 @@ static void test_parse_call_verb_does_not_hijack_inner_name() { // and were relocated here when #341 was split. They focus on edge cases // that complement the core call:{} suite above. -static void test_parse_call_verb_single() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_single) { std::string text = "call:get_country_info{country: \"France\"}"; auto result = parse_tool_calls(text); TEST_ASSERT(result.tool_calls.size() == 1); @@ -632,7 +617,7 @@ static void test_parse_call_verb_single() { TEST_ASSERT(result.cleaned_text.find("call:") == std::string::npos); } -static void test_parse_call_verb_namespaced() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_namespaced) { std::string text = "call:execute-bead:read-file{path: \"crates/foo/src/lib.rs\"}"; auto result = parse_tool_calls(text); TEST_ASSERT(result.tool_calls.size() == 1); @@ -644,7 +629,7 @@ static void test_parse_call_verb_namespaced() { } } -static void test_parse_call_verb_snake_and_hyphen() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_snake_and_hyphen) { std::string text = "call:execute-bead:list-files{path: \"src/\"}\n\n" "call:execute-bead:read_file{path: \"a/b.rs\"}"; @@ -656,7 +641,7 @@ static void test_parse_call_verb_snake_and_hyphen() { } } -static void test_parse_call_verb_tool_allowed_filter() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_tool_allowed_filter) { std::string text = "call:disallowed_verb{x: 1}call:allowed_verb{y: 2}"; json tools = json::array({ {{"type", "function"}, {"function", {{"name", "allowed_verb"}}}} @@ -668,14 +653,14 @@ static void test_parse_call_verb_tool_allowed_filter() { } } -static void test_parse_call_verb_inline_prose_rejected() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_inline_prose_rejected) { // No sentinel char before `call:` — must NOT match. std::string text = "narrative.call:foo{x:1}"; auto result = parse_tool_calls(text); TEST_ASSERT(result.tool_calls.empty()); } -static void test_parse_call_verb_inline_prose_after_space() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_inline_prose_after_space) { // Whitespace IS a valid sentinel — this should match. std::string text = "Sure, I'll call:foo{x: 1}"; auto result = parse_tool_calls(text); @@ -685,14 +670,14 @@ static void test_parse_call_verb_inline_prose_after_space() { } } -static void test_parse_call_verb_malformed_args() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_malformed_args) { // Unterminated brace — drop the call, don't crash. std::string text = "call:foo{country: \"France\""; auto result = parse_tool_calls(text); TEST_ASSERT(result.tool_calls.empty()); } -static void test_parse_call_verb_inner_brace_in_string() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_inner_brace_in_string) { // The `{` and `}` inside the string value must not confuse the // balanced-brace scanner. std::string text = "call:foo{cmd: \"echo {not_a_brace} ok\"}"; @@ -704,7 +689,7 @@ static void test_parse_call_verb_inner_brace_in_string() { } } -static void test_parse_call_verb_unquoted_keys() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_unquoted_keys) { // Relaxed-JSON path: bare keys get quoted. std::string text = "call:foo{path: \"x\", count: 3}"; auto result = parse_tool_calls(text); @@ -716,7 +701,7 @@ static void test_parse_call_verb_unquoted_keys() { } } -static void test_parse_call_verb_cleaned_text() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_cleaned_text) { // The matched span should be stripped from cleaned_text. std::string text = "Hello call:foo{x: 1} world."; auto result = parse_tool_calls(text); @@ -726,7 +711,7 @@ static void test_parse_call_verb_cleaned_text() { TEST_ASSERT(result.cleaned_text.find("world.") != std::string::npos); } -static void test_parse_call_verb_intercept_inner_json() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_intercept_inner_json) { // Regression case: inner args of the form {"name": ..., "arguments": ...} // must NOT be picked up by pattern 6 (bare-JSON sweep) as a spurious // `inner` ToolCall. Exactly one ToolCall, named `outer`, with the @@ -742,7 +727,7 @@ static void test_parse_call_verb_intercept_inner_json() { } } -static void test_parse_call_verb_multiline_args() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_multiline_args) { // Snapshot rows have multi-line nested args; the balanced-brace // scanner is line-agnostic, so this must Just Work. std::string text = @@ -762,7 +747,7 @@ static void test_parse_call_verb_multiline_args() { } -static void test_parse_call_verb_singlequote_with_inner_doublequote() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_singlequote_with_inner_doublequote) { // Cubic PR #329 review: when the relaxed-JSON rewrite converts // single-quoted strings to double-quoted, inner `"` chars must be // escaped to `\"` — otherwise `'he said "hi"'` rewrites to @@ -778,7 +763,7 @@ static void test_parse_call_verb_singlequote_with_inner_doublequote() { } } -static void test_parse_call_verb_backtick_with_inner_doublequote() { +TEST_CASE(ServerUnitFixture, test_parse_call_verb_backtick_with_inner_doublequote) { // Same escape concern as the single-quote case, but with the // backtick string flavor. std::string text = "call:say{quote: `he said \"hi\" loudly`}"; @@ -795,7 +780,7 @@ static void test_parse_call_verb_backtick_with_inner_doublequote() { // SSE Emitter tests // ═══════════════════════════════════════════════════════════════════════ -static void test_emitter_reasoning_split_openai() { +TEST_CASE(ServerUnitFixture, test_emitter_reasoning_split_openai) { // Feed reasoning + content through emitter, verify split. // Model emits the opening as its first token (Qwen3.6 path // — the streaming on_token lambda maps the special id to @@ -824,7 +809,7 @@ static void test_emitter_reasoning_split_openai() { // (model self-closes mid-stream). Each test feeds tokens // one-per-call so the emit_token index is straightforward to reason // about. -static void test_emitter_first_content_index_natural_close() { +TEST_CASE(ServerUnitFixture, test_emitter_first_content_index_natural_close) { // Emit reasoning tokens (with explicit open + // close), then content tokens. The emit_token_count() reflects // all delivered tokens; the reasoning/content split is also @@ -848,7 +833,7 @@ static void test_emitter_first_content_index_natural_close() { TEST_ASSERT(em.accumulated_text().find("content2") != std::string::npos); } -static void test_emitter_first_content_index_never_closed() { +TEST_CASE(ServerUnitFixture, test_emitter_first_content_index_never_closed) { // Model opens then emits reasoning only — never closes // . All produced text lands in reasoning_text; visible // accumulated_text stays empty. @@ -865,7 +850,7 @@ static void test_emitter_first_content_index_never_closed() { TEST_ASSERT(em.accumulated_text().empty()); } -static void test_emitter_first_content_index_content_only() { +TEST_CASE(ServerUnitFixture, test_emitter_first_content_index_content_only) { // Non-thinking request: emitter starts in CONTENT mode, so the // very first emit_token lands at index 0. auto em = make_emitter(ApiFormat::OPENAI_CHAT); @@ -877,7 +862,7 @@ static void test_emitter_first_content_index_content_only() { TEST_ASSERT(em.emit_token_count() == 1); } -static void test_emitter_first_content_index_qwen36_streaming_thinking() { +TEST_CASE(ServerUnitFixture, test_emitter_first_content_index_qwen36_streaming_thinking) { // Regression: when the chat template emits a leading `` token // (Qwen3.6 thinking-enabled path, or gemma4 `<|channel>` → `` // map) the emitter starts in CONTENT mode by default and naively @@ -911,7 +896,7 @@ static void test_emitter_first_content_index_qwen36_streaming_thinking() { TEST_ASSERT(em.emit_token_count() - em.first_content_token_index() > 0); } -static void test_emitter_reasoning_strips_leading_think_tag() { +TEST_CASE(ServerUnitFixture, test_emitter_reasoning_strips_leading_think_tag) { // Model emits leading whitespace + as one token, then // continues thinking. The leading--with-whitespace-prefix // strip ensures the reasoning text doesn't contain the open tag. @@ -930,7 +915,7 @@ static void test_emitter_reasoning_strips_leading_think_tag() { TEST_ASSERT(em.reasoning_text().find("Actual reasoning") != std::string::npos); } -static void test_emitter_content_only_no_thinking() { +TEST_CASE(ServerUnitFixture, test_emitter_content_only_no_thinking) { auto em = make_emitter(ApiFormat::OPENAI_CHAT); em.emit_start(); em.emit_token("Hello, world!"); @@ -940,7 +925,7 @@ static void test_emitter_content_only_no_thinking() { TEST_ASSERT(em.reasoning_text().empty()); } -static void test_emitter_tool_buffer_detection() { +TEST_CASE(ServerUnitFixture, test_emitter_tool_buffer_detection) { // When the emitter sees , it should buffer and parse tools. auto em = make_emitter(ApiFormat::OPENAI_CHAT, weather_tools()); em.emit_start(); @@ -959,7 +944,7 @@ static void test_emitter_tool_buffer_detection() { TEST_ASSERT(em.accumulated_text().find("") == std::string::npos); } -static void test_emitter_anthropic_tool_use_blocks() { +TEST_CASE(ServerUnitFixture, test_emitter_anthropic_tool_use_blocks) { // The Anthropic streaming tool-use branch used to be a no-op; the model // would emit a ... block, the parser would detect // it, but no tool_use SSE event was sent. Verify the lifecycle now: @@ -998,7 +983,7 @@ static void test_emitter_anthropic_tool_use_blocks() { TEST_ASSERT(n_stop >= 2); } -static void test_emitter_single_tool_bare_json_args() { +TEST_CASE(ServerUnitFixture, test_emitter_single_tool_bare_json_args) { auto em = make_emitter(ApiFormat::ANTHROPIC, shell_tools()); em.emit_start(); em.emit_token("{\n"); @@ -1015,7 +1000,7 @@ static void test_emitter_single_tool_bare_json_args() { TEST_ASSERT(em.accumulated_text().empty()); } -static void test_emitter_bare_json_args_do_not_trigger_after_content() { +TEST_CASE(ServerUnitFixture, test_emitter_bare_json_args_do_not_trigger_after_content) { auto em = make_emitter(ApiFormat::ANTHROPIC, shell_tools()); em.emit_start(); em.emit_token("This answer already emitted visible prose before JSON appears."); @@ -1029,7 +1014,7 @@ static void test_emitter_bare_json_args_do_not_trigger_after_content() { std::string::npos); } -static void test_emitter_bare_function_tool_buffer_detection() { +TEST_CASE(ServerUnitFixture, test_emitter_bare_function_tool_buffer_detection) { auto em = make_emitter(ApiFormat::OPENAI_CHAT, weather_tools()); em.emit_start(); em.emit_token("\n" @@ -1048,7 +1033,7 @@ static void test_emitter_bare_function_tool_buffer_detection() { TEST_ASSERT(em.accumulated_text().find("") == std::string::npos); } -static void test_emitter_does_not_leak_malformed_tool_xml() { +TEST_CASE(ServerUnitFixture, test_emitter_does_not_leak_malformed_tool_xml) { auto em = make_emitter(ApiFormat::OPENAI_CHAT, weather_tools()); em.emit_start(); em.emit_token("Let me list files.\n\n"); @@ -1065,7 +1050,7 @@ static void test_emitter_does_not_leak_malformed_tool_xml() { TEST_ASSERT(em.accumulated_text().find("") == std::string::npos); } -static void test_emitter_parses_tool_call_missing_outer_close() { +TEST_CASE(ServerUnitFixture, test_emitter_parses_tool_call_missing_outer_close) { auto em = make_emitter(ApiFormat::OPENAI_CHAT, weather_tools()); em.emit_start(); em.emit_token("\n" @@ -1086,7 +1071,7 @@ static void test_emitter_parses_tool_call_missing_outer_close() { TEST_ASSERT(em.accumulated_text().find("") == std::string::npos); } -static void test_emitter_no_tools_keeps_tool_like_text() { +TEST_CASE(ServerUnitFixture, test_emitter_no_tools_keeps_tool_like_text) { auto em = make_emitter(ApiFormat::OPENAI_CHAT); em.emit_start(); em.emit_token("\n" @@ -1098,7 +1083,7 @@ static void test_emitter_no_tools_keeps_tool_like_text() { TEST_ASSERT(em.accumulated_text().find("") != std::string::npos); } -static void test_emitter_anthropic_structure() { +TEST_CASE(ServerUnitFixture, test_emitter_anthropic_structure) { // Verify Anthropic format emits proper event sequence. auto em = make_emitter(ApiFormat::ANTHROPIC); auto start = em.emit_start(); @@ -1123,7 +1108,7 @@ static void test_emitter_anthropic_structure() { TEST_ASSERT(finish_str.find("message_stop") != std::string::npos); } -static void test_emitter_responses_structure() { +TEST_CASE(ServerUnitFixture, test_emitter_responses_structure) { auto em = make_emitter(ApiFormat::RESPONSES); auto start = em.emit_start(); std::string start_str = concat(start); @@ -1138,7 +1123,7 @@ static void test_emitter_responses_structure() { TEST_ASSERT(finish_str.find("response.completed") != std::string::npos); } -static void test_emitter_responses_bare_function_tool_call() { +TEST_CASE(ServerUnitFixture, test_emitter_responses_bare_function_tool_call) { json tools = json::array({{ {"type", "function"}, {"name", "exec_command"}, @@ -1167,7 +1152,7 @@ static void test_emitter_responses_bare_function_tool_call() { TEST_ASSERT(finish_str.find("response.function_call_arguments.done") != std::string::npos); } -static void test_emitter_streaming_openai_has_done() { +TEST_CASE(ServerUnitFixture, test_emitter_streaming_openai_has_done) { auto em = make_emitter(ApiFormat::OPENAI_CHAT); em.emit_start(); em.emit_token("Hello"); @@ -1177,7 +1162,7 @@ static void test_emitter_streaming_openai_has_done() { TEST_ASSERT(finish_str.find("[DONE]") != std::string::npos); } -static void test_emitter_nonstreaming_accumulates() { +TEST_CASE(ServerUnitFixture, test_emitter_nonstreaming_accumulates) { // Non-streaming: tokens fed through emitter, accumulated_text() has all content. auto em = make_emitter(ApiFormat::OPENAI_CHAT); em.emit_token("Hello "); @@ -1188,7 +1173,7 @@ static void test_emitter_nonstreaming_accumulates() { TEST_ASSERT(em.accumulated_text().find("world") != std::string::npos); } -static void test_emitter_anthropic_thinking_blocks() { +TEST_CASE(ServerUnitFixture, test_emitter_anthropic_thinking_blocks) { auto em = make_emitter(ApiFormat::ANTHROPIC); auto start = em.emit_start(); std::string start_str = concat(start); @@ -1218,7 +1203,7 @@ static SseEmitter make_emitter_with_stops(ApiFormat fmt, json::array(), nullptr, stops); } -static void test_stop_sequence_basic() { +TEST_CASE(ServerUnitFixture, test_stop_sequence_basic) { // Stop sequence should truncate content at the match point. auto em = make_emitter_with_stops(ApiFormat::OPENAI_CHAT,{"STOP"}); em.emit_token("Hello "); @@ -1234,7 +1219,7 @@ static void test_stop_sequence_basic() { TEST_ASSERT(em.accumulated_text().find("more") == std::string::npos); } -static void test_stop_sequence_mid_token() { +TEST_CASE(ServerUnitFixture, test_stop_sequence_mid_token) { // Stop sequence may span multiple tokens due to holdback buffering. auto em = make_emitter_with_stops(ApiFormat::OPENAI_CHAT,{"END"}); em.emit_token("Go "); @@ -1248,7 +1233,7 @@ static void test_stop_sequence_mid_token() { TEST_ASSERT(em.accumulated_text().find("now") == std::string::npos); } -static void test_stop_sequence_multiple() { +TEST_CASE(ServerUnitFixture, test_stop_sequence_multiple) { // Multiple stop sequences — earliest match wins. auto em = make_emitter_with_stops(ApiFormat::OPENAI_CHAT,{"AAA", "BB"}); em.emit_token("xBBy"); @@ -1258,7 +1243,7 @@ static void test_stop_sequence_multiple() { TEST_ASSERT(em.accumulated_text() == "x"); } -static void test_stop_sequence_no_match() { +TEST_CASE(ServerUnitFixture, test_stop_sequence_no_match) { // No stop sequence hit — normal operation. auto em = make_emitter_with_stops(ApiFormat::OPENAI_CHAT,{"NOMATCH"}); em.emit_token("Hello world this is a long text"); @@ -1268,7 +1253,7 @@ static void test_stop_sequence_no_match() { TEST_ASSERT(em.accumulated_text().find("Hello") != std::string::npos); } -static void test_stop_sequence_empty_list() { +TEST_CASE(ServerUnitFixture, test_stop_sequence_empty_list) { // Empty stop list — no effect. auto em = make_emitter_with_stops(ApiFormat::OPENAI_CHAT,{}); em.emit_token("Hello STOP world"); @@ -1278,7 +1263,7 @@ static void test_stop_sequence_empty_list() { TEST_ASSERT(em.accumulated_text().find("STOP") != std::string::npos); } -static void test_stop_sequence_finish_reason() { +TEST_CASE(ServerUnitFixture, test_stop_sequence_finish_reason) { // finish_reason should be "stop" when stop sequence hit. auto em = make_emitter_with_stops(ApiFormat::OPENAI_CHAT,{"END"}); em.emit_token("content END more"); @@ -1288,7 +1273,7 @@ static void test_stop_sequence_finish_reason() { TEST_ASSERT(em.finish_reason() == "stop"); } -static void test_stop_sequence_streaming_output() { +TEST_CASE(ServerUnitFixture, test_stop_sequence_streaming_output) { // Streaming: verify the [DONE] is still emitted after stop. auto em = make_emitter_with_stops(ApiFormat::OPENAI_CHAT,{"HALT"}); auto start = em.emit_start(); @@ -1301,7 +1286,7 @@ static void test_stop_sequence_streaming_output() { TEST_ASSERT(all.find("\"finish_reason\":\"stop\"") != std::string::npos); } -static void test_stop_sequence_anthropic_format() { +TEST_CASE(ServerUnitFixture, test_stop_sequence_anthropic_format) { // Anthropic format should emit end_turn stop_reason. auto em = make_emitter_with_stops(ApiFormat::ANTHROPIC, {"DONE"}); em.emit_start(); @@ -1314,7 +1299,7 @@ static void test_stop_sequence_anthropic_format() { TEST_ASSERT(all.find("message_stop") != std::string::npos); } -static void test_stop_sequence_in_reasoning_mode() { +TEST_CASE(ServerUnitFixture, test_stop_sequence_in_reasoning_mode) { // Stop sequence in reasoning mode should still stop. Model opens // first to enter REASONING. auto em = make_emitter_with_stops(ApiFormat::OPENAI_CHAT, {"CUTOFF"}); @@ -1327,7 +1312,7 @@ static void test_stop_sequence_in_reasoning_mode() { TEST_ASSERT(em.reasoning_text().find("CUTOFF") == std::string::npos); } -static void test_stop_sequence_holdback_extends() { +TEST_CASE(ServerUnitFixture, test_stop_sequence_holdback_extends) { // With a long stop sequence, holdback buffer should extend to prevent // emitting text that's part of a stop sequence. auto em = make_emitter_with_stops(ApiFormat::OPENAI_CHAT, @@ -1350,14 +1335,14 @@ static void test_stop_sequence_holdback_extends() { // Prefix cache hash tests (model-free) // ═══════════════════════════════════════════════════════════════════════ -static void test_hash_prefix_deterministic() { +TEST_CASE(ServerUnitFixture, test_hash_prefix_deterministic) { std::vector ids = {100, 200, 300, 400, 500}; auto h1 = hash_prefix(ids.data(), (int)ids.size()); auto h2 = hash_prefix(ids.data(), (int)ids.size()); TEST_ASSERT(h1 == h2); } -static void test_hash_prefix_different_inputs() { +TEST_CASE(ServerUnitFixture, test_hash_prefix_different_inputs) { std::vector ids1 = {100, 200, 300}; std::vector ids2 = {100, 200, 301}; auto h1 = hash_prefix(ids1.data(), (int)ids1.size()); @@ -1365,7 +1350,7 @@ static void test_hash_prefix_different_inputs() { TEST_ASSERT(h1 != h2); } -static void test_hash_prefix_different_lengths() { +TEST_CASE(ServerUnitFixture, test_hash_prefix_different_lengths) { std::vector ids1 = {100, 200, 300}; std::vector ids2 = {100, 200, 300, 400}; auto h1 = hash_prefix(ids1.data(), (int)ids1.size()); @@ -1373,13 +1358,13 @@ static void test_hash_prefix_different_lengths() { TEST_ASSERT(h1 != h2); } -static void test_hash_prefix_empty() { +TEST_CASE(ServerUnitFixture, test_hash_prefix_empty) { auto h = hash_prefix(nullptr, 0); // Should not crash, just return a hash of empty input TEST_ASSERT(h.size() == 16); } -static void test_find_boundaries_empty() { +TEST_CASE(ServerUnitFixture, test_find_boundaries_empty) { ChatMarkers markers; markers.family = "qwen"; std::vector ids; @@ -1389,30 +1374,30 @@ static void test_find_boundaries_empty() { // ── Prefix-aware eviction policy (model-free) ─────────────────────────── -static void test_evict_empty_is_zero() { +TEST_CASE(ServerUnitFixture, test_evict_empty_is_zero) { std::vector> ids; TEST_ASSERT(select_inline_evict_victim(ids) == 0); } -static void test_evict_single_is_zero() { +TEST_CASE(ServerUnitFixture, test_evict_single_is_zero) { std::vector> ids = {{1, 2, 3}}; TEST_ASSERT(select_inline_evict_victim(ids) == 0); } -static void test_evict_chain_keeps_ancestors() { +TEST_CASE(ServerUnitFixture, test_evict_chain_keeps_ancestors) { // Oldest-first chain: [s] < [s,a] < [s,a,b]. Only the longest is a leaf, so // the short shared ancestors are kept and the victim is the deepest entry. std::vector> ids = {{9}, {9, 1}, {9, 1, 2}}; TEST_ASSERT(select_inline_evict_victim(ids) == 2); } -static void test_evict_unrelated_falls_back_to_lru() { +TEST_CASE(ServerUnitFixture, test_evict_unrelated_falls_back_to_lru) { // No prefix relation: all are leaves, so evict the oldest (index 0). std::vector> ids = {{1, 1}, {2, 2}, {3, 3}}; TEST_ASSERT(select_inline_evict_victim(ids) == 0); } -static void test_evict_branch_spares_shared_root() { +TEST_CASE(ServerUnitFixture, test_evict_branch_spares_shared_root) { // [s] is an ancestor of both branches, so it is never the victim; the oldest // leaf ([s,a] at index 1) is evicted instead. std::vector> ids = {{9}, {9, 1}, {9, 2}}; @@ -1425,7 +1410,7 @@ static void test_evict_branch_spares_shared_root() { // PFlash config tests (model-free) // ═══════════════════════════════════════════════════════════════════════ -static void test_pflash_config_defaults() { +TEST_CASE(ServerUnitFixture, test_pflash_config_defaults) { ServerConfig cfg; TEST_ASSERT(cfg.pflash_mode == ServerConfig::PflashMode::OFF); TEST_ASSERT(cfg.pflash_threshold == 32000); @@ -1435,7 +1420,7 @@ static void test_pflash_config_defaults() { TEST_ASSERT(cfg.draft_residency == DraftResidencyPolicy::Auto); } -static void test_pflash_config_modes() { +TEST_CASE(ServerUnitFixture, test_pflash_config_modes) { ServerConfig cfg; cfg.pflash_mode = ServerConfig::PflashMode::AUTO; TEST_ASSERT(cfg.pflash_mode != ServerConfig::PflashMode::OFF); @@ -1445,7 +1430,7 @@ static void test_pflash_config_modes() { TEST_ASSERT(cfg.pflash_mode != ServerConfig::PflashMode::AUTO); } -static void test_pflash_compress_request_struct() { +TEST_CASE(ServerUnitFixture, test_pflash_compress_request_struct) { ModelBackend::CompressRequest req; req.input_ids = {1, 2, 3, 4, 5}; req.keep_ratio = 0.05f; @@ -1458,13 +1443,13 @@ static void test_pflash_compress_request_struct() { TEST_ASSERT(req.skip_park); } -static void test_pflash_compress_result_defaults() { +TEST_CASE(ServerUnitFixture, test_pflash_compress_result_defaults) { ModelBackend::CompressResult result; TEST_ASSERT(!result.ok); TEST_ASSERT(result.compressed_ids.empty()); } -static void test_pflash_threshold_auto_mode() { +TEST_CASE(ServerUnitFixture, test_pflash_threshold_auto_mode) { // Simulate the threshold check logic from http_server.cpp ServerConfig cfg; cfg.pflash_mode = ServerConfig::PflashMode::AUTO; @@ -1483,7 +1468,7 @@ static void test_pflash_threshold_auto_mode() { TEST_ASSERT(should); } -static void test_pflash_threshold_always_mode() { +TEST_CASE(ServerUnitFixture, test_pflash_threshold_always_mode) { ServerConfig cfg; cfg.pflash_mode = ServerConfig::PflashMode::ALWAYS; @@ -1494,7 +1479,7 @@ static void test_pflash_threshold_always_mode() { TEST_ASSERT(should); } -static void test_pflash_config_upstream_defaults() { +TEST_CASE(ServerUnitFixture, test_pflash_config_upstream_defaults) { ServerConfig cfg; TEST_ASSERT(cfg.pflash_upstream_base.empty()); TEST_ASSERT(cfg.pflash_upstream_key.empty()); @@ -1502,7 +1487,7 @@ static void test_pflash_config_upstream_defaults() { TEST_ASSERT(cfg.pflash_curve.empty()); } -static void test_pflash_curve_interpolation() { +TEST_CASE(ServerUnitFixture, test_pflash_curve_interpolation) { ServerConfig cfg; cfg.pflash_curve = {{10000, 0.50f}, {40000, 0.20f}, {100000, 0.10f}}; @@ -1534,7 +1519,7 @@ static void test_pflash_curve_interpolation() { TEST_ASSERT(keep(200000) == 0.10f); } -static void test_pflash_curve_empty_uses_flat() { +TEST_CASE(ServerUnitFixture, test_pflash_curve_empty_uses_flat) { ServerConfig cfg; cfg.pflash_keep_ratio = 0.05f; // With empty curve, should fall back to flat ratio @@ -1542,7 +1527,7 @@ static void test_pflash_curve_empty_uses_flat() { TEST_ASSERT(cfg.pflash_keep_ratio == 0.05f); } -static void test_pflash_upstream_proxy_config() { +TEST_CASE(ServerUnitFixture, test_pflash_upstream_proxy_config) { ServerConfig cfg; cfg.pflash_upstream_base = "http://localhost:8080/v1"; cfg.pflash_upstream_key = "test-key"; @@ -1553,7 +1538,7 @@ static void test_pflash_upstream_proxy_config() { TEST_ASSERT(cfg.pflash_upstream_model == "test-model"); } -static void test_pflash_raw_body_preserved() { +TEST_CASE(ServerUnitFixture, test_pflash_raw_body_preserved) { ParsedRequest req; req.raw_body = {{"model", "test"}, {"messages", json::array()}, {"temperature", 0.7}}; @@ -1562,7 +1547,7 @@ static void test_pflash_raw_body_preserved() { TEST_ASSERT(req.raw_body["temperature"].get() > 0.6f); } -static void test_pflash_placement_same_backend_local() { +TEST_CASE(ServerUnitFixture, test_pflash_placement_same_backend_local) { DevicePlacement target; target.backend = compiled_placement_backend(); target.gpu = 0; @@ -1581,7 +1566,7 @@ static void test_pflash_placement_same_backend_local() { TEST_ASSERT(!placement.remote.enabled()); } -static void test_pflash_placement_mixed_backend_remote() { +TEST_CASE(ServerUnitFixture, test_pflash_placement_mixed_backend_remote) { DevicePlacement target; target.backend = PlacementBackend::Cuda; target.gpu = 0; @@ -1602,7 +1587,7 @@ static void test_pflash_placement_mixed_backend_remote() { TEST_ASSERT(placement.remote.work_dir == "/tmp/pflash-ipc"); } -static void test_pflash_placement_auto_draft_follows_target() { +TEST_CASE(ServerUnitFixture, test_pflash_placement_auto_draft_follows_target) { DevicePlacement target; target.backend = PlacementBackend::Hip; target.gpu = 0; @@ -1620,7 +1605,7 @@ static void test_pflash_placement_auto_draft_follows_target() { TEST_ASSERT(!placement.remote_drafter); } -static void test_pflash_placement_disabled_never_remote() { +TEST_CASE(ServerUnitFixture, test_pflash_placement_disabled_never_remote) { DevicePlacement target; target.backend = PlacementBackend::Cuda; DevicePlacement drafter; @@ -1636,7 +1621,7 @@ static void test_pflash_placement_disabled_never_remote() { TEST_ASSERT(!placement.remote.enabled()); } -static void test_pflash_placement_usage_gate() { +TEST_CASE(ServerUnitFixture, test_pflash_placement_usage_gate) { TEST_ASSERT(!pflash_drafter_placement_used( /*pflash_enabled=*/false, /*has_decode_draft=*/false)); TEST_ASSERT(pflash_drafter_placement_used( @@ -1647,7 +1632,7 @@ static void test_pflash_placement_usage_gate() { /*pflash_enabled=*/true, /*has_decode_draft=*/true)); } -static void test_draft_residency_parse() { +TEST_CASE(ServerUnitFixture, test_draft_residency_parse) { DraftResidencyPolicy policy = DraftResidencyPolicy::Auto; TEST_ASSERT(parse_draft_residency_policy("auto", policy)); TEST_ASSERT(policy == DraftResidencyPolicy::Auto); @@ -1660,7 +1645,7 @@ static void test_draft_residency_parse() { TEST_ASSERT(!parse_draft_residency_policy("request", policy)); } -static void test_draft_residency_pflash_auto() { +TEST_CASE(ServerUnitFixture, test_draft_residency_pflash_auto) { auto action = resolve_draft_residency_action( DraftResidencyPolicy::Auto, DraftResidencyContext{ @@ -1680,7 +1665,7 @@ static void test_draft_residency_pflash_auto() { TEST_ASSERT(action == DraftResidencyAction::ReleaseAfterUse); } -static void test_draft_residency_dflash_auto_and_request_scoped() { +TEST_CASE(ServerUnitFixture, test_draft_residency_dflash_auto_and_request_scoped) { auto action = resolve_draft_residency_action( DraftResidencyPolicy::Auto, DraftResidencyContext{ @@ -1733,7 +1718,7 @@ static const char MINI_JINJA_TEMPLATE[] = "<|assistant|>" "{%- endif -%}"; -static void test_deepseek4_render_system_only_gen_prompt() { +TEST_CASE(ServerUnitFixture, test_deepseek4_render_system_only_gen_prompt) { std::vector msgs = { {"system", "sys only", ""}, }; @@ -1747,7 +1732,7 @@ static void test_deepseek4_render_system_only_gen_prompt() { TEST_ASSERT(out == expected); } -static void test_deepseek4_render_empty_chat_gen_prompt() { +TEST_CASE(ServerUnitFixture, test_deepseek4_render_empty_chat_gen_prompt) { std::vector msgs; const std::string out = render_chat_template( msgs, ChatFormat::DEEPSEEK4, @@ -1759,7 +1744,7 @@ static void test_deepseek4_render_empty_chat_gen_prompt() { TEST_ASSERT(out == expected); } -static void test_jinja_render_basic() { +TEST_CASE(ServerUnitFixture, test_jinja_render_basic) { std::vector msgs = { {"system", "you are helpful", ""}, {"user", "hi", ""}, @@ -1774,7 +1759,7 @@ static void test_jinja_render_basic() { TEST_ASSERT(out.find("<|assistant|>") != std::string::npos); } -static void test_jinja_render_no_gen_prompt() { +TEST_CASE(ServerUnitFixture, test_jinja_render_no_gen_prompt) { std::vector msgs = {{"user", "ping", ""}}; std::string out = render_chat_template_jinja( MINI_JINJA_TEMPLATE, msgs, "", "", @@ -1783,7 +1768,7 @@ static void test_jinja_render_no_gen_prompt() { TEST_ASSERT(out.find("<|assistant|>") == std::string::npos); } -static void test_jinja_render_tools_injected() { +TEST_CASE(ServerUnitFixture, test_jinja_render_tools_injected) { // Template references `tools` to confirm it was passed in. static const char TPL[] = "{%- if tools -%}TOOLS_PRESENT:{{ tools[0].name }}{%- endif -%}" @@ -1795,7 +1780,7 @@ static void test_jinja_render_tools_injected() { TEST_ASSERT(out.find("TOOLS_PRESENT:my_tool") != std::string::npos); } -static void test_jinja_render_empty_tools_skipped() { +TEST_CASE(ServerUnitFixture, test_jinja_render_empty_tools_skipped) { // tools_json == "[]" must NOT define `tools` in the template context. static const char TPL[] = "{%- if tools -%}TOOLS_PRESENT{%- else -%}NO_TOOLS{%- endif -%}"; @@ -1806,7 +1791,7 @@ static void test_jinja_render_empty_tools_skipped() { TEST_ASSERT(out.find("TOOLS_PRESENT") == std::string::npos); } -static void test_jinja_render_bos_eos_threaded() { +TEST_CASE(ServerUnitFixture, test_jinja_render_bos_eos_threaded) { // {{ bos_token }} and {{ eos_token }} must reach the template. static const char TPL[] = "{{ bos_token }}HI{{ eos_token }}"; std::vector msgs; @@ -1815,7 +1800,7 @@ static void test_jinja_render_bos_eos_threaded() { TEST_ASSERT(out == "HI"); } -static void test_jinja_render_empty_template_throws() { +TEST_CASE(ServerUnitFixture, test_jinja_render_empty_template_throws) { std::vector msgs = {{"user", "x", ""}}; bool threw = false; try { @@ -1826,7 +1811,7 @@ static void test_jinja_render_empty_template_throws() { TEST_ASSERT(threw); } -static void test_jinja_render_bad_tools_json_throws() { +TEST_CASE(ServerUnitFixture, test_jinja_render_bad_tools_json_throws) { static const char TPL[] = "{%- for m in messages -%}{{ m.role }}{%- endfor -%}"; std::vector msgs = {{"user", "x", ""}}; bool threw = false; @@ -1839,7 +1824,7 @@ static void test_jinja_render_bad_tools_json_throws() { TEST_ASSERT(threw); } -static void test_normalize_responses_tool_followup_messages() { +TEST_CASE(ServerUnitFixture, test_normalize_responses_tool_followup_messages) { ToolMemory tool_memory; const std::string call_id = "call_exec_001"; const std::string raw_tool_call = @@ -1897,7 +1882,7 @@ static void test_normalize_responses_tool_followup_messages() { // Placement config tests // ═══════════════════════════════════════════════════════════════════════ -static void test_parse_target_device_list_same_backend() { +TEST_CASE(ServerUnitFixture, test_parse_target_device_list_same_backend) { DevicePlacement placement; TEST_ASSERT(parse_placement_device_list("cuda:0,cuda:1", placement)); TEST_ASSERT(placement.backend == PlacementBackend::Cuda); @@ -1913,7 +1898,7 @@ static void test_parse_target_device_list_same_backend() { TEST_ASSERT(placement.layer_split_weights.empty()); } -static void test_parse_target_device_list_mixed_backend() { +TEST_CASE(ServerUnitFixture, test_parse_target_device_list_mixed_backend) { DevicePlacement placement; TEST_ASSERT(parse_placement_device_list("cuda:0,hip:1", placement)); TEST_ASSERT(placement.backend == PlacementBackend::Cuda); @@ -1930,7 +1915,7 @@ static void test_parse_target_device_list_mixed_backend() { TEST_ASSERT(placement.layer_split_gpus[1] == 1); } -static void test_parse_target_device_list_mixed_backend_multi_remote() { +TEST_CASE(ServerUnitFixture, test_parse_target_device_list_mixed_backend_multi_remote) { DevicePlacement placement; TEST_ASSERT(parse_placement_device_list("cuda:0,hip:0,hip:1", placement)); TEST_ASSERT(placement.backend == PlacementBackend::Cuda); @@ -1947,7 +1932,7 @@ static void test_parse_target_device_list_mixed_backend_multi_remote() { TEST_ASSERT(placement.layer_split_gpus[2] == 1); } -static void test_parse_target_device_list_single_gpu_is_not_layer_split() { +TEST_CASE(ServerUnitFixture, test_parse_target_device_list_single_gpu_is_not_layer_split) { DevicePlacement placement; TEST_ASSERT(parse_placement_device_list("hip:2", placement)); TEST_ASSERT(placement.backend == PlacementBackend::Hip); @@ -1958,7 +1943,7 @@ static void test_parse_target_device_list_single_gpu_is_not_layer_split() { TEST_ASSERT(placement.layer_split_gpus.empty()); } -static void test_validate_layer_split_weights_shape() { +TEST_CASE(ServerUnitFixture, test_validate_layer_split_weights_shape) { DevicePlacement placement; TEST_ASSERT(parse_placement_device_list("cuda:0,cuda:1", placement)); @@ -1972,7 +1957,7 @@ static void test_validate_layer_split_weights_shape() { TEST_ASSERT(validate_device_placement(placement, -1).empty()); } -static void test_target_shard_plan_same_backend_split() { +TEST_CASE(ServerUnitFixture, test_target_shard_plan_same_backend_split) { DevicePlacement placement; TEST_ASSERT(parse_placement_device_list("cuda:0,cuda:1,cuda:2", placement)); @@ -1983,7 +1968,7 @@ static void test_target_shard_plan_same_backend_split() { TEST_ASSERT(plan.remote_backend == PlacementBackend::Cuda); } -static void test_target_shard_plan_mixed_backend_split() { +TEST_CASE(ServerUnitFixture, test_target_shard_plan_mixed_backend_split) { DevicePlacement placement; TEST_ASSERT(parse_placement_device_list("cuda:0,cuda:1,hip:0,hip:1", placement)); @@ -1994,7 +1979,7 @@ static void test_target_shard_plan_mixed_backend_split() { TEST_ASSERT(plan.remote_backend == PlacementBackend::Hip); } -static void test_target_shard_plan_rejects_bad_local_backend() { +TEST_CASE(ServerUnitFixture, test_target_shard_plan_rejects_bad_local_backend) { DevicePlacement placement; TEST_ASSERT(parse_placement_device_list("cuda:0,hip:0", placement)); @@ -2008,7 +1993,7 @@ static bool kvflash_test_sync_identity(KvFlashPager & pager, int committed) { pager, committed, pager.pool_tokens(), "test-target-split"); } -static void test_kvflash_pager_identity_sync_contract() { +TEST_CASE(ServerUnitFixture, test_kvflash_pager_identity_sync_contract) { KvFlashConfig cfg; cfg.pool_tokens = 512; @@ -2034,7 +2019,7 @@ static void test_kvflash_pager_identity_sync_contract() { TEST_ASSERT(local.slot_of(127) == 127); } -static void test_layer_split_kvflash_history_contract() { +TEST_CASE(ServerUnitFixture, test_layer_split_kvflash_history_contract) { std::vector history; layer_split_kvflash_sync_history(history, {1, 2, 3}, 0); TEST_ASSERT((history == std::vector{1, 2, 3})); @@ -2066,7 +2051,7 @@ static void test_layer_split_kvflash_history_contract() { TEST_ASSERT((history == std::vector{0, 0, 0})); } -static void test_backend_precision_cuda_sm_policy() { +TEST_CASE(ServerUnitFixture, test_backend_precision_cuda_sm_policy) { TEST_ASSERT(select_cuda_backend_precision_type_for_sm(90) == GGML_TYPE_BF16); TEST_ASSERT(select_cuda_backend_precision_type_for_sm(80) == GGML_TYPE_BF16); TEST_ASSERT(select_cuda_backend_precision_type_for_sm(75) == GGML_TYPE_F16); @@ -2077,7 +2062,7 @@ static void test_backend_precision_cuda_sm_policy() { TEST_ASSERT(select_cuda_backend_precision_type_for_sm(52) == GGML_TYPE_F32); } -static void test_backend_precision_hip_arch_policy() { +TEST_CASE(ServerUnitFixture, test_backend_precision_hip_arch_policy) { TEST_ASSERT(select_hip_activation_precision_type_for_arch("gfx90a") == GGML_TYPE_BF16); TEST_ASSERT(select_hip_activation_precision_type_for_arch("gfx942") == GGML_TYPE_BF16); TEST_ASSERT(select_hip_activation_precision_type_for_arch("gfx950") == GGML_TYPE_BF16); @@ -2089,7 +2074,7 @@ static void test_backend_precision_hip_arch_policy() { TEST_ASSERT(select_hip_activation_precision_type_for_arch("") == GGML_TYPE_F32); } -static void test_backend_precision_activation_type_combine() { +TEST_CASE(ServerUnitFixture, test_backend_precision_activation_type_combine) { TEST_ASSERT(combine_activation_precision_types(GGML_TYPE_BF16, GGML_TYPE_BF16) == GGML_TYPE_BF16); TEST_ASSERT(combine_activation_precision_types(GGML_TYPE_BF16, GGML_TYPE_F16) == GGML_TYPE_F16); TEST_ASSERT(combine_activation_precision_types(GGML_TYPE_F16, GGML_TYPE_BF16) == GGML_TYPE_F16); @@ -2209,7 +2194,7 @@ struct MockLayerSplitAdapter : LayerSplitAdapter { void shutdown() override { shutdown_calls++; } }; -static void test_layer_split_backend_inline_snapshot_and_restore_delta() { +TEST_CASE(ServerUnitFixture, test_layer_split_backend_inline_snapshot_and_restore_delta) { auto * raw = new MockLayerSplitAdapter(); LayerSplitBackend backend{std::unique_ptr(raw)}; @@ -2253,7 +2238,7 @@ static void test_layer_split_backend_inline_snapshot_and_restore_delta() { TEST_ASSERT(raw->dflash_last == 99); } -static void test_layer_split_backend_sampling_capability_gate() { +TEST_CASE(ServerUnitFixture, test_layer_split_backend_sampling_capability_gate) { { auto * raw = new MockLayerSplitAdapter(); LayerSplitBackend backend{std::unique_ptr(raw)}; @@ -2290,7 +2275,7 @@ static void test_layer_split_backend_sampling_capability_gate() { } } -static void test_layer_split_backend_chunks_prefill_by_adapter_limit() { +TEST_CASE(ServerUnitFixture, test_layer_split_backend_chunks_prefill_by_adapter_limit) { auto * raw = new MockLayerSplitAdapter(); raw->prefill_chunk = 3; LayerSplitBackend backend{std::unique_ptr(raw)}; @@ -2312,7 +2297,7 @@ static void test_layer_split_backend_chunks_prefill_by_adapter_limit() { TEST_ASSERT(raw->prefill_sizes[2] == 2); } -static void test_layer_split_compress_nopark_uses_default_drafter_path() { +TEST_CASE(ServerUnitFixture, test_layer_split_compress_nopark_uses_default_drafter_path) { const std::string ids_path = "/tmp/dflash_test_layer_split_compress_ids.bin"; unlink(ids_path.c_str()); TEST_ASSERT(write_int32_file(ids_path, {1, 2, 3, 4})); @@ -2332,7 +2317,7 @@ static void test_layer_split_compress_nopark_uses_default_drafter_path() { unlink(ids_path.c_str()); } -static void test_layer_split_compress_rejects_bad_keep_ratio() { +TEST_CASE(ServerUnitFixture, test_layer_split_compress_rejects_bad_keep_ratio) { const std::string ids_path = "/tmp/dflash_test_layer_split_compress_bad.bin"; unlink(ids_path.c_str()); TEST_ASSERT(write_int32_file(ids_path, {1, 2, 3, 4})); @@ -2348,7 +2333,7 @@ static void test_layer_split_compress_rejects_bad_keep_ratio() { unlink(ids_path.c_str()); } -static void test_layer_split_backend_shutdown_is_idempotent() { +TEST_CASE(ServerUnitFixture, test_layer_split_backend_shutdown_is_idempotent) { auto * raw = new MockLayerSplitAdapter(); LayerSplitBackend backend{std::unique_ptr(raw)}; backend.shutdown(); @@ -2356,7 +2341,7 @@ static void test_layer_split_backend_shutdown_is_idempotent() { TEST_ASSERT(raw->shutdown_calls == 1); } -static void test_layer_split_backend_capability_proxy() { +TEST_CASE(ServerUnitFixture, test_layer_split_backend_capability_proxy) { auto * raw = new MockLayerSplitAdapter(); LayerSplitBackend backend{std::unique_ptr(raw)}; @@ -2469,7 +2454,7 @@ static void rm_rf(const std::string & path) { rmdir(path.c_str()); } -static void test_disk_cache_config_defaults() { +TEST_CASE(ServerUnitFixture, test_disk_cache_config_defaults) { DiskCacheConfig cfg; TEST_ASSERT(cfg.cache_dir.empty()); TEST_ASSERT(cfg.budget_bytes == (size_t)4 * 1024 * 1024 * 1024); @@ -2478,7 +2463,7 @@ static void test_disk_cache_config_defaults() { TEST_ASSERT(cfg.cold_max_tokens == 10240); } -static void test_disk_cache_policy_parse() { +TEST_CASE(ServerUnitFixture, test_disk_cache_policy_parse) { DiskPrefixCachePolicy policy; TEST_ASSERT(parse_disk_prefix_cache_policy("off", policy)); TEST_ASSERT(policy.mode == DiskPrefixCacheMode::Off); @@ -2501,7 +2486,7 @@ static void test_disk_cache_policy_parse() { // BUG-A: apply_request_scope_override must preserve server-level compress flag. // A request-level scope override (e.g. "auto") must NOT clear compress=true // that was set by the server configuration. -static void test_scope_override_preserves_compress() { +TEST_CASE(ServerUnitFixture, test_scope_override_preserves_compress) { // Server policy: compress=true, mode=Full. DiskPrefixCachePolicy server; server.mode = DiskPrefixCacheMode::Full; @@ -2540,7 +2525,7 @@ static void test_scope_override_preserves_compress() { TEST_ASSERT(server4.mode == DiskPrefixCacheMode::Full); } -static void test_disk_cache_fixed_boundary() { +TEST_CASE(ServerUnitFixture, test_disk_cache_fixed_boundary) { DiskPrefixCachePolicy policy; TEST_ASSERT(parse_disk_prefix_cache_policy("1000", policy)); TEST_ASSERT(disk_prefix_cache_fixed_boundary(policy, 2000) == 1000); @@ -2549,7 +2534,7 @@ static void test_disk_cache_fixed_boundary() { TEST_ASSERT(disk_prefix_cache_fixed_boundary(policy, 2000, 1000) == 1000); } -static void test_disk_cache_auto_boundary_lcp() { +TEST_CASE(ServerUnitFixture, test_disk_cache_auto_boundary_lcp) { std::vector current{1, 2, 3, 4, 5, 9}; std::vector> recent{ {1, 2, 3, 4, 8}, @@ -2567,7 +2552,7 @@ static void test_disk_cache_auto_boundary_lcp() { current, recent, 2, safe_boundaries, 5) == 0); } -static void test_disk_cache_auto_window_limits_history() { +TEST_CASE(ServerUnitFixture, test_disk_cache_auto_window_limits_history) { std::vector current{1, 2, 3, 4, 5}; std::vector> recent{ {9}, @@ -2584,7 +2569,7 @@ static void test_disk_cache_auto_window_limits_history() { current, recent, 0, {}, 2) == 0); } -static void test_disk_cache_disabled_when_no_dir() { +TEST_CASE(ServerUnitFixture, test_disk_cache_disabled_when_no_dir) { MockBackend backend; DiskCacheConfig cfg; cfg.cache_dir = ""; @@ -2596,7 +2581,7 @@ static void test_disk_cache_disabled_when_no_dir() { TEST_ASSERT(!cache.save(0, ids)); } -static void test_disk_cache_init_creates_directory() { +TEST_CASE(ServerUnitFixture, test_disk_cache_init_creates_directory) { MockBackend backend; std::string dir = "/tmp/dflash_test_disk_cache_init"; rm_rf(dir); @@ -2614,13 +2599,13 @@ static void test_disk_cache_init_creates_directory() { rm_rf(dir); } -static void test_disk_cache_header_size() { +TEST_CASE(ServerUnitFixture, test_disk_cache_header_size) { // The header should be exactly 80 bytes. TEST_ASSERT(DISK_CACHE_HEADER_SIZE == 80); TEST_ASSERT(DISK_CACHE_VERSION == 1); } -static void test_disk_cache_header_round_trip() { +TEST_CASE(ServerUnitFixture, test_disk_cache_header_round_trip) { // Write and read a header to verify serialization. std::string path = "/tmp/dflash_test_header_rt.dkv"; unlink(path.c_str()); @@ -2685,7 +2670,7 @@ static void test_disk_cache_header_round_trip() { unlink(path.c_str()); } -static void test_disk_cache_continued_boundary() { +TEST_CASE(ServerUnitFixture, test_disk_cache_continued_boundary) { // Test maybe_store_continued logic: saves at interval boundaries. MockBackend backend; std::string dir = "/tmp/dflash_test_continued"; @@ -2715,7 +2700,7 @@ static void test_disk_cache_continued_boundary() { rm_rf(dir); } -static void test_disk_cache_continued_interval_logic() { +TEST_CASE(ServerUnitFixture, test_disk_cache_continued_interval_logic) { // Verify the continued boundary math independently. // Target = (cur_pos / interval) * interval // Only fires when target > last_store_pos AND target >= min_tokens. @@ -2747,7 +2732,7 @@ static void test_disk_cache_continued_interval_logic() { (void)min_tokens; } -static void test_disk_cache_cold_prefix_short_prompt() { +TEST_CASE(ServerUnitFixture, test_disk_cache_cold_prefix_short_prompt) { // Cold prefix should not trigger for short prompts. MockBackend backend; std::string dir = "/tmp/dflash_test_cold_short"; @@ -2768,7 +2753,7 @@ static void test_disk_cache_cold_prefix_short_prompt() { rm_rf(dir); } -static void test_disk_cache_cold_prefix_no_boundaries() { +TEST_CASE(ServerUnitFixture, test_disk_cache_cold_prefix_no_boundaries) { // Cold prefix should not trigger if no boundaries provided. MockBackend backend; std::string dir = "/tmp/dflash_test_cold_nobound"; @@ -2788,7 +2773,7 @@ static void test_disk_cache_cold_prefix_no_boundaries() { rm_rf(dir); } -static void test_disk_cache_cold_prefix_finds_boundary() { +TEST_CASE(ServerUnitFixture, test_disk_cache_cold_prefix_finds_boundary) { // Cold prefix should find the last boundary <= cold_max_tokens. MockBackend backend; std::string dir = "/tmp/dflash_test_cold_finds"; @@ -2813,7 +2798,7 @@ static void test_disk_cache_cold_prefix_finds_boundary() { rm_rf(dir); } -static void test_disk_cache_budget_enforcement_scoring() { +TEST_CASE(ServerUnitFixture, test_disk_cache_budget_enforcement_scoring) { // Test that eviction scoring prefers lower-value entries. // score = (hits+1) * token_count / file_size // Entry with fewer tokens + fewer hits should have lower score. @@ -2838,7 +2823,7 @@ static void test_disk_cache_budget_enforcement_scoring() { TEST_ASSERT(score_b_ancient > score_a); } -static void test_disk_cache_lookup_miss_no_layout() { +TEST_CASE(ServerUnitFixture, test_disk_cache_lookup_miss_no_layout) { // Lookup with no layout known should return false. MockBackend backend; std::string dir = "/tmp/dflash_test_lookup_miss"; @@ -2855,7 +2840,7 @@ static void test_disk_cache_lookup_miss_no_layout() { rm_rf(dir); } -static void test_disk_cache_save_below_min_tokens() { +TEST_CASE(ServerUnitFixture, test_disk_cache_save_below_min_tokens) { // Save with fewer tokens than min_tokens should be rejected. MockBackend backend; std::string dir = "/tmp/dflash_test_save_below"; @@ -2911,7 +2896,7 @@ static std::array read_layout_id_from_cache_dir(const std::string & return id; } -static void test_disk_identity_salt_changes_layout_id() { +TEST_CASE(ServerUnitFixture, test_disk_identity_salt_changes_layout_id) { MockBackendWithLayout backend; std::vector prompt; for (int i = 0; i < 10; ++i) prompt.push_back(i + 1); @@ -2971,7 +2956,7 @@ static void test_disk_identity_salt_changes_layout_id() { rm_rf(dir_a2); } -static void test_disk_identity_salt_zero_is_backcompat() { +TEST_CASE(ServerUnitFixture, test_disk_identity_salt_zero_is_backcompat) { // Explicit all-zero salt must produce the same layout_id as no salt call // (default-constructed identity_salt_ is already all-zero). MockBackendWithLayout backend; @@ -3009,7 +2994,7 @@ static void test_disk_identity_salt_zero_is_backcompat() { rm_rf(dir2); } -static void test_backend_ipc_rejects_file_work_dir() { +TEST_CASE(ServerUnitFixture, test_backend_ipc_rejects_file_work_dir) { const std::string file_path = "/tmp/dflash_test_backend_ipc_work_dir_file"; unlink(file_path.c_str()); int fd = open(file_path.c_str(), O_CREAT | O_TRUNC | O_WRONLY, 0600); @@ -3031,7 +3016,7 @@ static void test_backend_ipc_rejects_file_work_dir() { unlink(file_path.c_str()); } -static void test_backend_ipc_payload_pipe_round_trip() { +TEST_CASE(ServerUnitFixture, test_backend_ipc_payload_pipe_round_trip) { int payload_pipe[2] = {-1, -1}; int status_pipe[2] = {-1, -1}; TEST_ASSERT(pipe(payload_pipe) == 0); @@ -3070,7 +3055,7 @@ static void test_backend_ipc_payload_pipe_round_trip() { close(status_pipe[0]); } -static void test_backend_ipc_payload_transport_parse() { +TEST_CASE(ServerUnitFixture, test_backend_ipc_payload_transport_parse) { BackendIpcMode mode = BackendIpcMode::DFlashDraft; TEST_ASSERT(parse_backend_ipc_mode("dflash-draft", mode)); TEST_ASSERT(mode == BackendIpcMode::DFlashDraft); @@ -3095,7 +3080,7 @@ static void test_backend_ipc_payload_transport_parse() { "stream") == 0); } -static void test_backend_ipc_payload_bounds() { +TEST_CASE(ServerUnitFixture, test_backend_ipc_payload_bounds) { size_t out = 0; TEST_ASSERT(backend_ipc_checked_add_size(4, 8, out)); TEST_ASSERT(out == 12); @@ -3108,7 +3093,7 @@ static void test_backend_ipc_payload_bounds() { std::numeric_limits::max(), 1, 16)); } -static void test_backend_ipc_shared_payload_map_sizing() { +TEST_CASE(ServerUnitFixture, test_backend_ipc_shared_payload_map_sizing) { size_t map_bytes = 0; TEST_ASSERT(backend_ipc_shared_payload_map_bytes(1024, map_bytes)); TEST_ASSERT(map_bytes == 1024 + backend_ipc_shared_payload_header_bytes()); @@ -3123,7 +3108,7 @@ static void test_backend_ipc_shared_payload_map_sizing() { std::numeric_limits::max(), map_bytes)); } -static void test_backend_ipc_shared_payload_segment_contract() { +TEST_CASE(ServerUnitFixture, test_backend_ipc_shared_payload_segment_contract) { const BackendIpcPayloadSegment a{reinterpret_cast(1), 16}; const BackendIpcPayloadSegment b{reinterpret_cast(2), 32}; const BackendIpcPayloadSegment segments[] = {a, b}; @@ -3136,13 +3121,13 @@ static void test_backend_ipc_shared_payload_segment_contract() { TEST_ASSERT(!backend_ipc_payload_in_bounds(0, total + 1, 48)); } -static void test_moe_hybrid_expert_compute_batch_default() { +TEST_CASE(ServerUnitFixture, test_moe_hybrid_expert_compute_batch_default) { dflash_unsetenv("DFLASH_MOE_EXPERT_COMPUTE_BATCH"); dflash_unsetenv("DFLASH_MOE_EXPERT_COMPUTE_BATCH_MAX"); TEST_ASSERT(moe_hybrid_expert_compute_batch_limit() == 32); } -static void test_moe_hybrid_expert_compute_ipc_mode_batch_limit() { +TEST_CASE(ServerUnitFixture, test_moe_hybrid_expert_compute_ipc_mode_batch_limit) { dflash_unsetenv("DFLASH_MOE_EXPERT_COMPUTE_IPC_MODE"); dflash_unsetenv("DFLASH_MOE_EXPERT_COMPUTE_IPC_BATCH_CAPACITY"); TEST_ASSERT(moe_hybrid_expert_compute_ipc_batch_limit(2048) == 1024); @@ -3161,7 +3146,7 @@ static void test_moe_hybrid_expert_compute_ipc_mode_batch_limit() { dflash_unsetenv("DFLASH_MOE_EXPERT_COMPUTE_IPC_BATCH_CAPACITY"); } -static void test_moe_hybrid_prefill_hot_sub_batch_limit() { +TEST_CASE(ServerUnitFixture, test_moe_hybrid_prefill_hot_sub_batch_limit) { dflash_unsetenv("DFLASH_MOE_PREFILL_HOT_SUB_BATCH"); TEST_ASSERT(moe_hybrid_prefill_hot_sub_batch_limit() == 4); @@ -3181,7 +3166,7 @@ static void test_moe_hybrid_prefill_hot_sub_batch_limit() { // Sampler tests (model-independent, CPU-only) // ═══════════════════════════════════════════════════════════════════════ -static void test_sampler_cfg_defaults() { +TEST_CASE(ServerUnitFixture, test_sampler_cfg_defaults) { SamplerCfg cfg; TEST_ASSERT(cfg.temp == 0.0f); TEST_ASSERT(cfg.top_p == 1.0f); @@ -3193,7 +3178,7 @@ static void test_sampler_cfg_defaults() { TEST_ASSERT(cfg.pres_pen == 0.0f); } -static void test_sampler_greedy_argmax() { +TEST_CASE(ServerUnitFixture, test_sampler_greedy_argmax) { // With temp=0 logic, caller uses argmax. But sample_logits with very // low temp should still pick the highest logit token reliably. float logits[] = {1.0f, 5.0f, 2.0f, 3.0f, 0.5f}; @@ -3206,7 +3191,7 @@ static void test_sampler_greedy_argmax() { TEST_ASSERT(tok == 1); // token 1 has logit 5.0 (highest) } -static void test_sampler_temperature_affects_distribution() { +TEST_CASE(ServerUnitFixture, test_sampler_temperature_affects_distribution) { // High temperature should spread probability; verify by sampling many // times and checking that non-top tokens appear. float logits[] = {0.0f, 1.0f, 0.0f, 0.0f, 0.0f}; @@ -3226,7 +3211,7 @@ static void test_sampler_temperature_affects_distribution() { TEST_ASSERT(counts[1] > 100); // token 1 still most likely } -static void test_sampler_top_p_truncation() { +TEST_CASE(ServerUnitFixture, test_sampler_top_p_truncation) { // With very low top_p, only the top token(s) should be selected. float logits[] = {0.0f, 10.0f, 0.0f, 0.0f, 0.0f}; SamplerCfg cfg; @@ -3241,7 +3226,7 @@ static void test_sampler_top_p_truncation() { } } -static void test_sampler_top_k_truncation() { +TEST_CASE(ServerUnitFixture, test_sampler_top_k_truncation) { // top_k=2 should limit candidates to the top 2. float logits[] = {1.0f, 5.0f, 3.0f, 0.0f, 0.0f}; SamplerCfg cfg; @@ -3263,7 +3248,7 @@ static void test_sampler_top_k_truncation() { TEST_ASSERT(counts[2] > 0); } -static void test_sampler_repetition_penalty() { +TEST_CASE(ServerUnitFixture, test_sampler_repetition_penalty) { // Multiplicative rep_pen should reduce probability of repeated tokens. float logits[] = {3.0f, 3.0f, 3.0f, 3.0f}; SamplerCfg cfg; @@ -3281,7 +3266,7 @@ static void test_sampler_repetition_penalty() { TEST_ASSERT(counts[2] + counts[3] > counts[0] + counts[1]); } -static void test_sampler_frequency_penalty() { +TEST_CASE(ServerUnitFixture, test_sampler_frequency_penalty) { // freq_pen subtracts freq_pen * count(token) from logits. // Token 0 appears 5 times → logit reduced by 5*1.0 = 5.0 float logits[] = {5.0f, 5.0f, 5.0f, 5.0f}; @@ -3303,7 +3288,7 @@ static void test_sampler_frequency_penalty() { TEST_ASSERT(counts[0] < counts[1]); } -static void test_sampler_presence_penalty() { +TEST_CASE(ServerUnitFixture, test_sampler_presence_penalty) { // pres_pen subtracts pres_pen * 1(appeared) from logits. float logits[] = {5.0f, 5.0f, 5.0f, 5.0f}; SamplerCfg cfg; @@ -3321,7 +3306,7 @@ static void test_sampler_presence_penalty() { TEST_ASSERT(counts[2] + counts[3] > counts[0] + counts[1]); } -static void test_sampler_freq_and_pres_combined() { +TEST_CASE(ServerUnitFixture, test_sampler_freq_and_pres_combined) { // Both penalties applied together. float logits[] = {5.0f, 5.0f, 5.0f}; SamplerCfg cfg; @@ -3344,7 +3329,7 @@ static void test_sampler_freq_and_pres_combined() { TEST_ASSERT(counts[1] > counts[0]); } -static void test_sampler_negative_frequency_penalty() { +TEST_CASE(ServerUnitFixture, test_sampler_negative_frequency_penalty) { // Negative freq_pen should encourage repetition. float logits[] = {3.0f, 3.0f, 3.0f}; SamplerCfg cfg; @@ -3363,7 +3348,7 @@ static void test_sampler_negative_frequency_penalty() { TEST_ASSERT(counts[0] > counts[2]); } -static void test_sampler_seed_reproducibility() { +TEST_CASE(ServerUnitFixture, test_sampler_seed_reproducibility) { // Same seed should produce identical sequences. float logits[] = {1.0f, 2.0f, 3.0f, 2.0f, 1.0f}; SamplerCfg cfg; @@ -3380,7 +3365,7 @@ static void test_sampler_seed_reproducibility() { } } -static void test_sampler_rep_window_limits_scope() { +TEST_CASE(ServerUnitFixture, test_sampler_rep_window_limits_scope) { // With rep_window=2, only the last 2 history tokens should be penalized. float logits[] = {5.0f, 5.0f, 5.0f, 5.0f}; SamplerCfg cfg; @@ -3400,7 +3385,7 @@ static void test_sampler_rep_window_limits_scope() { TEST_ASSERT(counts[0] + counts[1] > counts[2] + counts[3]); } -static void test_parse_sampler_token_basic() { +TEST_CASE(ServerUnitFixture, test_parse_sampler_token_basic) { std::string line = "gen 128 samp=0.7,0.9,40,1.1,42"; SamplerCfg cfg; TEST_ASSERT(parse_sampler_token(line, cfg)); @@ -3414,7 +3399,7 @@ static void test_parse_sampler_token_basic() { TEST_ASSERT(cfg.pres_pen == 0.0f); } -static void test_parse_sampler_token_with_penalties() { +TEST_CASE(ServerUnitFixture, test_parse_sampler_token_with_penalties) { std::string line = "gen 64 samp=0.5,0.95,20,1.0,0,0.8,1.2"; SamplerCfg cfg; TEST_ASSERT(parse_sampler_token(line, cfg)); @@ -3428,7 +3413,7 @@ static void test_parse_sampler_token_with_penalties() { TEST_ASSERT(std::abs(cfg.pres_pen - 1.2f) < 1e-5f); } -static void test_parse_sampler_token_minimal() { +TEST_CASE(ServerUnitFixture, test_parse_sampler_token_minimal) { // Only temp specified. std::string line = "gen 32 samp=0.3"; SamplerCfg cfg; @@ -3441,14 +3426,14 @@ static void test_parse_sampler_token_minimal() { TEST_ASSERT(cfg.pres_pen == 0.0f); } -static void test_parse_sampler_token_no_samp() { +TEST_CASE(ServerUnitFixture, test_parse_sampler_token_no_samp) { std::string line = "gen 128"; SamplerCfg cfg; TEST_ASSERT(!parse_sampler_token(line, cfg)); TEST_ASSERT(line == "gen 128"); // unchanged } -static void test_sampler_temp_zero_with_penalties_uses_argmax() { +TEST_CASE(ServerUnitFixture, test_sampler_temp_zero_with_penalties_uses_argmax) { // temp=0 + penalties should apply penalties then return argmax (deterministic). float logits[] = {5.0f, 5.0f, 5.0f, 5.0f}; SamplerCfg cfg; @@ -3469,7 +3454,7 @@ static void test_sampler_temp_zero_with_penalties_uses_argmax() { } } -static void test_sampler_needs_logit_processing() { +TEST_CASE(ServerUnitFixture, test_sampler_needs_logit_processing) { SamplerCfg cfg; TEST_ASSERT(!cfg.needs_logit_processing()); // all defaults → no processing @@ -3516,7 +3501,7 @@ static ServerConfig make_props_config_with_sidecar(const json & sidecar) { return cfg; } -static void test_props_model_card_wholesale_sidecar() { +TEST_CASE(ServerUnitFixture, test_props_model_card_wholesale_sidecar) { // When a sidecar was loaded, /props.model_card should be the parsed // sidecar JSON verbatim — *all* fields from the file, not just the // five budget-derived ones from the pre-refactor shape. @@ -3564,7 +3549,7 @@ static void test_props_model_card_wholesale_sidecar() { TEST_ASSERT(!body["model_card"].contains("hard_limit_reply_budget")); } -static void test_props_model_card_null_on_family_fallback() { +TEST_CASE(ServerUnitFixture, test_props_model_card_null_on_family_fallback) { // When family or hard fallback was used (no sidecar), /props.model_card // is JSON null. The budget_envelope still carries the resolved values. ServerConfig cfg; @@ -3588,7 +3573,7 @@ static void test_props_model_card_null_on_family_fallback() { TEST_ASSERT(body["budget_envelope"]["default_max_tokens"].get() == 32768); } -static void test_props_budget_envelope_shape() { +TEST_CASE(ServerUnitFixture, test_props_budget_envelope_shape) { // budget_envelope is always present with all five fields and the // expected effort_tiers vocabulary (low|medium|high|x-high|max). // Values mirror ServerConfig regardless of what the sidecar carried. @@ -3641,7 +3626,7 @@ static void test_props_budget_envelope_shape() { // Snapshot/bench tooling reads /props.runtime wholesale into // result.json.server_info; this test pins the field set so additions // elsewhere don't accidentally drop a knob we depend on for forensics. -static void test_props_runtime_shape() { +TEST_CASE(ServerUnitFixture, test_props_runtime_shape) { ServerConfig cfg = make_props_config_with_sidecar(json{ {"name", "Qwen3.6 27B"}, {"source", "https://huggingface.co/Qwen/Qwen3.6-27B"}, @@ -3690,7 +3675,7 @@ static void test_props_runtime_shape() { // response shapes plus the zero-decode_s div-by-zero guard. // ═══════════════════════════════════════════════════════════════════════ -static void test_usage_timings_openai_chat_streaming() { +TEST_CASE(ServerUnitFixture, test_usage_timings_openai_chat_streaming) { // OpenAI Chat streaming: the terminal usage chunk (just before // data: [DONE]) carries `timings.{prefill_ms, decode_ms, // decode_tokens_per_sec}` when timings are passed to emit_finish. @@ -3710,7 +3695,7 @@ static void test_usage_timings_openai_chat_streaming() { TEST_ASSERT(finish_str.find("[DONE]") != std::string::npos); } -static void test_usage_timings_anthropic_streaming() { +TEST_CASE(ServerUnitFixture, test_usage_timings_anthropic_streaming) { // Anthropic streaming: message_delta.usage gains a `timings` // sibling alongside `output_tokens`. auto em = make_emitter(ApiFormat::ANTHROPIC); @@ -3727,7 +3712,7 @@ static void test_usage_timings_anthropic_streaming() { TEST_ASSERT(finish_str.find("\"decode_tokens_per_sec\":20.0") != std::string::npos); } -static void test_usage_timings_responses_streaming() { +TEST_CASE(ServerUnitFixture, test_usage_timings_responses_streaming) { // Responses streaming: response.completed.usage gains `timings`. auto em = make_emitter(ApiFormat::RESPONSES); em.emit_start(); @@ -3743,7 +3728,7 @@ static void test_usage_timings_responses_streaming() { TEST_ASSERT(finish_str.find("\"decode_tokens_per_sec\":25.0") != std::string::npos); } -static void test_usage_timings_zero_decode_no_div_by_zero() { +TEST_CASE(ServerUnitFixture, test_usage_timings_zero_decode_no_div_by_zero) { // decode_s == 0 (prefill-only / no tokens generated path): emit // decode_tokens_per_sec = 0.0 without div-by-zero. GenTimings t{0.123, 0.0}; @@ -3763,7 +3748,7 @@ static void test_usage_timings_zero_decode_no_div_by_zero() { TEST_ASSERT(finish_str.find("nan") == std::string::npos); } -static void test_usage_timings_omitted_when_null() { +TEST_CASE(ServerUnitFixture, test_usage_timings_omitted_when_null) { // Backward compat: emit_finish(n) (no timings) emits the legacy // usage block — no `timings` key. Guards the SDK-facing default // for callers that don't yet wire timings through. @@ -3825,7 +3810,7 @@ struct EmptySpecRetryBackend : MockBackend { } }; -static void test_model_backend_retries_empty_spec_generate_once_with_ar() { +TEST_CASE(ServerUnitFixture, test_model_backend_retries_empty_spec_generate_once_with_ar) { EmptySpecRetryBackend backend; GenerateRequest req; req.prompt = {1, 2, 3}; @@ -3842,7 +3827,7 @@ static void test_model_backend_retries_empty_spec_generate_once_with_ar() { TEST_ASSERT(backend.generate_saw_force_ar); } -static void test_model_backend_retries_empty_spec_restore_once_with_ar() { +TEST_CASE(ServerUnitFixture, test_model_backend_retries_empty_spec_restore_once_with_ar) { EmptySpecRetryBackend backend; GenerateRequest req; req.prompt = {1, 2, 3}; @@ -3860,7 +3845,7 @@ static void test_model_backend_retries_empty_spec_restore_once_with_ar() { TEST_ASSERT(backend.restore_saw_force_ar); } -static void test_model_backend_retries_empty_visible_spec_generate_once_with_ar() { +TEST_CASE(ServerUnitFixture, test_model_backend_retries_empty_visible_spec_generate_once_with_ar) { EmptySpecRetryBackend backend; backend.generate_first_empty_visible = true; GenerateRequest req; @@ -3879,7 +3864,7 @@ static void test_model_backend_retries_empty_visible_spec_generate_once_with_ar( TEST_ASSERT(backend.generate_saw_force_ar); } -static void test_model_backend_retries_empty_visible_spec_restore_once_with_ar() { +TEST_CASE(ServerUnitFixture, test_model_backend_retries_empty_visible_spec_restore_once_with_ar) { EmptySpecRetryBackend backend; backend.restore_first_empty_visible = true; GenerateRequest req; @@ -3901,18 +3886,18 @@ static void test_model_backend_retries_empty_visible_spec_restore_once_with_ar() // GenerateResult.accept_rate plumbing tests (Day 1 of bandit MVP) // ═══════════════════════════════════════════════════════════════════════ -static void test_generate_result_accept_rate_defaults_to_zero() { +TEST_CASE(ServerUnitFixture, test_generate_result_accept_rate_defaults_to_zero) { GenerateResult r; TEST_ASSERT(r.accept_rate == 0.0f); } -static void test_generate_result_accept_rate_can_be_set() { +TEST_CASE(ServerUnitFixture, test_generate_result_accept_rate_can_be_set) { GenerateResult r; r.accept_rate = 0.85f; TEST_ASSERT(r.accept_rate == 0.85f); } -static void test_generate_result_accept_rate_bounds() { +TEST_CASE(ServerUnitFixture, test_generate_result_accept_rate_bounds) { GenerateResult r; r.accept_rate = 0.0f; TEST_ASSERT(r.accept_rate >= 0.0f && r.accept_rate <= 1.0f); @@ -3920,7 +3905,7 @@ static void test_generate_result_accept_rate_bounds() { TEST_ASSERT(r.accept_rate >= 0.0f && r.accept_rate <= 1.0f); } -static void test_generate_result_accept_rate_in_usage_openai() { +TEST_CASE(ServerUnitFixture, test_generate_result_accept_rate_in_usage_openai) { // Simulate the non-streaming OpenAI JSON response build. // Verify accept_rate flows from GenerateResult into usage block. GenerateResult result; @@ -3944,7 +3929,7 @@ static void test_generate_result_accept_rate_in_usage_openai() { TEST_ASSERT(std::abs(resp["usage"]["accept_rate"].get() - 0.75f) < 1e-6f); } -static void test_generate_result_accept_rate_in_usage_anthropic() { +TEST_CASE(ServerUnitFixture, test_generate_result_accept_rate_in_usage_anthropic) { GenerateResult result; result.succeed(); result.tokens = {1, 2}; @@ -3964,7 +3949,7 @@ static void test_generate_result_accept_rate_in_usage_anthropic() { TEST_ASSERT(std::abs(resp["usage"]["accept_rate"].get() - 0.60f) < 1e-6f); } -static void test_generate_result_accept_rate_zero_when_no_spec_decode() { +TEST_CASE(ServerUnitFixture, test_generate_result_accept_rate_zero_when_no_spec_decode) { // When spec decode doesn't run (no draft model), accept_rate stays 0. GenerateResult r; r.succeed(); @@ -3972,7 +3957,7 @@ static void test_generate_result_accept_rate_zero_when_no_spec_decode() { TEST_ASSERT(r.accept_rate == 0.0f); } -static void test_generate_result_error_state_is_consistent() { +TEST_CASE(ServerUnitFixture, test_generate_result_error_state_is_consistent) { GenerateResult result; TEST_ASSERT(!result.ok()); TEST_ASSERT(result.error->code == GenerateErrorCode::Incomplete); @@ -3995,7 +3980,7 @@ static void test_generate_result_error_state_is_consistent() { // normalize_system_for_cache — header-strip tests // ═══════════════════════════════════════════════════════════════════════ -static void test_normalize_strips_billing_header_anthropic_array() { +TEST_CASE(ServerUnitFixture, test_normalize_strips_billing_header_anthropic_array) { // Anthropic system-as-array: one billing-header block + one real block. json system_blocks = json::array({ {{"type", "text"}, @@ -4008,7 +3993,7 @@ static void test_normalize_strips_billing_header_anthropic_array() { TEST_ASSERT(out.find("helpful coding assistant") != std::string::npos); } -static void test_normalize_strips_billing_header_openai_messages0() { +TEST_CASE(ServerUnitFixture, test_normalize_strips_billing_header_openai_messages0) { // OpenAI messages[0] system containing the billing header in content. json messages = json::array({ {{"role", "system"}, @@ -4020,7 +4005,7 @@ static void test_normalize_strips_billing_header_openai_messages0() { TEST_ASSERT(out.find("code reviewer") != std::string::npos); } -static void test_normalize_idempotent_across_changing_header() { +TEST_CASE(ServerUnitFixture, test_normalize_idempotent_across_changing_header) { // Two OpenAI messages arrays identical except the header turn value. // normalize_system_for_cache must return EQUAL strings for both. json messages_turn4 = json::array({ @@ -4038,7 +4023,7 @@ static void test_normalize_idempotent_across_changing_header() { TEST_ASSERT(out4 == out5); } -static void test_normalize_preserves_legit_system_content() { +TEST_CASE(ServerUnitFixture, test_normalize_preserves_legit_system_content) { // A normal system prompt containing no billing header must pass through unchanged. json messages = json::array({ {{"role", "system"}, @@ -4049,7 +4034,7 @@ static void test_normalize_preserves_legit_system_content() { TEST_ASSERT(out == "You are an expert in C++ performance optimization."); } -static void test_normalize_handles_leading_whitespace_header() { +TEST_CASE(ServerUnitFixture, test_normalize_handles_leading_whitespace_header) { // Header block with leading whitespace must still be stripped. json system_blocks = json::array({ {{"type", "text"}, @@ -4062,7 +4047,7 @@ static void test_normalize_handles_leading_whitespace_header() { TEST_ASSERT(out.find("Be concise.") != std::string::npos); } -static void test_prefix_key_stable_across_header_change() { +TEST_CASE(ServerUnitFixture, test_prefix_key_stable_across_header_change) { // Two /v1/chat/completions-style messages arrays differing ONLY in the // billing header value must normalize to EQUAL strings. json messages_a = json::array({ @@ -4084,7 +4069,7 @@ static void test_prefix_key_stable_across_header_change() { // FlowKV + disk-cache compose tests (T1–T7) // T4 (compress=false): policy name has no "+compress" suffix. -static void test_flowkv_T4_compress_false_policy_name_no_suffix() { +TEST_CASE(ServerUnitFixture, test_flowkv_T4_compress_false_policy_name_no_suffix) { DiskPrefixCachePolicy p; p.mode = DiskPrefixCacheMode::Full; p.compress = false; @@ -4094,7 +4079,7 @@ static void test_flowkv_T4_compress_false_policy_name_no_suffix() { } // T4 (compress=true): policy name has "+compress" suffix. -static void test_flowkv_T4_compress_true_policy_name_has_suffix() { +TEST_CASE(ServerUnitFixture, test_flowkv_T4_compress_true_policy_name_has_suffix) { DiskPrefixCachePolicy p; p.mode = DiskPrefixCacheMode::Full; p.compress = true; @@ -4114,13 +4099,13 @@ static void test_flowkv_T4_compress_true_policy_name_has_suffix() { } // T4: default DiskPrefixCachePolicy has compress=false (no-op). -static void test_flowkv_T4_default_no_compress() { +TEST_CASE(ServerUnitFixture, test_flowkv_T4_default_no_compress) { DiskPrefixCachePolicy p; TEST_ASSERT_MSG(!p.compress, "default compress must be false (byte-identical to pr364-base)"); } // T6: frozen_block_key is deterministic — same tokens → same hash. -static void test_flowkv_T6_frozen_block_key_deterministic() { +TEST_CASE(ServerUnitFixture, test_flowkv_T6_frozen_block_key_deterministic) { std::vector ids = {10, 20, 30, 40, 50}; PrefixHash k1 = frozen_block_key(ids.data(), 0, (int)ids.size()); PrefixHash k2 = frozen_block_key(ids.data(), 0, (int)ids.size()); @@ -4128,7 +4113,7 @@ static void test_flowkv_T6_frozen_block_key_deterministic() { } // T6: frozen_block_key returns zero hash on empty slice. -static void test_flowkv_T6_frozen_block_key_zero_on_empty() { +TEST_CASE(ServerUnitFixture, test_flowkv_T6_frozen_block_key_zero_on_empty) { std::vector ids = {10, 20, 30}; PrefixHash k = frozen_block_key(ids.data(), 2, 2); // begin == end PrefixHash zero{}; @@ -4138,7 +4123,7 @@ static void test_flowkv_T6_frozen_block_key_zero_on_empty() { } // T6: distinct token content → distinct hashes. -static void test_flowkv_T6_frozen_block_key_distinct_content() { +TEST_CASE(ServerUnitFixture, test_flowkv_T6_frozen_block_key_distinct_content) { std::vector a = {1, 2, 3}; std::vector b = {1, 2, 4}; PrefixHash ka = frozen_block_key(a.data(), 0, 3); @@ -4148,7 +4133,7 @@ static void test_flowkv_T6_frozen_block_key_distinct_content() { // T7: disk clamp — with compress=true, boundary should use system_end (first // safe boundary), not the full prompt. Tested via the fixed-boundary logic. -static void test_flowkv_T7_disk_clamp_system_end_boundary() { +TEST_CASE(ServerUnitFixture, test_flowkv_T7_disk_clamp_system_end_boundary) { // Simulate: effective_prompt has a system_end at token 300. // The FlowKV disk-clamp should set fixed_tokens = system_end. // We test this by constructing a DiskPrefixCachePolicy and verifying that @@ -4179,7 +4164,7 @@ static void test_flowkv_T7_disk_clamp_system_end_boundary() { // T3 (WS1): non-continuation messages JSON has no assistant role. // This tests the JSON shape that the is_continuation check reads. -static void test_flowkv_T3_ws1_continuation_json_shape() { +TEST_CASE(ServerUnitFixture, test_flowkv_T3_ws1_continuation_json_shape) { // Single user message: NOT a continuation. json msgs = json::array({ {{"role", "system"}, {"content", "You are an assistant."}}, @@ -4215,7 +4200,7 @@ static void test_flowkv_T3_ws1_continuation_json_shape() { // T1 (head-verbatim): system_end is the FIRST boundary (boundary[0]). // Verifies the disk-clamp invariant: system_end = find_all_boundaries()[0]. // Tests the boundary function returns a sane first boundary on a chat prompt. -static void test_flowkv_T1_system_end_boundary_first() { +TEST_CASE(ServerUnitFixture, test_flowkv_T1_system_end_boundary_first) { // Construct a synthetic token stream where chat markers appear at known // positions. find_all_boundaries uses prefix_cache_.chat_markers() which // are model-specific; test the boundary API directly. @@ -4238,7 +4223,7 @@ static void test_flowkv_T1_system_end_boundary_first() { // T5 (inert-guard): aged_token_estimate < 512 → FlowKV-OFF. // Tests the guard constant and comparison logic. -static void test_flowkv_T5_inert_guard_token_count() { +TEST_CASE(ServerUnitFixture, test_flowkv_T5_inert_guard_token_count) { static constexpr int kFkvInertMinTokens = 512; // Below threshold: FlowKV should not fire. TEST_ASSERT(400 < kFkvInertMinTokens); @@ -4325,7 +4310,7 @@ static std::string write_qwen3_drafter_fixture_gguf() { return path; } -static void test_qwen3_drafter_rejects_truncated_gguf() { +TEST_CASE(ServerUnitFixture, test_qwen3_drafter_rejects_truncated_gguf) { const std::string path = write_qwen3_drafter_fixture_gguf(); ggml_backend_t backend = ggml_backend_cpu_init(); @@ -4370,7 +4355,7 @@ static void test_qwen3_drafter_rejects_truncated_gguf() { // past the mapping (#438), without wrongly rejecting valid files (#318). These // tests pin the boundary and, critically, the size_t overflow behaviour that a // naive `data_off + tensor_off + tensor_sz > file_size` test gets wrong. -static void test_gguf_tensor_in_file_bounds() { +TEST_CASE(ServerUnitFixture, test_gguf_tensor_in_file_bounds) { // Typical layout: 100-byte header/kv, 900-byte data section, 1000-byte file. const size_t data_off = 100; const size_t file = 1000; @@ -4405,7 +4390,7 @@ static void test_gguf_tensor_in_file_bounds() { TEST_ASSERT(!gguf_tensor_in_file(kMax, 10, 10, file)); // huge data_off } -static void test_gguf_bounds_error_reports_operands() { +TEST_CASE(ServerUnitFixture, test_gguf_bounds_error_reports_operands) { // A normal (non-overflowing) rejection: the message must surface every // operand so a false positive on a valid file (#318) is diagnosable. const std::string e = gguf_bounds_error("target GGUF", "blk.56.ssm_out.weight", @@ -4431,294 +4416,3 @@ static void test_gguf_bounds_error_reports_operands() { kMax, 10, 10, 100); TEST_ASSERT(o.find("overflow") != std::string::npos); } - -int main() { - std::fprintf(stderr, "══════════════════════════════════════════\n"); - std::fprintf(stderr, " Server Unit Tests\n"); - std::fprintf(stderr, "══════════════════════════════════════════\n"); - - std::fprintf(stderr, "\n── UTF-8 utilities ──\n"); - RUN_TEST(test_utf8_safe_len_ascii); - RUN_TEST(test_utf8_safe_len_partial_2byte); - RUN_TEST(test_utf8_safe_len_partial_3byte); - RUN_TEST(test_utf8_safe_len_partial_4byte); - RUN_TEST(test_utf8_sanitize_valid); - RUN_TEST(test_utf8_sanitize_replaces_invalid); - RUN_TEST(test_utf8_sanitize_empty); - - std::fprintf(stderr, "\n── Reasoning parser ──\n"); - RUN_TEST(test_reasoning_basic); - RUN_TEST(test_reasoning_no_tags); - RUN_TEST(test_reasoning_started_in_thinking); - RUN_TEST(test_emitter_started_in_thinking_without_open_tag); - RUN_TEST(test_reasoning_unclosed_think); - RUN_TEST(test_reasoning_empty_thinking); - RUN_TEST(test_reasoning_whitespace_in_think); - RUN_TEST(test_reasoning_disabled); - - std::fprintf(stderr, "\n── Tool parser ──\n"); - RUN_TEST(test_parse_tool_call_xml); - RUN_TEST(test_parse_bare_function_xml); - RUN_TEST(test_parse_bare_tool_name_xml_with_function_close); - RUN_TEST(test_parse_json_tool_call); - RUN_TEST(test_parse_single_tool_bare_json_args); - RUN_TEST(test_parse_single_tool_bare_json_args_allows_empty_optional_object); - RUN_TEST(test_parse_single_tool_bare_json_args_rejects_prose); - RUN_TEST(test_parse_single_tool_bare_json_args_rejects_ambiguous_tools); - RUN_TEST(test_parse_no_tools); - RUN_TEST(test_parse_tool_code_wrapper); - RUN_TEST(test_parse_tool_allowed_filter); - RUN_TEST(test_parse_call_verb_empty_args); - RUN_TEST(test_parse_call_verb_strict_json_args); - RUN_TEST(test_parse_call_verb_namespaced_verb); - RUN_TEST(test_parse_call_verb_whitespace_before_key); - RUN_TEST(test_parse_call_verb_missing_close_brace_rejected); - RUN_TEST(test_parse_call_verb_narrative_without_body_rejected); - RUN_TEST(test_parse_call_verb_underscore_prefix); - RUN_TEST(test_parse_call_verb_nested_object_args); - RUN_TEST(test_parse_call_verb_back_to_back); - RUN_TEST(test_parse_call_verb_relaxed_single_quotes); - RUN_TEST(test_parse_call_verb_glued_to_word_rejected); - RUN_TEST(test_parse_call_verb_does_not_hijack_inner_name); - // PR #341 imports (relocated alongside the test bodies above) - RUN_TEST(test_parse_call_verb_single); - RUN_TEST(test_parse_call_verb_namespaced); - RUN_TEST(test_parse_call_verb_snake_and_hyphen); - RUN_TEST(test_parse_call_verb_tool_allowed_filter); - RUN_TEST(test_parse_call_verb_inline_prose_rejected); - RUN_TEST(test_parse_call_verb_inline_prose_after_space); - RUN_TEST(test_parse_call_verb_malformed_args); - RUN_TEST(test_parse_call_verb_inner_brace_in_string); - RUN_TEST(test_parse_call_verb_unquoted_keys); - RUN_TEST(test_parse_call_verb_cleaned_text); - RUN_TEST(test_parse_call_verb_intercept_inner_json); - RUN_TEST(test_parse_call_verb_multiline_args); - RUN_TEST(test_parse_call_verb_singlequote_with_inner_doublequote); - RUN_TEST(test_parse_call_verb_backtick_with_inner_doublequote); - - std::fprintf(stderr, "\n── SSE Emitter ──\n"); - RUN_TEST(test_emitter_reasoning_split_openai); - RUN_TEST(test_emitter_first_content_index_natural_close); - RUN_TEST(test_emitter_first_content_index_never_closed); - RUN_TEST(test_emitter_first_content_index_content_only); - RUN_TEST(test_emitter_first_content_index_qwen36_streaming_thinking); - RUN_TEST(test_emitter_reasoning_strips_leading_think_tag); - RUN_TEST(test_emitter_content_only_no_thinking); - RUN_TEST(test_emitter_tool_buffer_detection); - RUN_TEST(test_emitter_anthropic_tool_use_blocks); - RUN_TEST(test_emitter_single_tool_bare_json_args); - RUN_TEST(test_emitter_bare_json_args_do_not_trigger_after_content); - RUN_TEST(test_emitter_bare_function_tool_buffer_detection); - RUN_TEST(test_emitter_does_not_leak_malformed_tool_xml); - RUN_TEST(test_emitter_parses_tool_call_missing_outer_close); - RUN_TEST(test_emitter_no_tools_keeps_tool_like_text); - RUN_TEST(test_emitter_anthropic_structure); - RUN_TEST(test_emitter_responses_structure); - RUN_TEST(test_emitter_responses_bare_function_tool_call); - RUN_TEST(test_emitter_streaming_openai_has_done); - RUN_TEST(test_emitter_nonstreaming_accumulates); - RUN_TEST(test_emitter_anthropic_thinking_blocks); - - std::fprintf(stderr, "\n── Stop sequences ──\n"); - RUN_TEST(test_stop_sequence_basic); - RUN_TEST(test_stop_sequence_mid_token); - RUN_TEST(test_stop_sequence_multiple); - RUN_TEST(test_stop_sequence_no_match); - RUN_TEST(test_stop_sequence_empty_list); - RUN_TEST(test_stop_sequence_finish_reason); - RUN_TEST(test_stop_sequence_streaming_output); - RUN_TEST(test_stop_sequence_anthropic_format); - RUN_TEST(test_stop_sequence_in_reasoning_mode); - RUN_TEST(test_stop_sequence_holdback_extends); - - std::fprintf(stderr, "\n── Prefix cache (hash) ──\n"); - RUN_TEST(test_hash_prefix_deterministic); - RUN_TEST(test_hash_prefix_different_inputs); - RUN_TEST(test_hash_prefix_different_lengths); - RUN_TEST(test_hash_prefix_empty); - RUN_TEST(test_find_boundaries_empty); - RUN_TEST(test_evict_empty_is_zero); - RUN_TEST(test_evict_single_is_zero); - RUN_TEST(test_evict_chain_keeps_ancestors); - RUN_TEST(test_evict_unrelated_falls_back_to_lru); - RUN_TEST(test_evict_branch_spares_shared_root); - - std::fprintf(stderr, "\n── PFlash config ──\n"); - RUN_TEST(test_pflash_config_defaults); - RUN_TEST(test_pflash_config_modes); - RUN_TEST(test_pflash_compress_request_struct); - RUN_TEST(test_pflash_compress_result_defaults); - RUN_TEST(test_pflash_threshold_auto_mode); - RUN_TEST(test_pflash_threshold_always_mode); - RUN_TEST(test_pflash_config_upstream_defaults); - RUN_TEST(test_pflash_curve_interpolation); - RUN_TEST(test_pflash_curve_empty_uses_flat); - RUN_TEST(test_pflash_upstream_proxy_config); - RUN_TEST(test_pflash_raw_body_preserved); - RUN_TEST(test_pflash_placement_same_backend_local); - RUN_TEST(test_pflash_placement_mixed_backend_remote); - RUN_TEST(test_pflash_placement_auto_draft_follows_target); - RUN_TEST(test_pflash_placement_disabled_never_remote); - RUN_TEST(test_pflash_placement_usage_gate); - RUN_TEST(test_draft_residency_parse); - RUN_TEST(test_draft_residency_pflash_auto); - RUN_TEST(test_draft_residency_dflash_auto_and_request_scoped); - - std::fprintf(stderr, "\n── Backend IPC ──\n"); - RUN_TEST(test_backend_ipc_rejects_file_work_dir); - RUN_TEST(test_backend_ipc_payload_pipe_round_trip); - RUN_TEST(test_backend_ipc_payload_transport_parse); - RUN_TEST(test_backend_ipc_payload_bounds); - RUN_TEST(test_backend_ipc_shared_payload_map_sizing); - RUN_TEST(test_backend_ipc_shared_payload_segment_contract); - RUN_TEST(test_moe_hybrid_expert_compute_batch_default); - RUN_TEST(test_moe_hybrid_expert_compute_ipc_mode_batch_limit); - RUN_TEST(test_moe_hybrid_prefill_hot_sub_batch_limit); - - std::fprintf(stderr, "\n── Jinja chat template ──\n"); - RUN_TEST(test_deepseek4_render_system_only_gen_prompt); - RUN_TEST(test_deepseek4_render_empty_chat_gen_prompt); - RUN_TEST(test_jinja_render_basic); - RUN_TEST(test_jinja_render_no_gen_prompt); - RUN_TEST(test_jinja_render_tools_injected); - RUN_TEST(test_jinja_render_empty_tools_skipped); - RUN_TEST(test_jinja_render_bos_eos_threaded); - RUN_TEST(test_jinja_render_empty_template_throws); - RUN_TEST(test_jinja_render_bad_tools_json_throws); - RUN_TEST(test_normalize_responses_tool_followup_messages); - - std::fprintf(stderr, "\n── Placement config ──\n"); - RUN_TEST(test_parse_target_device_list_same_backend); - RUN_TEST(test_parse_target_device_list_mixed_backend); - RUN_TEST(test_parse_target_device_list_mixed_backend_multi_remote); - RUN_TEST(test_parse_target_device_list_single_gpu_is_not_layer_split); - RUN_TEST(test_validate_layer_split_weights_shape); - RUN_TEST(test_target_shard_plan_same_backend_split); - RUN_TEST(test_target_shard_plan_mixed_backend_split); - RUN_TEST(test_target_shard_plan_rejects_bad_local_backend); - RUN_TEST(test_kvflash_pager_identity_sync_contract); - RUN_TEST(test_layer_split_kvflash_history_contract); - RUN_TEST(test_backend_precision_cuda_sm_policy); - RUN_TEST(test_backend_precision_hip_arch_policy); - RUN_TEST(test_backend_precision_activation_type_combine); - RUN_TEST(test_layer_split_backend_inline_snapshot_and_restore_delta); - RUN_TEST(test_layer_split_backend_sampling_capability_gate); - RUN_TEST(test_layer_split_backend_chunks_prefill_by_adapter_limit); - RUN_TEST(test_layer_split_compress_nopark_uses_default_drafter_path); - RUN_TEST(test_layer_split_compress_rejects_bad_keep_ratio); - RUN_TEST(test_layer_split_backend_shutdown_is_idempotent); - RUN_TEST(test_layer_split_backend_capability_proxy); - - std::fprintf(stderr, "\n── Disk prefix cache ──\n"); - RUN_TEST(test_disk_cache_config_defaults); - RUN_TEST(test_disk_cache_policy_parse); - RUN_TEST(test_scope_override_preserves_compress); - RUN_TEST(test_disk_cache_fixed_boundary); - RUN_TEST(test_disk_cache_auto_boundary_lcp); - RUN_TEST(test_disk_cache_auto_window_limits_history); - RUN_TEST(test_disk_cache_disabled_when_no_dir); - RUN_TEST(test_disk_cache_init_creates_directory); - RUN_TEST(test_disk_cache_header_size); - RUN_TEST(test_disk_cache_header_round_trip); - RUN_TEST(test_disk_cache_continued_boundary); - RUN_TEST(test_disk_cache_continued_interval_logic); - RUN_TEST(test_disk_cache_cold_prefix_short_prompt); - RUN_TEST(test_disk_cache_cold_prefix_no_boundaries); - RUN_TEST(test_disk_cache_cold_prefix_finds_boundary); - RUN_TEST(test_disk_cache_budget_enforcement_scoring); - RUN_TEST(test_disk_cache_lookup_miss_no_layout); - RUN_TEST(test_disk_cache_save_below_min_tokens); - - std::fprintf(stderr, "\n── Disk-cache identity salt (manifest hardening) ──\n"); - RUN_TEST(test_disk_identity_salt_changes_layout_id); - RUN_TEST(test_disk_identity_salt_zero_is_backcompat); - - std::fprintf(stderr, "\n── Sampler ──\n"); - RUN_TEST(test_sampler_cfg_defaults); - RUN_TEST(test_sampler_greedy_argmax); - RUN_TEST(test_sampler_temperature_affects_distribution); - RUN_TEST(test_sampler_top_p_truncation); - RUN_TEST(test_sampler_top_k_truncation); - RUN_TEST(test_sampler_repetition_penalty); - RUN_TEST(test_sampler_frequency_penalty); - RUN_TEST(test_sampler_presence_penalty); - RUN_TEST(test_sampler_freq_and_pres_combined); - RUN_TEST(test_sampler_negative_frequency_penalty); - RUN_TEST(test_sampler_seed_reproducibility); - RUN_TEST(test_sampler_rep_window_limits_scope); - RUN_TEST(test_parse_sampler_token_basic); - RUN_TEST(test_parse_sampler_token_with_penalties); - RUN_TEST(test_parse_sampler_token_minimal); - RUN_TEST(test_parse_sampler_token_no_samp); - RUN_TEST(test_sampler_temp_zero_with_penalties_uses_argmax); - RUN_TEST(test_sampler_needs_logit_processing); - - std::fprintf(stderr, "\n── /props body shape ──\n"); - RUN_TEST(test_props_model_card_wholesale_sidecar); - RUN_TEST(test_props_model_card_null_on_family_fallback); - RUN_TEST(test_props_budget_envelope_shape); - RUN_TEST(test_props_runtime_shape); - - std::fprintf(stderr, "\n── usage.timings ──\n"); - RUN_TEST(test_usage_timings_openai_chat_streaming); - RUN_TEST(test_usage_timings_anthropic_streaming); - RUN_TEST(test_usage_timings_responses_streaming); - RUN_TEST(test_usage_timings_zero_decode_no_div_by_zero); - RUN_TEST(test_usage_timings_omitted_when_null); - - std::fprintf(stderr, "\n── ModelBackend empty-spec retry ──\n"); - RUN_TEST(test_model_backend_retries_empty_spec_generate_once_with_ar); - RUN_TEST(test_model_backend_retries_empty_spec_restore_once_with_ar); - RUN_TEST(test_model_backend_retries_empty_visible_spec_generate_once_with_ar); - RUN_TEST(test_model_backend_retries_empty_visible_spec_restore_once_with_ar); - - std::fprintf(stderr, "\n── GenerateResult.accept_rate ──\n"); - RUN_TEST(test_generate_result_accept_rate_defaults_to_zero); - RUN_TEST(test_generate_result_accept_rate_can_be_set); - RUN_TEST(test_generate_result_accept_rate_bounds); - RUN_TEST(test_generate_result_accept_rate_in_usage_openai); - RUN_TEST(test_generate_result_accept_rate_in_usage_anthropic); - RUN_TEST(test_generate_result_accept_rate_zero_when_no_spec_decode); - RUN_TEST(test_generate_result_error_state_is_consistent); - - std::fprintf(stderr, "\n── normalize_system_for_cache ──\n"); - RUN_TEST(test_normalize_strips_billing_header_anthropic_array); - RUN_TEST(test_normalize_strips_billing_header_openai_messages0); - RUN_TEST(test_normalize_idempotent_across_changing_header); - RUN_TEST(test_normalize_preserves_legit_system_content); - RUN_TEST(test_normalize_handles_leading_whitespace_header); - RUN_TEST(test_prefix_key_stable_across_header_change); - - // ─── FlowKV + disk-cache compose ───────────────────────────────────── - // T1-T7 from split/11-flowkv-compose brief. - std::fprintf(stderr, "\n── FlowKV + disk-cache compose ──\n"); - RUN_TEST(test_flowkv_T4_compress_false_policy_name_no_suffix); - RUN_TEST(test_flowkv_T4_compress_true_policy_name_has_suffix); - RUN_TEST(test_flowkv_T4_default_no_compress); - RUN_TEST(test_flowkv_T6_frozen_block_key_deterministic); - RUN_TEST(test_flowkv_T6_frozen_block_key_zero_on_empty); - RUN_TEST(test_flowkv_T6_frozen_block_key_distinct_content); - RUN_TEST(test_flowkv_T7_disk_clamp_system_end_boundary); - RUN_TEST(test_flowkv_T3_ws1_continuation_json_shape); - RUN_TEST(test_flowkv_T1_system_end_boundary_first); - RUN_TEST(test_flowkv_T5_inert_guard_token_count); - - std::fprintf(stderr, "\n── Qwen3-0.6B drafter loader (bug #438) ──\n"); - RUN_TEST(test_qwen3_drafter_rejects_truncated_gguf); - - std::fprintf(stderr, "\n── GGUF tensor bounds ──\n"); - RUN_TEST(test_gguf_tensor_in_file_bounds); - RUN_TEST(test_gguf_bounds_error_reports_operands); - - std::fprintf(stderr, "\n══════════════════════════════════════════\n"); - std::fprintf(stderr, " Results: %d assertions, %d failures\n", - test_count, test_failures); - std::fprintf(stderr, "══════════════════════════════════════════\n"); - - if (test_failures) { - std::fprintf(stderr, "FAILED\n"); - return 1; - } - std::fprintf(stderr, "ALL PASSED\n"); - return 0; -} diff --git a/server/test/test_skip_park_guard.cpp b/server/test/test_skip_park_guard.cpp index 02d5381d4..7b98de3aa 100644 --- a/server/test/test_skip_park_guard.cpp +++ b/server/test/test_skip_park_guard.cpp @@ -1,68 +1,34 @@ // Unit tests for skip_park_allowed — pure, GPU-free. -// Build: /usr/bin/c++ -std=gnu++17 -O0 -I server/src -o /tmp/test_skip_park_guard server/test/test_skip_park_guard.cpp && /tmp/test_skip_park_guard -#include "placement/skip_park_guard.h" - -#include -static int test_failures = 0; -static int test_count = 0; - -#define TEST_ASSERT(expr) do { \ - test_count++; \ - if (!(expr)) { \ - test_failures++; \ - std::fprintf(stderr, " FAIL: %s:%d: %s\n", \ - __FILE__, __LINE__, #expr); \ - } \ -} while (0) +#include "CppUnitTestFramework.hpp" +#include "placement/skip_park_guard.h" -#define RUN_TEST(fn) do { \ - std::fprintf(stderr, " %s ...", #fn); \ - int before = test_failures; \ - fn(); \ - std::fprintf(stderr, (test_failures == before) ? " ok\n" : "\n"); \ -} while (0) +namespace { +struct SkipParkGuardFixture {}; +} static constexpr size_t GiB = 1024ull * 1024 * 1024; -// not_requested stays off regardless of card size or ctx -static void T1_not_requested_stays_off() { - TEST_ASSERT(dflash::common::skip_park_allowed(false, 24 * GiB, 32768) == false); -} - -// >=32GB card: safe at any ctx -static void T2_big_card_any_ctx() { - TEST_ASSERT(dflash::common::skip_park_allowed(true, 32 * GiB, 131072) == true); +TEST_CASE(SkipParkGuardFixture, T1_not_requested_stays_off) { + CHECK(!dflash::common::skip_park_allowed(false, 24 * GiB, 32768)); } -// <32GB card, max_ctx<=65536: proven safe -static void T3_small_card_small_ctx_allowed() { - TEST_ASSERT(dflash::common::skip_park_allowed(true, 24 * GiB, 65536) == true); +TEST_CASE(SkipParkGuardFixture, T2_big_card_any_ctx) { + CHECK(dflash::common::skip_park_allowed(true, 32 * GiB, 131072)); } -// <32GB card, max_ctx=131072: tonight's crash cell — must downgrade -static void T4_small_card_big_ctx_downgraded() { - TEST_ASSERT(dflash::common::skip_park_allowed(true, 24 * GiB, 131072) == false); +TEST_CASE(SkipParkGuardFixture, T3_small_card_small_ctx_allowed) { + CHECK(dflash::common::skip_park_allowed(true, 24 * GiB, 65536)); } -// <32GB card, max_ctx=65537: one over the proven-safe boundary -static void T5_boundary_ctx_one_over() { - TEST_ASSERT(dflash::common::skip_park_allowed(true, 24 * GiB, 65537) == false); +TEST_CASE(SkipParkGuardFixture, T4_small_card_big_ctx_downgraded) { + CHECK(!dflash::common::skip_park_allowed(true, 24 * GiB, 131072)); } -// just under 32GB: still counts as small card -static void T6_boundary_vram_just_under_32g() { - TEST_ASSERT(dflash::common::skip_park_allowed(true, 32 * GiB - 1, 131072) == false); +TEST_CASE(SkipParkGuardFixture, T5_boundary_ctx_one_over) { + CHECK(!dflash::common::skip_park_allowed(true, 24 * GiB, 65537)); } -int main() { - std::fprintf(stderr, "=== test_skip_park_guard ===\n"); - RUN_TEST(T1_not_requested_stays_off); - RUN_TEST(T2_big_card_any_ctx); - RUN_TEST(T3_small_card_small_ctx_allowed); - RUN_TEST(T4_small_card_big_ctx_downgraded); - RUN_TEST(T5_boundary_ctx_one_over); - RUN_TEST(T6_boundary_vram_just_under_32g); - std::fprintf(stderr, "\n%d tests, %d failures\n", test_count, test_failures); - return (test_failures == 0) ? 0 : 1; +TEST_CASE(SkipParkGuardFixture, T6_boundary_vram_just_under_32g) { + CHECK(!dflash::common::skip_park_allowed(true, 32 * GiB - 1, 131072)); } diff --git a/server/test/test_unit_main.cpp b/server/test/test_unit_main.cpp new file mode 100644 index 000000000..60cdb0ae6 --- /dev/null +++ b/server/test/test_unit_main.cpp @@ -0,0 +1,2 @@ +#define GENERATE_UNIT_TEST_MAIN +#include "CppUnitTestFramework.hpp"