Files
blackbird/memdb/test_libmemdb.cpp
T
qing 26de527a09 feat(memdb×libmemdb): memdb 服务化落地——RPC 服务端 + 构建/测试/部署闭环
- libmemdb: 补全 flush() 惰性 TTL 驱逐;修复 explicit 误用/头文件缺失/
  时间戳碰撞误删;size()/get_db() 前 flush 消除脏读;三处双重哈希查找
  合并;index_keys 去冗余字段;update(refresh_ttl) 改原地重索引,
  免 remove+insert 的拷贝与重复 flush
- memdb: 重写 RPC 服务端(libmsg RpcServer, :8011, 9 路由, mutex 保护);
  补全 Makefile/_set.sh/_run.sh 与 --instance/--log-level CLI;
  SIGINT/SIGTERM 优雅退出;注册 services.yaml
- 测试: libmemdb 单元测试 22 例 + memdb RPC 集成测试 12 场景(含并发)
- 清理: 删除误合入的 db2/create_sqlite3.sql 旧版(scripts/ 下为权威版)
2026-08-15 09:27:12 +08:00

354 lines
13 KiB
C++

/*
* test_libmemdb.cpp — libmemdb 模板类单元测试
*
* 编译: g++ -std=c++20 -O0 -g -Wall -Werror -I ../libmemdb/inc -o test_libmemdb test_libmemdb.cpp
* 运行: ./test_libmemdb
*/
#include <us/memdb.hpp>
#include <cassert>
#include <chrono>
#include <iostream>
#include <string>
#include <thread>
#include <vector>
static int g_passed = 0;
static int g_failed = 0;
#define TEST(name) \
do { \
std::cout << " " << (name) << " ... "; \
} while (0)
#define PASS() \
do { \
std::cout << "PASS" << std::endl; \
++g_passed; \
} while (0)
#define FAIL(msg) \
do { \
std::cout << "FAIL: " << (msg) << std::endl; \
++g_failed; \
} while (0)
#define CHECK(cond, msg) \
do { \
if (!(cond)) { FAIL(msg); return; } \
} while (0)
// ── insert ──────────────────────────────────────────────────────────────
static void test_insert_basic() {
TEST("insert basic");
Memdb<std::string, std::string> db;
CHECK(db.insert("a", "1"), "insert new key should succeed");
CHECK(db.size() == 1, "size should be 1");
CHECK(db.search("a").has_value(), "search should find key");
CHECK(db.search("a").value() == "1", "value should match");
PASS();
}
static void test_insert_duplicate() {
TEST("insert duplicate");
Memdb<std::string, std::string> db;
db.insert("a", "1");
CHECK(!db.insert("a", "2"), "insert duplicate should fail");
CHECK(db.search("a").value() == "1", "value should be unchanged");
CHECK(db.size() == 1, "size should still be 1");
PASS();
}
static void test_insert_int_keys() {
TEST("insert int keys");
Memdb<int, std::string> db;
CHECK(db.insert(1, "one"), "");
CHECK(db.insert(2, "two"), "");
CHECK(db.search(1).value() == "one", "");
CHECK(db.search(2).value() == "two", "");
CHECK(db.size() == 2, "");
PASS();
}
// ── remove ──────────────────────────────────────────────────────────────
static void test_remove_existing() {
TEST("remove existing");
Memdb<std::string, std::string> db;
db.insert("a", "1");
db.insert("b", "2");
CHECK(db.remove("a"), "remove existing should succeed");
CHECK(!db.search("a").has_value(), "removed key should not be found");
CHECK(db.size() == 1, "size should be 1");
CHECK(db.search("b").value() == "2", "other key should still exist");
PASS();
}
static void test_remove_nonexistent() {
TEST("remove nonexistent");
Memdb<std::string, std::string> db;
CHECK(!db.remove("ghost"), "remove nonexistent should fail");
CHECK(db.size() == 0, "size should still be 0");
PASS();
}
static void test_remove_all() {
TEST("remove all");
Memdb<std::string, std::string> db;
db.insert("a", "1");
db.insert("b", "2");
db.insert("c", "3");
db.remove("a");
db.remove("b");
db.remove("c");
CHECK(db.size() == 0, "size should be 0");
CHECK(!db.search("a").has_value(), "");
CHECK(!db.search("b").has_value(), "");
CHECK(!db.search("c").has_value(), "");
PASS();
}
// ── update ──────────────────────────────────────────────────────────────
static void test_update_without_refresh() {
TEST("update without refresh TTL");
Memdb<std::string, std::string> db(1); // 1s TTL
db.insert("a", "1");
std::this_thread::sleep_for(std::chrono::milliseconds(500));
CHECK(db.update("a", "2", false), "update should succeed");
CHECK(db.search("a").value() == "2", "value should be updated");
// 500ms left — should still exist
std::this_thread::sleep_for(std::chrono::milliseconds(300));
CHECK(db.search("a").has_value(), "should still exist (800ms < 1s)");
PASS();
}
static void test_update_with_refresh() {
TEST("update with refresh TTL");
Memdb<std::string, std::string> db(1); // 1s TTL
db.insert("a", "1");
std::this_thread::sleep_for(std::chrono::milliseconds(800));
CHECK(db.update("a", "2", true), "update should succeed");
CHECK(db.search("a").value() == "2", "value should be updated");
// TTL was refreshed — should still exist
std::this_thread::sleep_for(std::chrono::milliseconds(500));
CHECK(db.search("a").has_value(), "should still exist after refresh");
PASS();
}
static void test_update_nonexistent() {
TEST("update nonexistent");
Memdb<std::string, std::string> db;
CHECK(!db.update("ghost", "1"), "update nonexistent should fail");
PASS();
}
// ── search ──────────────────────────────────────────────────────────────
static void test_search_nonexistent() {
TEST("search nonexistent");
Memdb<std::string, std::string> db;
CHECK(!db.search("ghost").has_value(), "search nonexistent should return nullopt");
PASS();
}
// ── size ────────────────────────────────────────────────────────────────
static void test_size_empty() {
TEST("size empty");
Memdb<std::string, std::string> db;
CHECK(db.size() == 0, "empty db should have size 0");
PASS();
}
static void test_size_after_ops() {
TEST("size after operations");
Memdb<std::string, std::string> db;
db.insert("a", "1");
db.insert("b", "2");
CHECK(db.size() == 2, "size after 2 inserts");
db.remove("a");
CHECK(db.size() == 1, "size after remove");
db.insert("c", "3");
CHECK(db.size() == 2, "size after re-insert");
PASS();
}
// ── TTL expiration (flush) ──────────────────────────────────────────────
static void test_ttl_expiration() {
TEST("TTL expiration");
Memdb<std::string, std::string> db(1); // 1s TTL
db.insert("a", "1");
db.insert("b", "2");
CHECK(db.size() == 2, "initial size 2");
std::this_thread::sleep_for(std::chrono::milliseconds(1100));
// search triggers flush
CHECK(!db.search("a").has_value(), "a should be expired");
CHECK(!db.search("b").has_value(), "b should be expired");
CHECK(db.size() == 0, "size should be 0 after flush");
PASS();
}
static void test_ttl_partial_expiration() {
TEST("TTL partial expiration");
Memdb<std::string, std::string> db(1);
db.insert("a", "1");
std::this_thread::sleep_for(std::chrono::milliseconds(600));
db.insert("b", "2"); // fresher
std::this_thread::sleep_for(std::chrono::milliseconds(500));
// a: 1100ms old → expired, b: 500ms old → alive
CHECK(!db.search("a").has_value(), "a should be expired");
CHECK(db.search("b").has_value(), "b should still be alive");
CHECK(db.size() == 1, "size should be 1");
PASS();
}
static void test_ttl_zero_means_immediate() {
TEST("TTL zero means immediate expiration");
Memdb<std::string, std::string> db(0);
db.insert("a", "1");
CHECK(!db.search("a").has_value(), "zero TTL should expire immediately");
CHECK(db.size() == 0, "size should be 0");
PASS();
}
// ── get_db / show ───────────────────────────────────────────────────────
static void test_get_db() {
TEST("get_db returns all entries");
Memdb<std::string, std::string> db(10);
db.insert("x", "100");
db.insert("y", "200");
const auto& lst = db.get_db();
CHECK(lst.size() == 2, "list should have 2 entries");
// verify values are accessible
bool found_x = false, found_y = false;
for (const auto& col : lst) {
if (col.key == "x" && col.value == "100") found_x = true;
if (col.key == "y" && col.value == "200") found_y = true;
}
CHECK(found_x && found_y, "both entries should be in the list");
PASS();
}
// ── get_keys ────────────────────────────────────────────────────────────
static void test_get_keys() {
TEST("get_keys returns key index");
Memdb<std::string, std::string> db;
db.insert("k1", "v1");
const auto& keys = db.get_keys();
CHECK(keys.size() == 1, "keys should have 1 entry");
CHECK(keys.find("k1") != keys.end(), "k1 should be in keys");
PASS();
}
// ── get_time_touch ──────────────────────────────────────────────────────
static void test_get_time_touch() {
TEST("get_time_touch returns sorted timestamps");
Memdb<std::string, std::string> db(10);
db.insert("a", "1");
db.insert("b", "2");
const auto& tt = db.get_time_touch();
CHECK(tt.size() == 2, "time_touch should have 2 entries");
// verify sorted order (a before b since a was inserted first)
auto it = tt.begin();
auto first = it;
++it;
CHECK(first->item->timestamp <= it->item->timestamp,
"time_touch should be sorted by timestamp");
PASS();
}
// ── copy assignment ─────────────────────────────────────────────────────
static void test_copy_assignment() {
TEST("copy assignment");
Memdb<std::string, std::string> db1;
db1.insert("a", "1");
Memdb<std::string, std::string> db2;
db2 = db1;
CHECK(db2.search("a").value() == "1", "copied db should have same data");
// verify independence
db2.insert("b", "2");
CHECK(db1.size() == 1, "original should be unchanged");
CHECK(db2.size() == 2, "copy should be independent");
PASS();
}
// ── edge cases ──────────────────────────────────────────────────────────
static void test_large_number_of_keys() {
TEST("large number of keys");
Memdb<int, int> db(60);
const int N = 1000;
for (int i = 0; i < N; ++i) {
CHECK(db.insert(i, i * 10), "all inserts should succeed");
}
CHECK(db.size() == static_cast<size_t>(N), "size should be N");
for (int i = 0; i < N; ++i) {
CHECK(db.search(i).value() == i * 10, "all values should match");
}
PASS();
}
static void test_reinsert_after_remove() {
TEST("reinsert after remove");
Memdb<std::string, std::string> db;
db.insert("a", "1");
db.remove("a");
CHECK(db.insert("a", "2"), "reinsert should succeed");
CHECK(db.search("a").value() == "2", "value should be updated");
PASS();
}
static void test_update_without_refresh_keeps_old_ttl() {
TEST("update without refresh keeps old TTL");
Memdb<std::string, std::string> db(1);
db.insert("a", "1");
std::this_thread::sleep_for(std::chrono::milliseconds(800));
db.update("a", "2", false); // value changes, TTL unchanged
// wait remaining 200ms+ — should expire
std::this_thread::sleep_for(std::chrono::milliseconds(300));
CHECK(!db.search("a").has_value(), "should expire at original TTL");
PASS();
}
// ────────────────────────────────────────────────────────────────────────
int main() {
std::cout << "=== libmemdb unit tests ===" << std::endl;
test_insert_basic();
test_insert_duplicate();
test_insert_int_keys();
test_remove_existing();
test_remove_nonexistent();
test_remove_all();
test_update_without_refresh();
test_update_with_refresh();
test_update_nonexistent();
test_search_nonexistent();
test_size_empty();
test_size_after_ops();
test_ttl_expiration();
test_ttl_partial_expiration();
test_ttl_zero_means_immediate();
test_get_db();
test_get_keys();
test_get_time_touch();
test_copy_assignment();
test_large_number_of_keys();
test_reinsert_after_remove();
test_update_without_refresh_keeps_old_ttl();
std::cout << "\n=== Results: " << g_passed << " passed, "
<< g_failed << " failed ===" << std::endl;
return g_failed > 0 ? 1 : 0;
}