summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-06-29 21:43:54 +0900
committertanaka takashi <isazaka@gmail.com>2026-06-30 01:23:51 +0900
commit4bc721c3bf7589b519c5730d093f704ce56e3f09 (patch)
tree38da055ab15fd70f5c92584560ea73486e5ddce0 /tests
parent4c6a831aa2a57e2bcc20aa3fc2f1b7172f5be285 (diff)
refactor: MCP層をmcpc APIベースに刷新
- 独自JSON-RPC処理をmcpcへ置き換え - 各MCPツールを個別ソースへ分離 - 共通コンテキストと登録処理を整理 - mcpc互換修正と統合テストを更新
Diffstat (limited to 'tests')
-rw-r--r--tests/CMakeLists.txt62
-rw-r--r--tests/audit.cmake42
-rw-r--r--tests/boundary_test.c275
-rw-r--r--tests/domain_test.c65
-rw-r--r--tests/fixtures/health.jsonl1
-rw-r--r--tests/fixtures/smoke-input.jsonl6
-rw-r--r--tests/fixtures/write-once.jsonl3
-rw-r--r--tests/function-coverage.md17
-rw-r--r--tests/lifecycle.cmake1
-rw-r--r--tests/smoke.cmake2
-rw-r--r--tests/storage_test.c130
-rw-r--r--tests/support/test.h48
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(&copy);
- 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(&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(&copy);
+ 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