diff --git a/CMakeLists.txt b/CMakeLists.txt index e0d03f8..8e8f3b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,6 +84,9 @@ endmacro() # Tests if(BUILD_TESTS) enable_testing() + add_cloudsql_test(catalog_coverage_tests tests/catalog_coverage_tests.cpp) + add_cloudsql_test(transaction_coverage_tests tests/transaction_coverage_tests.cpp) + add_cloudsql_test(utils_coverage_tests tests/utils_coverage_tests.cpp) add_cloudsql_test(cloudSQL_tests tests/cloudSQL_tests.cpp) add_cloudsql_test(server_tests tests/server_tests.cpp) add_cloudsql_test(statement_tests tests/statement_tests.cpp) diff --git a/tests/catalog_coverage_tests.cpp b/tests/catalog_coverage_tests.cpp new file mode 100644 index 0000000..7470997 --- /dev/null +++ b/tests/catalog_coverage_tests.cpp @@ -0,0 +1,158 @@ +/** + * @file catalog_coverage_tests.cpp + * @brief Targeted unit tests to increase coverage of the Catalog module + */ + +#include + +#include +#include + +#include "catalog/catalog.hpp" +#include "common/value.hpp" +#include "distributed/raft_types.hpp" + +using namespace cloudsql; + +namespace { + +/** + * @brief Tests Catalog behavior with missing entities and invalid lookups. + */ +TEST(CatalogCoverageTests, MissingEntities) { + auto catalog = Catalog::create(); + + // Invalid table lookup + EXPECT_FALSE(catalog->get_table(9999).has_value()); + EXPECT_FALSE(catalog->table_exists(9999)); + EXPECT_FALSE(catalog->table_exists_by_name("non_existent")); + EXPECT_FALSE(catalog->get_table_by_name("non_existent").has_value()); + + // Invalid index lookup + EXPECT_FALSE(catalog->get_index(8888).has_value()); + EXPECT_TRUE(catalog->get_table_indexes(9999).empty()); + + // Dropping non-existent entities + EXPECT_FALSE(catalog->drop_table(9999)); + EXPECT_FALSE(catalog->drop_index(8888)); + + // Update stats for non-existent table + EXPECT_FALSE(catalog->update_table_stats(9999, 100)); +} + +/** + * @brief Tests Catalog behavior with duplicate entities and creation edge cases. + */ +TEST(CatalogCoverageTests, DuplicateEntities) { + auto catalog = Catalog::create(); + std::vector cols = {{"id", common::ValueType::TYPE_INT64, 0}}; + + oid_t tid = catalog->create_table("test_table", cols); + ASSERT_NE(tid, 0); + + // Duplicate table creation should throw + EXPECT_THROW(catalog->create_table("test_table", cols), std::runtime_error); + + // Create an index + oid_t iid = catalog->create_index("idx_id", tid, {0}, IndexType::BTree, true); + ASSERT_NE(iid, 0); + + // Duplicate index creation should throw + EXPECT_THROW(catalog->create_index("idx_id", tid, {0}, IndexType::BTree, true), + std::runtime_error); + + // Creating index on missing table + EXPECT_EQ(catalog->create_index("idx_missing", 9999, {0}, IndexType::BTree, false), 0); +} + +/** + * @brief Helper to serialize CreateTable command for Raft simulation + */ +std::vector serialize_create_table(const std::string& name, + const std::vector& columns) { + std::vector data; + data.push_back(1); // Type 1 + + uint32_t name_len = name.size(); + size_t off = data.size(); + data.resize(off + 4 + name_len); + std::memcpy(data.data() + off, &name_len, 4); + std::memcpy(data.data() + off + 4, name.data(), name_len); + + uint32_t col_count = columns.size(); + off = data.size(); + data.resize(off + 4); + std::memcpy(data.data() + off, &col_count, 4); + + for (const auto& col : columns) { + uint32_t cname_len = col.name.size(); + off = data.size(); + data.resize(off + 4 + cname_len + 1 + 2); + std::memcpy(data.data() + off, &cname_len, 4); + std::memcpy(data.data() + off + 4, col.name.data(), cname_len); + data[off + 4 + cname_len] = static_cast(col.type); + std::memcpy(data.data() + off + 4 + cname_len + 1, &col.position, 2); + } + + uint32_t shard_count = 1; + off = data.size(); + data.resize(off + 4); + std::memcpy(data.data() + off, &shard_count, 4); + + std::string addr = "127.0.0.1"; + uint32_t addr_len = addr.size(); + uint32_t sid = 0; + uint16_t port = 6441; + + off = data.size(); + data.resize(off + 4 + addr_len + 4 + 2); + std::memcpy(data.data() + off, &addr_len, 4); + std::memcpy(data.data() + off + 4, addr.data(), addr_len); + std::memcpy(data.data() + off + 4 + addr_len, &sid, 4); + std::memcpy(data.data() + off + 4 + addr_len + 4, &port, 2); + + return data; +} + +/** + * @brief Tests the Raft state machine application (apply) in the Catalog. + */ +TEST(CatalogCoverageTests, RaftApply) { + auto catalog = Catalog::create(); + + // 1. Replay CreateTable + std::vector cols = {{"id", common::ValueType::TYPE_INT64, 0}}; + std::vector create_data = serialize_create_table("raft_table", cols); + + raft::LogEntry entry; + entry.term = 1; + entry.index = 1; + entry.data = create_data; + + catalog->apply(entry); + EXPECT_TRUE(catalog->table_exists_by_name("raft_table")); + + auto table_opt = catalog->get_table_by_name("raft_table"); + ASSERT_TRUE(table_opt.has_value()); + oid_t tid = (*table_opt)->table_id; + + // 2. Replay DropTable + std::vector drop_data; + drop_data.push_back(2); // Type 2 + drop_data.resize(5); + std::memcpy(drop_data.data() + 1, &tid, 4); + + entry.index = 2; + entry.data = drop_data; + + catalog->apply(entry); + EXPECT_FALSE(catalog->table_exists(tid)); + EXPECT_FALSE(catalog->table_exists_by_name("raft_table")); + + // 3. Replay with empty data (should do nothing) + entry.index = 3; + entry.data.clear(); + catalog->apply(entry); +} + +} // namespace diff --git a/tests/transaction_coverage_tests.cpp b/tests/transaction_coverage_tests.cpp new file mode 100644 index 0000000..eaa727f --- /dev/null +++ b/tests/transaction_coverage_tests.cpp @@ -0,0 +1,137 @@ +/** + * @file transaction_coverage_tests.cpp + * @brief Targeted unit tests to increase coverage of Transaction and Lock Manager + */ + +#include + +#include +#include +#include +#include + +#include "catalog/catalog.hpp" +#include "common/config.hpp" +#include "storage/buffer_pool_manager.hpp" +#include "storage/heap_table.hpp" +#include "storage/storage_manager.hpp" +#include "transaction/lock_manager.hpp" +#include "transaction/transaction.hpp" +#include "transaction/transaction_manager.hpp" + +using namespace cloudsql; +using namespace cloudsql::transaction; +using namespace cloudsql::storage; + +namespace { + +/** + * @brief Stress tests the LockManager with concurrent shared and exclusive requests. + */ +TEST(TransactionCoverageTests, LockManagerConcurrency) { + LockManager lm; + const int num_readers = 5; + std::vector readers; + std::atomic shared_granted{0}; + std::atomic stop{false}; + + Transaction writer_txn(100); + + // Writers holds exclusive lock initially + ASSERT_TRUE(lm.acquire_exclusive(&writer_txn, "RESOURCE")); + + for (int i = 0; i < num_readers; ++i) { + readers.emplace_back([&, i]() { + Transaction reader_txn(i); + if (lm.acquire_shared(&reader_txn, "RESOURCE")) { + shared_granted++; + while (!stop) { + std::this_thread::yield(); + } + lm.unlock(&reader_txn, "RESOURCE"); + } + }); + } + + // Readers should be blocked by the writer + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + EXPECT_EQ(shared_granted.load(), 0); + + // Release writer lock, readers should proceed + lm.unlock(&writer_txn, "RESOURCE"); + + // Wait for all readers to get the lock + for (int i = 0; i < 50 && shared_granted.load() < num_readers; ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + EXPECT_EQ(shared_granted.load(), num_readers); + + stop = true; + for (auto& t : readers) { + t.join(); + } +} + +/** + * @brief Tests deep rollback functionality via the Undo Log. + */ +TEST(TransactionCoverageTests, DeepRollback) { + auto catalog = Catalog::create(); + StorageManager disk_manager("./test_data"); + BufferPoolManager bpm(config::Config::DEFAULT_BUFFER_POOL_SIZE, disk_manager); + LockManager lm; + TransactionManager tm(lm, *catalog, bpm, nullptr); + + std::vector cols = {{"id", common::ValueType::TYPE_INT64, 0}, + {"val", common::ValueType::TYPE_TEXT, 1}}; + oid_t tid = catalog->create_table("rollback_stress", cols); + + executor::Schema schema; + schema.add_column("id", common::ValueType::TYPE_INT64); + schema.add_column("val", common::ValueType::TYPE_TEXT); + + HeapTable table("rollback_stress", bpm, schema); + table.create(); + + Transaction* txn = tm.begin(); + + // 1. Insert some data + auto rid1 = + table.insert(executor::Tuple({common::Value::make_int64(1), common::Value::make_text("A")}), + txn->get_id()); + txn->add_undo_log(UndoLog::Type::INSERT, "rollback_stress", rid1); + + auto rid2 = + table.insert(executor::Tuple({common::Value::make_int64(2), common::Value::make_text("B")}), + txn->get_id()); + txn->add_undo_log(UndoLog::Type::INSERT, "rollback_stress", rid2); + + // 2. Update data + table.remove(rid1, txn->get_id()); // Mark old version deleted + auto rid1_new = table.insert( + executor::Tuple({common::Value::make_int64(1), common::Value::make_text("A_NEW")}), + txn->get_id()); + txn->add_undo_log(UndoLog::Type::UPDATE, "rollback_stress", rid1_new, rid1); + + // 3. Delete data + table.remove(rid2, txn->get_id()); + txn->add_undo_log(UndoLog::Type::DELETE, "rollback_stress", rid2); + + EXPECT_EQ(table.tuple_count(), 1U); // rid1_new is active, rid1 and rid2 are logically deleted + + // 4. Abort + tm.abort(txn); + + // 5. Verify restoration + EXPECT_EQ(table.tuple_count(), + 0U); // Inserted rows should be physically removed or logically invisible + + // The table should be empty because we aborted the inserts + auto iter = table.scan(); + executor::Tuple t; + EXPECT_FALSE(iter.next(t)); + + static_cast(std::remove("./test_data/rollback_stress.heap")); +} + +} // namespace diff --git a/tests/utils_coverage_tests.cpp b/tests/utils_coverage_tests.cpp new file mode 100644 index 0000000..30d449f --- /dev/null +++ b/tests/utils_coverage_tests.cpp @@ -0,0 +1,113 @@ +/** + * @file utils_coverage_tests.cpp + * @brief Targeted unit tests to increase coverage of Common Values and Network Client + */ + +#include + +#include +#include + +#include "common/value.hpp" +#include "network/rpc_client.hpp" + +using namespace cloudsql::common; +using namespace cloudsql::network; + +namespace { + +/** + * @brief Tests Value accessor failure paths and boundary conversions. + */ +TEST(UtilsCoverageTests, ValueEdgeCases) { + // 1. Accessor failure paths + Value v_int(ValueType::TYPE_INT32); // Default 0 + EXPECT_THROW(v_int.as_bool(), std::runtime_error); + EXPECT_THROW(v_int.as_float64(), std::runtime_error); + EXPECT_THROW(v_int.as_text(), std::runtime_error); + + Value v_bool(true); + EXPECT_THROW(v_bool.as_int64(), std::runtime_error); + + Value v_text("123"); + EXPECT_THROW(v_text.as_int32(), std::runtime_error); + + // 2. boundary to_* conversions + EXPECT_EQ(v_text.to_int64(), 0); // Text doesn't auto-convert to int in to_int64() + EXPECT_EQ(v_text.to_float64(), 0.0); + + Value v_null = Value::make_null(); + EXPECT_EQ(v_null.to_int64(), 0); + EXPECT_EQ(v_null.to_float64(), 0.0); + EXPECT_STREQ(v_null.to_string().c_str(), "NULL"); + + Value v_f(1.23); + EXPECT_EQ(v_f.to_int64(), 1); + + // 3. Numeric check + EXPECT_TRUE(v_int.is_numeric()); + EXPECT_TRUE(v_f.is_numeric()); + EXPECT_FALSE(v_text.is_numeric()); + EXPECT_FALSE(v_bool.is_numeric()); +} + +/** + * @brief Tests complex Value comparisons including mixed types and NULLs. + */ +TEST(UtilsCoverageTests, ValueComparisons) { + Value v_null = Value::make_null(); + Value v_int(10); + Value v_float(10.0); + Value v_text("A"); + + // Equality + EXPECT_TRUE(v_int == v_float); // Numeric equality + EXPECT_FALSE(v_int == v_text); + EXPECT_FALSE(v_int == v_null); + + // Less than + EXPECT_FALSE(v_null < v_int); // NULL is not less than anything + EXPECT_TRUE(v_int < v_null); // non-NULL is less than NULL (by convention in this impl) + + Value v_int_small(5); + EXPECT_TRUE(v_int_small < v_float); + EXPECT_FALSE(v_float < v_int_small); + + Value v_text_b("B"); + EXPECT_TRUE(v_text < v_text_b); + + // Mixed numeric/non-numeric comparison + EXPECT_FALSE(v_int < v_text); +} + +/** + * @brief Tests RpcClient behavior on connection failures. + */ +TEST(UtilsCoverageTests, RpcClientFailure) { + // Attempt to connect to an unreachable port on localhost + RpcClient client("127.0.0.1", 1); // Port 1 is usually privileged and closed + + EXPECT_FALSE(client.connect()); + EXPECT_FALSE(client.is_connected()); + + std::vector resp; + EXPECT_FALSE(client.call(RpcType::AppendEntries, {1, 2, 3}, resp)); + EXPECT_FALSE(client.send_only(RpcType::AppendEntries, {1, 2, 3})); +} + +/** + * @brief Tests Value Hashing. + */ +TEST(UtilsCoverageTests, ValueHash) { + Value::Hash hasher; + Value v1(10); + Value v2(10); + Value v3("10"); + Value v_null = Value::make_null(); + + EXPECT_EQ(hasher(v1), hasher(v2)); + EXPECT_NE(hasher(v1), hasher(v3)); + EXPECT_NE(hasher(v1), hasher(v_null)); +} + +} // namespace