diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/CMakeLists.txt | 62 | ||||
| -rw-r--r-- | tests/audit.cmake | 42 | ||||
| -rw-r--r-- | tests/boundary_test.c | 275 | ||||
| -rw-r--r-- | tests/domain_test.c | 65 | ||||
| -rw-r--r-- | tests/fixtures/health.jsonl | 1 | ||||
| -rw-r--r-- | tests/fixtures/smoke-input.jsonl | 6 | ||||
| -rw-r--r-- | tests/fixtures/write-once.jsonl | 3 | ||||
| -rw-r--r-- | tests/function-coverage.md | 17 | ||||
| -rw-r--r-- | tests/lifecycle.cmake | 1 | ||||
| -rw-r--r-- | tests/smoke.cmake | 2 | ||||
| -rw-r--r-- | tests/storage_test.c | 130 | ||||
| -rw-r--r-- | tests/support/test.h | 48 |
12 files changed, 600 insertions, 52 deletions
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1407fc7..ad314eb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,19 +16,43 @@ add_executable(tiny-convmem-domain-test ) target_include_directories(tiny-convmem-domain-test PRIVATE "${PROJECT_SOURCE_DIR}/src") target_compile_features(tiny-convmem-domain-test PRIVATE c_std_17) +target_link_libraries(tiny-convmem-domain-test PRIVATE tiny-convmem-base) if(MSVC) target_compile_options(tiny-convmem-domain-test PRIVATE /utf-8 /W4) endif() add_test(NAME tiny-convmem-mcp.domain COMMAND tiny-convmem-domain-test) +add_executable(tiny-convmem-boundary-test + boundary_test.c + "${PROJECT_SOURCE_DIR}/src/mcp/json.c" + "${PROJECT_SOURCE_DIR}/src/storage/codec.c" +) +target_include_directories(tiny-convmem-boundary-test PRIVATE + "${PROJECT_SOURCE_DIR}/src" + "${CMAKE_CURRENT_SOURCE_DIR}" +) +target_compile_features(tiny-convmem-boundary-test PRIVATE c_std_17) +target_link_libraries(tiny-convmem-boundary-test PRIVATE tiny-convmem-sys tiny-convmem-base) +if(MSVC) + target_compile_options(tiny-convmem-boundary-test PRIVATE /utf-8 /W4) +endif() +add_test(NAME tiny-convmem-mcp.boundaries COMMAND tiny-convmem-boundary-test) +foreach(boundary_group allocator encoding codec json sys) + add_test( + NAME "tiny-convmem-mcp.${boundary_group}" + COMMAND tiny-convmem-boundary-test "${boundary_group}" + ) +endforeach() + add_executable(tiny-convmem-storage-test storage_test.c "${PROJECT_SOURCE_DIR}/src/domain/message.c" + "${PROJECT_SOURCE_DIR}/src/storage/codec.c" "${PROJECT_SOURCE_DIR}/src/storage/store.c" ) target_include_directories(tiny-convmem-storage-test PRIVATE "${PROJECT_SOURCE_DIR}/src") target_compile_features(tiny-convmem-storage-test PRIVATE c_std_17) -target_link_libraries(tiny-convmem-storage-test PRIVATE bcrypt) +target_link_libraries(tiny-convmem-storage-test PRIVATE tiny-convmem-sys) if(MSVC) target_compile_options(tiny-convmem-storage-test PRIVATE /utf-8 /W4) endif() @@ -47,3 +71,39 @@ set_tests_properties(tiny-convmem-mcp.lifecycle PROPERTIES TIMEOUT 15) add_test(NAME tiny-convmem-mcp.requires-data-dir COMMAND $<TARGET_FILE:tiny-convmem-mcp>) set_tests_properties(tiny-convmem-mcp.requires-data-dir PROPERTIES WILL_FAIL TRUE) + +find_package(Git REQUIRED) +find_program( + CLANG_FORMAT_EXECUTABLE + NAMES clang-format + HINTS "C:/Program Files/Microsoft Visual Studio/18/Community/VC/Tools/Llvm/x64/bin" +) +add_test( + NAME tiny-convmem-mcp.audit + COMMAND "${CMAKE_COMMAND}" + "-DSOURCE_DIR=${PROJECT_SOURCE_DIR}" + "-DGIT_EXECUTABLE=${GIT_EXECUTABLE}" + "-DCLANG_FORMAT_EXECUTABLE=${CLANG_FORMAT_EXECUTABLE}" + -P "${CMAKE_CURRENT_SOURCE_DIR}/audit.cmake" +) + +if(TINY_CONVMEM_ENABLE_ASAN AND MSVC) + get_filename_component(TINY_CONVMEM_MSVC_BIN_DIR "${CMAKE_C_COMPILER}" DIRECTORY) + set_property( + TEST + tiny-convmem-mcp.smoke + tiny-convmem-mcp.domain + tiny-convmem-mcp.boundaries + tiny-convmem-mcp.allocator + tiny-convmem-mcp.encoding + tiny-convmem-mcp.codec + tiny-convmem-mcp.json + tiny-convmem-mcp.sys + tiny-convmem-mcp.storage + tiny-convmem-mcp.lifecycle + tiny-convmem-mcp.requires-data-dir + tiny-convmem-mcp.audit + APPEND PROPERTY ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${TINY_CONVMEM_MSVC_BIN_DIR}" + ) +endif() diff --git a/tests/audit.cmake b/tests/audit.cmake new file mode 100644 index 0000000..9713f57 --- /dev/null +++ b/tests/audit.cmake @@ -0,0 +1,42 @@ +file(GLOB_RECURSE PROJECT_C_SOURCES + "${SOURCE_DIR}/src/*.c" + "${SOURCE_DIR}/src/*.h" +) + +foreach(source IN LISTS PROJECT_C_SOURCES) + file(READ "${source}" contents) + if(contents MATCHES "(^|[^A-Za-z0-9_])goto([^A-Za-z0-9_]|$)") + message(FATAL_ERROR "goto is forbidden in project source: ${source}") + endif() + if(contents MATCHES "#define[ \t]+static" OR contents MATCHES "#include[ \t]+\"[^\"]+\\.c\"") + message(FATAL_ERROR "test-only source visibility hack found: ${source}") + endif() + string(FIND "${source}" "/src/sys/" sys_position) + if(sys_position EQUAL -1 AND + contents MATCHES "windows\\.h|GetFullPathNameW|CreateFileW|ReadFile|WriteFile|MoveFileExW|BCryptGenRandom") + message(FATAL_ERROR "OS-specific API reference outside src/sys: ${source}") + endif() +endforeach() + +if(NOT CLANG_FORMAT_EXECUTABLE STREQUAL "") + execute_process( + COMMAND "${CLANG_FORMAT_EXECUTABLE}" --dry-run --Werror ${PROJECT_C_SOURCES} + RESULT_VARIABLE format_result + OUTPUT_VARIABLE format_output + ERROR_VARIABLE format_error + ) + if(NOT format_result EQUAL 0) + message(FATAL_ERROR "clang-format check failed:\n${format_output}${format_error}") + endif() +endif() + +execute_process( + COMMAND "${GIT_EXECUTABLE}" diff --check + WORKING_DIRECTORY "${SOURCE_DIR}" + RESULT_VARIABLE diff_result + OUTPUT_VARIABLE diff_output + ERROR_VARIABLE diff_error +) +if(NOT diff_result EQUAL 0) + message(FATAL_ERROR "git diff --check failed:\n${diff_output}${diff_error}") +endif() diff --git a/tests/boundary_test.c b/tests/boundary_test.c new file mode 100644 index 0000000..452e2ee --- /dev/null +++ b/tests/boundary_test.c @@ -0,0 +1,275 @@ +#include <limits.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +#include "support/test.h" + +#include "base/allocator.h" +#include "mcp/json.h" +#include "storage/codec.h" +#include "sys/encoding.h" +#include "sys/system.h" + +static void +test_allocator_boundaries(tiny_test_state_t *state) +{ + void *first; + void *resized; + size_t baseline = tiny_allocator_outstanding(); + TINY_TEST_CASE(state, "allocator overflow failure positions and balance"); + TINY_EXPECT(state, tiny_calloc(SIZE_MAX, 2) == NULL); + tiny_allocator_fail_after(0); + TINY_EXPECT(state, tiny_alloc(1) == NULL); + TINY_EXPECT_EQ_U64(state, 1, tiny_allocator_call_count()); + tiny_allocator_reset(); + first = tiny_alloc(1); + TINY_EXPECT(state, first != NULL); + tiny_allocator_fail_after(0); + resized = tiny_realloc(first, 2); + TINY_EXPECT(state, resized == NULL); + tiny_allocator_reset(); + tiny_free(first); + TINY_EXPECT_EQ_U64(state, baseline, tiny_allocator_outstanding()); + first = tiny_alloc(1); + TINY_EXPECT(state, tiny_realloc(first, 0) == NULL); + TINY_EXPECT_EQ_U64(state, baseline, tiny_allocator_outstanding()); + tiny_free(NULL); +} + +static void +test_encoding_valid(tiny_test_state_t *state) +{ + static const struct vector + { + const char *utf8; + size_t utf8_length; + uint16_t utf16[3]; + size_t utf16_length; + } vectors[] = { + { "", 0, { 0 }, 0 }, + { "\x7f", 1, { 0x007f }, 1 }, + { "\xc2\x80", 2, { 0x0080 }, 1 }, + { "\xdf\xbf", 2, { 0x07ff }, 1 }, + { "\xe0\xa0\x80", 3, { 0x0800 }, 1 }, + { "\xed\x9f\xbf", 3, { 0xd7ff }, 1 }, + { "\xee\x80\x80", 3, { 0xe000 }, 1 }, + { "\xef\xbf\xbf", 3, { 0xffff }, 1 }, + { "\xf0\x90\x80\x80", 4, { 0xd800, 0xdc00 }, 2 }, + { "\xf4\x8f\xbf\xbf", 4, { 0xdbff, 0xdfff }, 2 }, + }; + size_t index; + TINY_TEST_CASE(state, "encoding valid scalar boundaries"); + for (index = 0; index < sizeof(vectors) / sizeof(vectors[0]); ++index) + { + uint16_t *wide = NULL; + char *roundtrip = NULL; + size_t wide_length = SIZE_MAX, roundtrip_length = SIZE_MAX; + TINY_EXPECT( + state, + tiny_utf8_to_utf16(vectors[index].utf8, vectors[index].utf8_length, &wide, &wide_length) + == TINY_ENCODING_OK); + TINY_EXPECT_EQ_U64(state, vectors[index].utf16_length, wide_length); + TINY_EXPECT_BYTES(state, vectors[index].utf16, wide, wide_length * sizeof(*wide)); + TINY_EXPECT(state, + tiny_utf16_to_utf8(wide, wide_length, &roundtrip, &roundtrip_length) + == TINY_ENCODING_OK); + TINY_EXPECT_EQ_U64(state, vectors[index].utf8_length, roundtrip_length); + TINY_EXPECT_BYTES(state, vectors[index].utf8, roundtrip, roundtrip_length); + free(roundtrip); + free(wide); + } +} + +static void +test_encoding_invalid(tiny_test_state_t *state) +{ + static const unsigned char invalid[][4] = { + { 0x80, 0, 0, 0 }, { 0xc0, 0x80, 0, 0 }, { 0xe0, 0x80, 0x80, 0 }, + { 0xed, 0xa0, 0x80, 0 }, { 0xf0, 0x80, 0x80, 0x80 }, { 0xf4, 0x90, 0x80, 0x80 }, + }; + static const size_t lengths[] = { 1, 2, 3, 3, 4, 4 }; + size_t index; + TINY_TEST_CASE(state, "encoding invalid and allocation failure"); + for (index = 0; index < sizeof(lengths) / sizeof(lengths[0]); ++index) + { + uint16_t *output = (uint16_t *)(uintptr_t)1; + size_t length = SIZE_MAX; + TINY_EXPECT(state, + tiny_utf8_to_utf16((const char *)invalid[index], lengths[index], &output, &length) + == TINY_ENCODING_INVALID); + TINY_EXPECT(state, output == NULL && length == 0); + } + { + uint16_t high[] = { 0xd800 }; + uint16_t low[] = { 0xdc00 }; + char *output = NULL; + size_t length = 0; + TINY_EXPECT(state, tiny_utf16_to_utf8(high, 1, &output, &length) == TINY_ENCODING_INVALID); + TINY_EXPECT(state, tiny_utf16_to_utf8(low, 1, &output, &length) == TINY_ENCODING_INVALID); + } + TINY_EXPECT(state, tiny_utf8_to_utf16(NULL, 1, NULL, NULL) == TINY_ENCODING_INVALID); + { + uint16_t *output = NULL; + size_t length = 0; + tiny_allocator_fail_after(0); + TINY_EXPECT(state, tiny_utf8_to_utf16("a", 1, &output, &length) == TINY_ENCODING_NO_MEMORY); + TINY_EXPECT(state, output == NULL && length == 0); + tiny_allocator_reset(); + } +} + +static void +test_codec_alignment(tiny_test_state_t *state) +{ + unsigned char storage[32]; + size_t offset; + TINY_TEST_CASE(state, "codec every alignment offset"); + for (offset = 0; offset < 16; ++offset) + { + unsigned char *position = storage + offset; + memset(storage, 0, sizeof(storage)); + tiny_codec_put_u32(position, UINT32_MAX); + TINY_EXPECT_EQ_U64(state, UINT32_MAX, tiny_codec_get_u32(position)); + tiny_codec_put_u64(position, UINT64_MAX); + TINY_EXPECT_EQ_U64(state, UINT64_MAX, tiny_codec_get_u64(position)); + } + TINY_EXPECT_EQ_U64(state, 0, tiny_codec_crc32(NULL, 1)); + TINY_EXPECT_EQ_U64(state, 0, tiny_codec_crc32(NULL, 0)); + tiny_codec_put_u32(NULL, 1); + tiny_codec_put_u64(NULL, 1); + TINY_EXPECT_EQ_U64(state, 0, tiny_codec_get_u32(NULL)); + TINY_EXPECT_EQ_U64(state, 0, tiny_codec_get_u64(NULL)); + TINY_EXPECT_EQ_U64(state, 0xcbf43926U, tiny_codec_crc32((const unsigned char *)"123456789", 9)); +} + +static void +test_json_boundaries(tiny_test_state_t *state) +{ + tiny_json_buffer_t buffer; + static const char controls[] = "\b\f\n\r\t\x01"; + TINY_TEST_CASE(state, "json growth escaping integer and OOM"); + tiny_json_init(&buffer); + tiny_json_init(NULL); + tiny_json_free(NULL); + TINY_EXPECT(state, !tiny_json_raw(NULL, "x")); + TINY_EXPECT(state, !tiny_json_raw(&buffer, NULL)); + TINY_EXPECT(state, !tiny_json_raw_n(&buffer, NULL, 1)); + TINY_EXPECT(state, !tiny_json_string(&buffer, NULL)); + TINY_EXPECT(state, tiny_json_raw_n(&buffer, "", 0)); + TINY_EXPECT(state, tiny_json_string(&buffer, controls)); + TINY_EXPECT(state, strstr(buffer.data, "\\b\\f\\n\\r\\t\\u0001") != NULL); + TINY_EXPECT(state, tiny_json_i64(&buffer, LLONG_MIN)); + TINY_EXPECT(state, tiny_json_u64(&buffer, ULLONG_MAX)); + tiny_json_free(&buffer); + tiny_allocator_fail_after(0); + tiny_json_init(&buffer); + TINY_EXPECT(state, !tiny_json_raw(&buffer, "allocation")); + TINY_EXPECT(state, buffer.failed); + tiny_json_free(&buffer); + tiny_allocator_reset(); +} + +static void +test_sys_boundaries(tiny_test_state_t *state) +{ + char *joined; + char *directory; + char *path; + tiny_sys_file_t file; + uint32_t actual = UINT32_MAX; + uint64_t size = UINT64_MAX; + static const unsigned char bytes[] = { 1, 2, 3, 4 }; + unsigned char received[sizeof(bytes)] = { 0 }; + TINY_TEST_CASE(state, "sys public boundary values"); + tiny_sys_test_reset(); + TINY_EXPECT(state, tiny_sys_temp_directory_create(NULL) == NULL); + TINY_EXPECT(state, tiny_sys_path_join(NULL, "name") == NULL); + TINY_EXPECT(state, tiny_sys_path_join("directory", NULL) == NULL); + joined = tiny_sys_path_join("", "name"); + TINY_EXPECT(state, joined != NULL && strcmp(joined, "name") == 0); + free(joined); + tiny_sys_file_init(NULL); + TINY_EXPECT(state, !tiny_sys_file_valid(NULL)); + tiny_sys_file_init(&file); + TINY_EXPECT(state, !tiny_sys_file_valid(&file)); + TINY_EXPECT(state, tiny_sys_file_open(NULL, "unused", TINY_SYS_FILE_READ) == TINY_SYS_ERROR); + TINY_EXPECT(state, + tiny_sys_file_open(&file, NULL, TINY_SYS_FILE_READ) == TINY_SYS_ERROR + && !tiny_sys_file_valid(&file)); + TINY_EXPECT(state, !tiny_sys_file_seek(&file, 0, (tiny_sys_seek_origin_t)-1)); + TINY_EXPECT(state, !tiny_sys_file_size(&file, &size)); + TINY_EXPECT_EQ_U64(state, 0, size); + TINY_EXPECT(state, !tiny_sys_file_read(&file, received, 1, &actual)); + TINY_EXPECT_EQ_U64(state, 0, actual); + + directory = tiny_sys_temp_directory_create("tcm"); + TINY_EXPECT(state, directory != NULL); + path = directory == NULL ? NULL : tiny_sys_path_join(directory, "fault.bin"); + TINY_EXPECT(state, path != NULL); + if (path != NULL) + { + TINY_EXPECT(state, tiny_sys_file_open(&file, path, TINY_SYS_FILE_WRITE_NEW) == TINY_SYS_OK); + tiny_sys_test_max_transfer(1); + TINY_EXPECT(state, tiny_sys_file_write(&file, bytes, sizeof(bytes), &actual)); + TINY_EXPECT_EQ_U64(state, 1, actual); + TINY_EXPECT(state, tiny_sys_test_fail_after(TINY_SYS_TEST_FLUSH, 1)); + TINY_EXPECT(state, !tiny_sys_file_flush(&file)); + tiny_sys_test_reset(); + TINY_EXPECT(state, tiny_sys_file_flush(&file)); + tiny_sys_file_close(&file); + + TINY_EXPECT(state, tiny_sys_test_fail_after(TINY_SYS_TEST_OPEN, 1)); + TINY_EXPECT(state, + tiny_sys_file_open(&file, path, TINY_SYS_FILE_READ) == TINY_SYS_ERROR + && !tiny_sys_file_valid(&file)); + tiny_sys_test_reset(); + TINY_EXPECT(state, tiny_sys_file_open(&file, path, TINY_SYS_FILE_READ) == TINY_SYS_OK); + TINY_EXPECT(state, tiny_sys_test_fail_after(TINY_SYS_TEST_READ, 1)); + actual = UINT32_MAX; + TINY_EXPECT(state, !tiny_sys_file_read(&file, received, sizeof(received), &actual)); + TINY_EXPECT_EQ_U64(state, 0, actual); + tiny_sys_test_reset(); + tiny_sys_file_close(&file); + TINY_EXPECT(state, tiny_sys_file_remove(path)); + } + free(path); + if (directory != NULL) + { + TINY_EXPECT(state, tiny_sys_directory_remove(directory)); + } + free(directory); + tiny_sys_test_reset(); +} + +int +main(int argc, char **argv) +{ + tiny_test_state_t state = { "initial", 0, 0 }; + const char *group = argc > 1 ? argv[1] : "all"; + tiny_allocator_reset(); + if (strcmp(group, "all") == 0 || strcmp(group, "allocator") == 0) + { + test_allocator_boundaries(&state); + } + if (strcmp(group, "all") == 0 || strcmp(group, "encoding") == 0) + { + test_encoding_valid(&state); + test_encoding_invalid(&state); + } + if (strcmp(group, "all") == 0 || strcmp(group, "codec") == 0) + { + test_codec_alignment(&state); + } + if (strcmp(group, "all") == 0 || strcmp(group, "json") == 0) + { + test_json_boundaries(&state); + } + if (strcmp(group, "all") == 0 || strcmp(group, "sys") == 0) + { + test_sys_boundaries(&state); + } + TINY_TEST_CASE(&state, "allocator leak balance"); + TINY_EXPECT_EQ_U64(&state, 0, tiny_allocator_outstanding()); + return tiny_test_finish(&state); +} diff --git a/tests/domain_test.c b/tests/domain_test.c index d50e4bd..4553657 100644 --- a/tests/domain_test.c +++ b/tests/domain_test.c @@ -5,6 +5,8 @@ #include <stdlib.h> #include <string.h> +#include "base/allocator.h" + static int failures; #define CHECK(value) \ do \ @@ -17,6 +19,18 @@ static int failures; } \ while (0) +static char * +duplicate_string(const char *value) +{ + const size_t length = strlen(value); + char *copy = malloc(length + 1); + if (copy != NULL) + { + memcpy(copy, value, length + 1); + } + return copy; +} + int main(void) { @@ -27,32 +41,44 @@ main(void) tiny_message_t original, copy; tiny_search_index_t *search; size_t *candidates = NULL, candidate_count = 0; + size_t failure_index; + + tiny_allocator_reset(); CHECK(tiny_role_parse("user", &role) && role == TINY_ROLE_USER); + CHECK(!tiny_role_parse(NULL, &role)); + CHECK(!tiny_role_parse("user", NULL)); CHECK(!tiny_role_parse("tool", &role)); CHECK(tiny_tier_parse("discard", &tier) && tier == TINY_TIER_DISCARD); CHECK(!tiny_tier_parse("archive", &tier)); + CHECK(!tiny_tier_parse(NULL, &tier)); + CHECK(!tiny_tier_parse("short", NULL)); + CHECK(tiny_utf8_valid(NULL, 0)); + CHECK(!tiny_utf8_valid(NULL, 1)); CHECK(tiny_utf8_valid(valid_utf8, strlen(valid_utf8))); CHECK(!tiny_utf8_valid(invalid_utf8, sizeof(invalid_utf8) - 1)); CHECK(tiny_message_id_valid("01ARZ3NDEKTSV4RRFFQ69G5FAV")); CHECK(!tiny_message_id_valid("81ARZ3NDEKTSV4RRFFQ69G5FAV")); CHECK(!tiny_message_id_valid("01ARZ3NDEKTSV4RRFFQ69G5FAI")); + CHECK(!tiny_message_id_valid(NULL)); + CHECK(!tiny_message_id_valid("01ARZ3NDEKTSV4RRFFQ69G5FA")); + CHECK(!tiny_message_id_valid("01ARZ3NDEKTSV4RRFFQ69G5FAV0")); tiny_message_init(&original); tiny_message_init(©); - original.session_id = _strdup("session"); - original.content = _strdup("content"); + original.session_id = duplicate_string("session"); + original.content = duplicate_string("content"); original.role = TINY_ROLE_ASSISTANT; - strcpy_s(original.message_id, sizeof(original.message_id), "01ARZ3NDEKTSV4RRFFQ69G5FAV"); + snprintf(original.message_id, sizeof(original.message_id), "%s", "01ARZ3NDEKTSV4RRFFQ69G5FAV"); CHECK(tiny_message_copy(©, &original) == 0); CHECK(copy.session_id != original.session_id); CHECK(strcmp(copy.content, "content") == 0); search = tiny_search_index_create(); CHECK(search != NULL); free(original.content); - original.content = _strdup("Curry and coffee"); + original.content = duplicate_string("Curry and coffee"); original.metadata.tags = calloc(1, sizeof(char *)); - original.metadata.tags[0] = _strdup("設計メモ"); + original.metadata.tags[0] = duplicate_string("設計メモ"); original.metadata.tag_count = 1; CHECK(tiny_search_index_add(search, &original, 7)); CHECK(tiny_search_index_candidates(search, "CURRY", &candidates, &candidate_count)); @@ -64,8 +90,37 @@ main(void) CHECK(candidate_count == 1 && candidates[0] == 7); CHECK(tiny_search_score("設計", &original) > 0.0); free(candidates); + + CHECK(!tiny_search_index_add(NULL, &original, 0)); + CHECK(!tiny_search_index_add(search, NULL, 0)); + CHECK(!tiny_search_index_candidates(NULL, "x", &candidates, &candidate_count)); + CHECK(!tiny_search_index_candidates(search, NULL, &candidates, &candidate_count)); + CHECK(!tiny_search_index_candidates(search, "x", NULL, &candidate_count)); + CHECK(tiny_search_score(NULL, &original) == 0.0); + CHECK(tiny_search_score("x", NULL) == 0.0); + + original.metadata.tool_call_id = duplicate_string("tool"); + original.metadata.tags = realloc(original.metadata.tags, 2 * sizeof(char *)); + original.metadata.tags[1] = duplicate_string("second"); + original.metadata.tag_count = 2; + for (failure_index = 0; failure_index < 16; ++failure_index) + { + const size_t baseline = tiny_allocator_outstanding(); + tiny_message_t swept; + tiny_allocator_fail_after(failure_index); + if (tiny_message_copy(&swept, &original) == 0) + { + tiny_message_free(&swept); + CHECK(tiny_allocator_outstanding() == baseline); + break; + } + CHECK(tiny_allocator_outstanding() == baseline); + } + CHECK(failure_index < 16); + tiny_allocator_reset(); tiny_search_index_free(search); tiny_message_free(&original); tiny_message_free(©); + CHECK(tiny_allocator_outstanding() == 0); return failures == 0 ? 0 : 1; } diff --git a/tests/fixtures/health.jsonl b/tests/fixtures/health.jsonl index 8d1a19f..95642eb 100644 --- a/tests/fixtures/health.jsonl +++ b/tests/fixtures/health.jsonl @@ -1,3 +1,4 @@ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"lifecycle","version":"1"}}} +{"jsonrpc":"2.0","method":"notifications/initialized","params":{}} {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_messages","arguments":{"session_id":"lifecycle"}}} {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_system_health","arguments":{}}} diff --git a/tests/fixtures/smoke-input.jsonl b/tests/fixtures/smoke-input.jsonl index 64bf18f..c746dac 100644 --- a/tests/fixtures/smoke-input.jsonl +++ b/tests/fixtures/smoke-input.jsonl @@ -1,9 +1,9 @@ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"tiny-convmem-smoke","version":"0.1.0"}}} {"jsonrpc":"2.0","method":"notifications/initialized","params":{}} {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}} -{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"write_message","arguments":{"session_id":"smoke","role":"user","content":"hello durable memory","metadata":{"tags":["hello","memory"],"memory_tier":"short"}}}} +{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"write_message","arguments":{"session_id":"smoke","role":"user","content":"hello durable memory","metadata_json":"{\"tags\":[\"hello\",\"memory\"],\"memory_tier\":\"short\"}"}}} {"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"read_messages","arguments":{"session_id":"smoke"}}} -{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"search_messages","arguments":{"query":"durable","limit":10}}} +{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"search_messages","arguments":{"query":"durable","session_id":"","limit":10}}} {"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"rebuild_index","arguments":{}}} {"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"get_system_health","arguments":{}}} -{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"write_message","arguments":{"session_id":"smoke","role":"user","content":"must not be stored","unexpected":true}}} +{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"write_message","arguments":{"session_id":"smoke","role":"user","content":"must not be stored","metadata_json":"{\"unexpected\":true}"}}} diff --git a/tests/fixtures/write-once.jsonl b/tests/fixtures/write-once.jsonl index 0378968..1ad02ae 100644 --- a/tests/fixtures/write-once.jsonl +++ b/tests/fixtures/write-once.jsonl @@ -1,2 +1,3 @@ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"lifecycle","version":"1"}}} -{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"write_message","arguments":{"session_id":"lifecycle","role":"user","content":"secret-to-remove","metadata":{"tags":["秘密","remove"],"memory_tier":"long"}}}} +{"jsonrpc":"2.0","method":"notifications/initialized","params":{}} +{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"write_message","arguments":{"session_id":"lifecycle","role":"user","content":"secret-to-remove","metadata_json":"{\"tags\":[\"秘密\",\"remove\"],\"memory_tier\":\"long\"}"}}} diff --git a/tests/function-coverage.md b/tests/function-coverage.md new file mode 100644 index 0000000..66c2e4a --- /dev/null +++ b/tests/function-coverage.md @@ -0,0 +1,17 @@ +# Function coverage map + +`third_party/` is excluded. Static adapters are covered through their owning public API; pure +byte/encoding logic is exposed through internal headers and tested directly. + +| Source | Functions | Test contract | +|---|---|---| +| `base/allocator.c` | `should_fail`, `tiny_alloc`, `tiny_calloc`, `tiny_realloc`, `tiny_free`, `tiny_allocator_reset`, `tiny_allocator_fail_after`, `tiny_allocator_call_count`, `tiny_allocator_outstanding` | `boundaries`: zero/overflow, each allocation primitive, injected failure, balance | +| `sys/encoding.c` | `utf8_codepoint`, `tiny_utf8_to_utf16`, `tiny_utf16_to_utf8` | `boundaries`: all UTF widths, scalar edges, surrogates, overlong/truncated/invalid input, OOM | +| `storage/codec.c` | `tiny_codec_put_u32`, `tiny_codec_put_u64`, `tiny_codec_get_u32`, `tiny_codec_get_u64`, `tiny_codec_crc32` | `boundaries`: offsets 0–15, integer limits, empty/NULL and known CRC | +| `sys/system.c` | test-hook helpers; UTF adapters; `native_file`; argument, path, directory, file, clock and random APIs | `boundaries`, `storage`, `smoke`: NULL/invalid handle, Unicode paths, short I/O, operation failure, cleanup | +| `domain/message.c` | `duplicate_string`, `copy_failed`, all `tiny_message_*`, role/tier parse/name, `tiny_utf8_valid`, `tiny_message_id_valid` | `domain`: enum failure, UTF-8 invalidity, ULID alphabet/length, deep copy and ownership | +| `domain/search.c` | `decode_codepoints`, `gram_hash`, `find_bucket`, `add_gram`, `index_text`, all `tiny_search_*` | `domain`: ASCII case folding, Unicode grams/tags, candidates, scoring and ownership | +| `storage/store.c` | error/I/O helpers, ULID, entry growth/search, record codec, replay/snapshot/recovery, all `tiny_store_*` | `storage`, `lifecycle`: lock, append/reopen, corruption, rebuild rollback, Unicode path | +| `mcp/json.c` | `reserve`, all `tiny_json_*` | `boundaries`: empty append, growth, control escaping, signed/unsigned integer limits, OOM | +| `mcp/server.c` | line/JSON/parser/result/search helpers, `handle_call`, `tiny_convmem_server_run` | `smoke`, `lifecycle`: initialize, list/call, malformed/EOF behavior, persistence | +| `app/main.c` | `usage`, `run`, `main` | `requires-data-dir`, `smoke`: missing CLI argument, normal startup and EOF | diff --git a/tests/lifecycle.cmake b/tests/lifecycle.cmake index 8d2f97d..e0b1149 100644 --- a/tests/lifecycle.cmake +++ b/tests/lifecycle.cmake @@ -24,6 +24,7 @@ endif() set(delete_input "${DATA_DIR}/delete.jsonl") file(WRITE "${delete_input}" "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"lifecycle\",\"version\":\"1\"}}}\n" + "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\",\"params\":{}}\n" "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"delete_message\",\"arguments\":{\"message_id\":\"${message_id}\",\"session_id\":\"lifecycle\"}}}\n" "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"read_messages\",\"arguments\":{\"session_id\":\"lifecycle\"}}}\n" "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/call\",\"params\":{\"name\":\"rebuild_index\",\"arguments\":{}}}\n" diff --git a/tests/smoke.cmake b/tests/smoke.cmake index 2481297..3992b6b 100644 --- a/tests/smoke.cmake +++ b/tests/smoke.cmake @@ -35,7 +35,7 @@ foreach(expected "\"name\":\"delete_message\"" "\"name\":\"rebuild_index\"" "\"name\":\"get_system_health\"" - "\"required\":[\"session_id\",\"role\",\"content\"]" + "\"metadata_json\"" "\"type\":\"text\"" "\\\"status\\\":\\\"OK\\\"" "\\\"code\\\":\\\"VALIDATION_ERROR\\\"" diff --git a/tests/storage_test.c b/tests/storage_test.c index bac0f0b..3217095 100644 --- a/tests/storage_test.c +++ b/tests/storage_test.c @@ -1,7 +1,5 @@ -#define WIN32_LEAN_AND_MEAN -#include <windows.h> - #include "storage/store.h" +#include "sys/system.h" #include <stdio.h> #include <stdlib.h> @@ -19,24 +17,69 @@ static int failures; } \ while (0) +static char * +duplicate_string(const char *value) +{ + const size_t length = strlen(value); + char *copy = malloc(length + 1); + if (copy != NULL) + { + memcpy(copy, value, length + 1); + } + return copy; +} + int -wmain(void) +main(void) { - wchar_t temporary[MAX_PATH], directory[MAX_PATH], log_path[MAX_PATH]; + char *temporary_root = tiny_sys_temp_directory_create("tcm"); + char *directory; + char *log_path; + char *temporary_path; + char *lock_path; + char *snapshot_path; tiny_store_t *store = NULL, *second = NULL; tiny_message_t input, first, second_message; tiny_store_status_t status; - HANDLE log; - HANDLE blocker; + tiny_sys_file_t log; tiny_rebuild_report_t report; - LARGE_INTEGER position; unsigned char byte; - DWORD count; + uint32_t count; + tiny_store_health_t health; + size_t operation; + static const tiny_sys_test_operation_t rebuild_failures[] = { TINY_SYS_TEST_OPEN, + TINY_SYS_TEST_RANDOM, + TINY_SYS_TEST_WRITE, + TINY_SYS_TEST_FLUSH, + TINY_SYS_TEST_REPLACE }; + + CHECK(tiny_store_open(NULL, "unused") == TINY_STORE_IO_ERROR); + store = (tiny_store_t *)(uintptr_t)1; + CHECK(tiny_store_open(&store, NULL) == TINY_STORE_IO_ERROR && store == NULL); + CHECK(tiny_store_entry_count(NULL) == 0); + CHECK(tiny_store_entry(NULL, 0) == NULL); + memset(&health, 0xff, sizeof(health)); + tiny_store_health(NULL, &health); + CHECK(health.active_messages == 0 && health.log_size == 0); + + CHECK(temporary_root != NULL); + if (temporary_root == NULL) + { + return 1; + } + directory = tiny_sys_path_join(temporary_root, "日本語-memory"); + CHECK(directory != NULL && tiny_sys_directory_create(directory) == TINY_SYS_OK); + if (directory == NULL) + { + free(temporary_root); + return 1; + } + log_path = tiny_sys_path_join(directory, "active.log"); + temporary_path = tiny_sys_path_join(directory, "active.tmp"); + lock_path = tiny_sys_path_join(directory, "store.lock"); + snapshot_path = tiny_sys_path_join(directory, "snapshot.bin"); + CHECK(log_path != NULL && temporary_path != NULL && lock_path != NULL && snapshot_path != NULL); - CHECK(GetTempPathW(MAX_PATH, temporary) > 0); - CHECK(GetTempFileNameW(temporary, L"tcm", 0, directory) > 0); - DeleteFileW(directory); - CHECK(CreateDirectoryW(directory, NULL) != 0); status = tiny_store_open(&store, directory); CHECK(status == TINY_STORE_OK); CHECK(tiny_store_open(&second, directory) == TINY_STORE_BUSY); @@ -44,12 +87,12 @@ wmain(void) tiny_message_init(&input); tiny_message_init(&first); tiny_message_init(&second_message); - input.session_id = _strdup("unit"); - input.content = _strdup("first"); + input.session_id = duplicate_string("unit"); + input.content = duplicate_string("first"); input.role = TINY_ROLE_USER; CHECK(tiny_store_write(store, &input, &first) == TINY_STORE_OK); free(input.content); - input.content = _strdup("second"); + input.content = duplicate_string("second"); CHECK(tiny_store_write(store, &input, &second_message) == TINY_STORE_OK); CHECK(strcmp(first.message_id, second_message.message_id) < 0); tiny_message_free(&input); @@ -60,38 +103,43 @@ wmain(void) CHECK(tiny_store_open(&store, directory) == TINY_STORE_OK); CHECK(tiny_store_entry_count(store) == 2); - swprintf_s(log_path, MAX_PATH, L"%s\\active.tmp", directory); - blocker = CreateFileW( - log_path, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); - CHECK(blocker != INVALID_HANDLE_VALUE); + for (operation = 0; operation < sizeof(rebuild_failures) / sizeof(rebuild_failures[0]); + ++operation) + { + tiny_sys_test_reset(); + CHECK(tiny_sys_test_fail_after(rebuild_failures[operation], 1)); + CHECK(tiny_store_rebuild(store, &report) != TINY_STORE_OK); + CHECK(tiny_store_entry_count(store) == 2); + tiny_sys_test_reset(); + (void)tiny_sys_file_remove(temporary_path); + } + CHECK(tiny_sys_directory_create(temporary_path) == TINY_SYS_OK); CHECK(tiny_store_rebuild(store, &report) == TINY_STORE_IO_ERROR); CHECK(tiny_store_entry_count(store) == 2); - CloseHandle(blocker); - DeleteFileW(log_path); + CHECK(tiny_sys_directory_remove(temporary_path)); tiny_store_close(store); store = NULL; - swprintf_s(log_path, MAX_PATH, L"%s\\active.log", directory); - log = CreateFileW( - log_path, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); - CHECK(log != INVALID_HANDLE_VALUE); - position.QuadPart = -1; - CHECK(SetFilePointerEx(log, position, NULL, FILE_END) != 0); - CHECK(ReadFile(log, &byte, 1, &count, NULL) != 0 && count == 1); + CHECK(tiny_sys_file_open(&log, log_path, TINY_SYS_FILE_READ_WRITE) == TINY_SYS_OK); + CHECK(tiny_sys_file_seek(&log, -1, TINY_SYS_SEEK_END)); + CHECK(tiny_sys_file_read(&log, &byte, 1, &count) && count == 1); byte ^= 0x5a; - position.QuadPart = -1; - CHECK(SetFilePointerEx(log, position, NULL, FILE_CURRENT) != 0); - CHECK(WriteFile(log, &byte, 1, &count, NULL) != 0 && count == 1); - CloseHandle(log); + CHECK(tiny_sys_file_seek(&log, -1, TINY_SYS_SEEK_CURRENT)); + CHECK(tiny_sys_file_write(&log, &byte, 1, &count) && count == 1); + tiny_sys_file_close(&log); CHECK(tiny_store_open(&store, directory) == TINY_STORE_CORRUPT); - DeleteFileW(log_path); - swprintf_s(log_path, MAX_PATH, L"%s\\active.tmp", directory); - DeleteFileW(log_path); - swprintf_s(log_path, MAX_PATH, L"%s\\store.lock", directory); - DeleteFileW(log_path); - swprintf_s(log_path, MAX_PATH, L"%s\\snapshot.bin", directory); - DeleteFileW(log_path); - RemoveDirectoryW(directory); + (void)tiny_sys_file_remove(log_path); + (void)tiny_sys_file_remove(temporary_path); + (void)tiny_sys_file_remove(lock_path); + (void)tiny_sys_file_remove(snapshot_path); + (void)tiny_sys_directory_remove(directory); + (void)tiny_sys_directory_remove(temporary_root); + free(snapshot_path); + free(lock_path); + free(temporary_path); + free(log_path); + free(directory); + free(temporary_root); return failures == 0 ? 0 : 1; } diff --git a/tests/support/test.h b/tests/support/test.h new file mode 100644 index 0000000..5de3864 --- /dev/null +++ b/tests/support/test.h @@ -0,0 +1,48 @@ +#ifndef TINY_CONVMEM_TEST_SUPPORT_H +#define TINY_CONVMEM_TEST_SUPPORT_H + +#include <stdio.h> +#include <string.h> + +typedef struct tiny_test_state +{ + const char *case_name; + unsigned checks; + unsigned failures; +} tiny_test_state_t; + +#define TINY_TEST_CASE(state, name) ((state)->case_name = (name)) +#define TINY_EXPECT(state, expression) \ + do \ + { \ + (state)->checks++; \ + if (!(expression)) \ + { \ + fprintf(stderr, \ + "%s:%d [%s] expected %s\n", \ + __FILE__, \ + __LINE__, \ + (state)->case_name, \ + #expression); \ + (state)->failures++; \ + } \ + } \ + while (0) +#define TINY_EXPECT_EQ_U64(state, expected, actual) \ + TINY_EXPECT((state), (unsigned long long)(expected) == (unsigned long long)(actual)) +#define TINY_EXPECT_BYTES(state, expected, actual, length) \ + TINY_EXPECT((state), memcmp((expected), (actual), (length)) == 0) + +static inline int +tiny_test_finish(const tiny_test_state_t *state) +{ + if (state->failures == 0) + { + printf("%u checks passed\n", state->checks); + return 0; + } + fprintf(stderr, "%u of %u checks failed\n", state->failures, state->checks); + return 1; +} + +#endif |
