#include "doctest.h" #include #include #include #include #include namespace { const char* TEST_PATH = "/tmp/tactility_kernel_preferences_test.properties"; struct ScratchFile { ScratchFile() { std::remove(TEST_PATH); } ~ScratchFile() { std::remove(TEST_PATH); } }; bool file_exists(const char* path) { FILE* file = std::fopen(path, "r"); if (file == nullptr) { return false; } std::fclose(file); return true; } bool is_directory(const char* path) { struct stat info {}; return stat(path, &info) == 0 && (info.st_mode & S_IFMT) == S_IFDIR; } // Writes a raw properties file directly (bypassing preferences_put_*()) so a test can exercise // a hand-crafted/corrupted payload that preferences_put_*() itself would never produce. void write_raw(const char* path, const char* content) { FILE* file = std::fopen(path, "w"); REQUIRE(file != nullptr); std::fputs(content, file); std::fclose(file); } } // namespace TEST_CASE("preferences_open_path on a missing file starts out empty, without creating it") { ScratchFile scratch; Preferences* preferences = preferences_open(TEST_PATH); CHECK_NE(preferences, nullptr); CHECK_FALSE(preferences_has_bool(preferences, "key")); CHECK_FALSE(file_exists(TEST_PATH)); preferences_close(preferences); } TEST_CASE("put_*/has_*/opt_* round-trip all four types") { ScratchFile scratch; Preferences* preferences = preferences_open(TEST_PATH); preferences_put_bool(preferences, "flag", true); preferences_put_int32(preferences, "count", -42); preferences_put_int64(preferences, "big", 123456789012345LL); preferences_put_string(preferences, "text", "hello world"); CHECK(preferences_has_bool(preferences, "flag")); bool bool_out = false; CHECK(preferences_opt_bool(preferences, "flag", &bool_out)); CHECK_EQ(bool_out, true); CHECK(preferences_has_int32(preferences, "count")); int32_t int32_out = 0; CHECK(preferences_opt_int32(preferences, "count", &int32_out)); CHECK_EQ(int32_out, -42); CHECK(preferences_has_int64(preferences, "big")); int64_t int64_out = 0; CHECK(preferences_opt_int64(preferences, "big", &int64_out)); CHECK_EQ(int64_out, 123456789012345LL); CHECK(preferences_has_string(preferences, "text")); char buffer[32]; CHECK_EQ(preferences_opt_string(preferences, "text", buffer, sizeof(buffer)), ERROR_NONE); CHECK_EQ(std::strcmp(buffer, "hello world"), 0); preferences_close(preferences); } TEST_CASE("opt_string reports ERROR_BUFFER_OVERFLOW and ERROR_NOT_FOUND") { ScratchFile scratch; Preferences* preferences = preferences_open(TEST_PATH); preferences_put_string(preferences, "text", "hello world"); char tiny[4]; CHECK_EQ(preferences_opt_string(preferences, "text", tiny, sizeof(tiny)), ERROR_BUFFER_OVERFLOW); char buffer[32]; CHECK_EQ(preferences_opt_string(preferences, "missing", buffer, sizeof(buffer)), ERROR_NOT_FOUND); preferences_close(preferences); } TEST_CASE("has_*/opt_* reject a key stored with a different type") { ScratchFile scratch; Preferences* preferences = preferences_open(TEST_PATH); preferences_put_bool(preferences, "key", true); CHECK_FALSE(preferences_has_int32(preferences, "key")); CHECK_FALSE(preferences_has_int64(preferences, "key")); CHECK_FALSE(preferences_has_string(preferences, "key")); int32_t out = 0; CHECK_FALSE(preferences_opt_int32(preferences, "key", &out)); preferences_close(preferences); } TEST_CASE("has_*/opt_* reject malformed scalar payloads instead of misparsing them") { ScratchFile scratch; write_raw(TEST_PATH, "bad_bool=b:garbage\n" "bad_bool_2=b:2\n" "trailing_junk=i32:42abc\n" "int32_overflow=i32:5000000000\n" "int64_overflow=i64:99999999999999999999\n" "empty_int=i32:\n"); Preferences* preferences = preferences_open(TEST_PATH); // "b:garbage" must not silently read back as false - has_bool()/opt_bool() must agree it's // not a valid bool at all. CHECK_FALSE(preferences_has_bool(preferences, "bad_bool")); bool bool_out = true; CHECK_FALSE(preferences_opt_bool(preferences, "bad_bool", &bool_out)); CHECK_FALSE(preferences_has_bool(preferences, "bad_bool_2")); CHECK_FALSE(preferences_opt_bool(preferences, "bad_bool_2", &bool_out)); // "42abc" must not silently parse as 42 - the full payload must be consumed. CHECK_FALSE(preferences_has_int32(preferences, "trailing_junk")); int32_t int32_out = 0; CHECK_FALSE(preferences_opt_int32(preferences, "trailing_junk", &int32_out)); // Fits in a (64-bit, on this platform) `long` but overflows int32_t - must not silently // truncate on the narrowing cast. CHECK_FALSE(preferences_has_int32(preferences, "int32_overflow")); CHECK_FALSE(preferences_opt_int32(preferences, "int32_overflow", &int32_out)); // Overflows even a 64-bit integer - strtoll() itself reports ERANGE. CHECK_FALSE(preferences_has_int64(preferences, "int64_overflow")); int64_t int64_out = 0; CHECK_FALSE(preferences_opt_int64(preferences, "int64_overflow", &int64_out)); CHECK_FALSE(preferences_has_int32(preferences, "empty_int")); CHECK_FALSE(preferences_opt_int32(preferences, "empty_int", &int32_out)); preferences_close(preferences); } TEST_CASE("a string value with embedded newlines and backslashes survives a reopen") { ScratchFile scratch; { Preferences* preferences = preferences_open(TEST_PATH); preferences_put_string(preferences, "text", "line1\nline2 with \\ backslash"); preferences_close(preferences); } { Preferences* preferences = preferences_open(TEST_PATH); char buffer[64]; CHECK_EQ(preferences_opt_string(preferences, "text", buffer, sizeof(buffer)), ERROR_NONE); CHECK_EQ(std::strcmp(buffer, "line1\nline2 with \\ backslash"), 0); preferences_close(preferences); } } TEST_CASE("preferences_close persists changes, and only close persists them") { ScratchFile scratch; Preferences* preferences = preferences_open(TEST_PATH); preferences_put_bool(preferences, "flag", true); // Not persisted yet - only preferences_close() writes to disk. CHECK_FALSE(file_exists(TEST_PATH)); preferences_close(preferences); CHECK(file_exists(TEST_PATH)); Preferences* reopened = preferences_open(TEST_PATH); CHECK(preferences_has_bool(reopened, "flag")); preferences_close(reopened); } TEST_CASE("put_* on a reopened instance is visible without reopening again, and persists") { ScratchFile scratch; Preferences* preferences = preferences_open(TEST_PATH); preferences_put_int32(preferences, "count", 1); preferences_close(preferences); Preferences* reopened = preferences_open(TEST_PATH); preferences_put_int32(reopened, "count", 2); int32_t out = 0; CHECK(preferences_opt_int32(reopened, "count", &out)); CHECK_EQ(out, 2); preferences_close(reopened); Preferences* final_instance = preferences_open(TEST_PATH); CHECK(preferences_opt_int32(final_instance, "count", &out)); CHECK_EQ(out, 2); preferences_close(final_instance); } TEST_CASE("preferences_open creates missing parent directories (recursively) and persists into them") { const char* nested_dir_a = "/tmp/tactility_kernel_preferences_test_nested"; const char* nested_dir_b = "/tmp/tactility_kernel_preferences_test_nested/a"; const char* nested_dir_c = "/tmp/tactility_kernel_preferences_test_nested/a/b"; const char* nested_path = "/tmp/tactility_kernel_preferences_test_nested/a/b/settings.properties"; std::remove(nested_path); rmdir(nested_dir_c); rmdir(nested_dir_b); rmdir(nested_dir_a); REQUIRE_FALSE(is_directory(nested_dir_a)); Preferences* preferences = preferences_open(nested_path); REQUIRE_NE(preferences, nullptr); CHECK(is_directory(nested_dir_a)); CHECK(is_directory(nested_dir_b)); CHECK(is_directory(nested_dir_c)); preferences_put_int32(preferences, "count", 7); preferences_close(preferences); CHECK(file_exists(nested_path)); Preferences* reopened = preferences_open(nested_path); REQUIRE_NE(reopened, nullptr); int32_t out = 0; CHECK(preferences_opt_int32(reopened, "count", &out)); CHECK_EQ(out, 7); preferences_close(reopened); std::remove(nested_path); rmdir(nested_dir_c); rmdir(nested_dir_b); rmdir(nested_dir_a); }