/** * Unit-тесты для route_lib - библиотеки маршрутизации */ #include #include #include #include #include #include #include "route_lib.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #ifndef DEBUG_CATEGORY_ROUTING #define DEBUG_CATEGORY_ROUTING 1 #endif static struct { int run, passed, failed; long ops; double time_ms; } stats = {0}; #define TEST(name) do { \ printf("TEST: %-40s ", name); fflush(stdout); \ stats.run++; \ } while(0) #define PASS() do { puts("PASS"); stats.passed++; } while(0) #define FAIL(msg) do { printf("FAIL: %s\n", msg); stats.failed++; } while(0) #define ASSERT(c, m) do { if (!(c)) { FAIL(m); return; } } while(0) #define ASSERT_EQ(a,b,m) ASSERT((a)==(b),m) #define ASSERT_NE(a,b,m) ASSERT((a)!=(b),m) /* ------------------------------------------------------------------ */ static double now_ms(void) { struct timeval tv; gettimeofday(&tv, NULL); return tv.tv_sec * 1000.0 + tv.tv_usec / 1000.0; } /* Создание тестового маршрута */ static void create_test_route(struct ROUTE_ENTRY *entry, uint32_t network, uint8_t prefix_len, route_type_t type, struct ETCP_CONN *next_hop) { memset(entry, 0, sizeof(*entry)); entry->network = network; entry->prefix_length = prefix_len; entry->type = type; entry->next_hop = next_hop; entry->flags = ROUTE_FLAG_ACTIVE; entry->metrics.latency_ms = 10 + (rand() % 100); entry->metrics.packet_loss_rate = rand() % 50; entry->hop_count = 1 + (rand() % 5); entry->metrics.bandwidth_kbps = 1000 + (rand() % 9000); route_update_quality(entry); } /* ------------------------------------------------------------------ */ static void test_table_create_destroy(void) { TEST("table create/destroy"); struct ROUTE_TABLE *table = route_table_create(); ASSERT(table != NULL, "failed to create table"); ASSERT_EQ(table->count, 0, "new table should be empty"); ASSERT(table->capacity > 0, "table should have capacity"); ASSERT(table->entries != NULL, "table should have entries array"); route_table_destroy(table); PASS(); } static void test_route_insert(void) { TEST("route insert"); struct ROUTE_TABLE *table = route_table_create(); ASSERT(table != NULL, "failed to create table"); struct ROUTE_ENTRY entry; create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_STATIC, NULL); ASSERT(route_table_insert(table, &entry), "failed to insert static route"); ASSERT_EQ(table->count, 1, "table should have 1 route"); create_test_route(&entry, 0xC0A80200, 24, ROUTE_TYPE_STATIC, NULL); ASSERT(route_table_insert(table, &entry), "failed to insert second route"); ASSERT_EQ(table->count, 2, "table should have 2 routes"); route_table_destroy(table); PASS(); } static void test_route_update_existing(void) { TEST("route update existing"); struct ROUTE_TABLE *table = route_table_create(); struct ROUTE_ENTRY entry; create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_STATIC, NULL); entry.metrics.latency_ms = 50; ASSERT(route_table_insert(table, &entry), "failed to insert route"); create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_STATIC, NULL); entry.metrics.latency_ms = 100; ASSERT(route_table_insert(table, &entry), "failed to update route"); ASSERT_EQ(table->count, 1, "should still have 1 route after update"); route_table_destroy(table); PASS(); } static void test_route_lookup_basic(void) { TEST("route lookup basic"); struct ROUTE_TABLE *table = route_table_create(); struct ROUTE_ENTRY entry; create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_STATIC, NULL); route_table_insert(table, &entry); struct ROUTE_ARRAY *result = route_table_lookup(table, 0xC0A80164); ASSERT(result != NULL, "should find route for 192.168.1.100"); ASSERT_EQ(result->routes, 1, "should find exactly 1 route"); ASSERT_EQ(result->entries[0]->network, 0xC0A80100, "wrong network"); route_cache_clear(); route_table_destroy(table); PASS(); } static void test_route_lookup_miss(void) { TEST("route lookup miss"); struct ROUTE_TABLE *table = route_table_create(); struct ROUTE_ENTRY entry; create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_STATIC, NULL); route_table_insert(table, &entry); struct ROUTE_ARRAY *result = route_table_lookup(table, 0x0A000001); ASSERT(result == NULL, "should not find route for 10.0.0.1"); route_table_destroy(table); PASS(); } static void test_longest_prefix_match(void) { TEST("longest prefix match"); struct ROUTE_TABLE *table = route_table_create(); struct ROUTE_ENTRY entry; // Используем фиктивные указатели для next_hop struct ETCP_CONN *dummy_conn1 = (struct ETCP_CONN *)0x1000; struct ETCP_CONN *dummy_conn2 = (struct ETCP_CONN *)0x2000; create_test_route(&entry, 0xC0A80000, 16, ROUTE_TYPE_STATIC, dummy_conn1); route_table_insert(table, &entry); create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_STATIC, dummy_conn2); route_table_insert(table, &entry); // Используем IP 192.168.1.200 (0xC0A801C8) - отличается от предыдущего теста // чтобы избежать попадания в кеш с предыдущего теста struct ROUTE_ARRAY *result = route_table_lookup(table, 0xC0A801C8); ASSERT(result != NULL, "should find routes"); ASSERT_EQ(result->routes, 2, "should find 2 routes"); int found_16 = 0, found_24 = 0; for (uint32_t i = 0; i < result->routes; i++) { if (result->entries[i]->prefix_length == 16) found_16 = 1; if (result->entries[i]->prefix_length == 24) found_24 = 1; } ASSERT(found_16 && found_24, "should find both prefix lengths"); route_cache_clear(); route_table_destroy(table); PASS(); } static void test_route_delete_by_conn(void) { TEST("route delete by connection"); struct ROUTE_TABLE *table = route_table_create(); struct ROUTE_ENTRY entry; struct ETCP_CONN *conn1 = (struct ETCP_CONN *)0x1000; struct ETCP_CONN *conn2 = (struct ETCP_CONN *)0x2000; create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_STATIC, conn1); route_table_insert(table, &entry); create_test_route(&entry, 0xC0A80200, 24, ROUTE_TYPE_STATIC, conn1); route_table_insert(table, &entry); create_test_route(&entry, 0xC0A80300, 24, ROUTE_TYPE_STATIC, conn2); route_table_insert(table, &entry); ASSERT_EQ(table->count, 3, "should have 3 routes"); route_table_delete(table, conn1); ASSERT_EQ(table->count, 1, "should have 1 route after deletion"); struct ROUTE_ARRAY *result = route_table_lookup(table, 0xC0A80301); ASSERT(result != NULL, "should still find route via conn2"); ASSERT_EQ(result->entries[0]->next_hop, conn2, "wrong next_hop"); route_cache_clear(); route_table_destroy(table); PASS(); } static void test_parse_subnet(void) { TEST("parse subnet"); uint32_t network; uint8_t prefix_len; ASSERT_EQ(parse_subnet("192.168.1.0/24", &network, &prefix_len), 0, "failed to parse 192.168.1.0/24"); ASSERT_EQ(network, 0xC0A80100, "wrong network for 192.168.1.0/24"); ASSERT_EQ(prefix_len, 24, "wrong prefix length"); ASSERT_EQ(parse_subnet("10.0.0.0/8", &network, &prefix_len), 0, "failed to parse 10.0.0.0/8"); ASSERT_EQ(network, 0x0A000000, "wrong network for 10.0.0.0/8"); ASSERT_EQ(prefix_len, 8, "wrong prefix length"); ASSERT_EQ(parse_subnet("0.0.0.0/0", &network, &prefix_len), 0, "failed to parse 0.0.0.0/0"); ASSERT_EQ(network, 0, "wrong network for 0.0.0.0/0"); ASSERT_EQ(prefix_len, 0, "wrong prefix length"); ASSERT_EQ(parse_subnet("invalid", &network, &prefix_len), -1, "should fail on invalid string"); ASSERT_EQ(parse_subnet("192.168.1.0/33", &network, &prefix_len), -1, "should fail on prefix > 32"); ASSERT_EQ(parse_subnet("192.168.1.0", &network, &prefix_len), -1, "should fail without prefix"); ASSERT_EQ(parse_subnet("256.0.0.0/8", &network, &prefix_len), -1, "should fail on invalid IP"); ASSERT_EQ(parse_subnet(NULL, &network, &prefix_len), -1, "should fail on NULL string"); PASS(); } static void test_update_quality(void) { TEST("update quality calculation"); struct ROUTE_ENTRY entry; memset(&entry, 0, sizeof(entry)); entry.latency = 100; // x0.1 ms units (= 10 ms) entry.metrics.packet_loss_rate = 0; entry.hop_count = 1; ASSERT(route_update_quality(&entry), "failed to update quality"); ASSERT_EQ(entry.metrics.metric, 70, "wrong quality calculation"); struct ROUTE_ENTRY entry2; memset(&entry2, 0, sizeof(entry2)); entry2.latency = 50; // x0.1 ms units (= 5 ms) entry2.metrics.packet_loss_rate = 0; entry2.hop_count = 1; route_update_quality(&entry2); ASSERT(entry2.metrics.metric < entry.metrics.metric, "better route should have lower quality metric"); PASS(); } static void test_dynamic_subnet_validation(void) { TEST("dynamic subnet validation"); struct ROUTE_TABLE *table = route_table_create(); ASSERT(table != NULL, "failed to create table"); ASSERT(route_add_dynamic_subnet(table, 0x0A000000, 8), "failed to add dynamic subnet"); struct ROUTE_ENTRY entry; create_test_route(&entry, 0x0A000100, 24, ROUTE_TYPE_DYNAMIC, NULL); ASSERT(route_table_insert(table, &entry), "should insert route in allowed subnet"); create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_DYNAMIC, NULL); ASSERT(!route_table_insert(table, &entry), "should reject route outside allowed subnet"); create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_STATIC, NULL); ASSERT(route_table_insert(table, &entry), "should accept static route regardless of validation"); route_table_destroy(table); PASS(); } static void test_local_subnet_validation(void) { TEST("local subnet validation"); struct ROUTE_TABLE *table = route_table_create(); ASSERT(route_add_local_subnet(table, 0xAC100000, 12), "failed to add local subnet"); struct ROUTE_ENTRY entry; create_test_route(&entry, 0xAC100100, 24, ROUTE_TYPE_LOCAL, NULL); ASSERT(route_table_insert(table, &entry), "should insert local route in allowed subnet"); create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_LOCAL, NULL); ASSERT(!route_table_insert(table, &entry), "should reject local route outside allowed subnet"); ASSERT_EQ(table->stats.local_routes, 1, "should have 1 local route"); route_table_destroy(table); PASS(); } static void test_get_all_routes(void) { TEST("get all routes for subnet"); struct ROUTE_TABLE *table = route_table_create(); struct ROUTE_ENTRY entry; struct ETCP_CONN *conn1 = (struct ETCP_CONN *)0x1000; struct ETCP_CONN *conn2 = (struct ETCP_CONN *)0x2000; create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_STATIC, conn1); route_table_insert(table, &entry); create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_STATIC, conn2); route_table_insert(table, &entry); create_test_route(&entry, 0xC0A80200, 24, ROUTE_TYPE_STATIC, conn1); route_table_insert(table, &entry); struct ROUTE_ENTRY *routes = NULL; size_t count = 0; ASSERT(route_get_all_routes(table, 0xC0A80100, 24, &routes, &count), "failed to get routes"); ASSERT_EQ(count, 2, "should find 2 routes for 192.168.1.0/24"); ASSERT(routes != NULL, "routes should not be NULL"); u_free(routes); route_table_destroy(table); PASS(); } static void test_inactive_route(void) { TEST("inactive route not returned"); struct ROUTE_TABLE *table = route_table_create(); struct ROUTE_ENTRY entry; struct ETCP_CONN *active_hop = (struct ETCP_CONN *)0x1000; struct ETCP_CONN *inactive_hop = (struct ETCP_CONN *)0x2000; // Активный маршрут через active_hop create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_STATIC, active_hop); route_table_insert(table, &entry); // Неактивный маршрут через inactive_hop (разный next_hop = разный маршрут) create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_STATIC, inactive_hop); entry.flags = 0; // Убираем флаг ACTIVE route_table_insert(table, &entry); ASSERT_EQ(table->count, 2, "should have 2 routes in table"); struct ROUTE_ARRAY *result = route_table_lookup(table, 0xC0A80101); ASSERT(result != NULL, "should find route"); ASSERT_EQ(result->routes, 1, "should find only 1 active route"); ASSERT(result->entries[0]->flags & ROUTE_FLAG_ACTIVE, "returned route should be active"); route_cache_clear(); route_table_destroy(table); PASS(); } static void test_table_expansion(void) { TEST("table expansion"); struct ROUTE_TABLE *table = route_table_create(); ASSERT(table != NULL, "failed to create table"); size_t initial_capacity = table->capacity; struct ROUTE_ENTRY entry; for (int i = 0; i < (int)initial_capacity + 10; i++) { create_test_route(&entry, 0xC0A80000 + (i << 8), 24, ROUTE_TYPE_STATIC, NULL); ASSERT(route_table_insert(table, &entry), "failed to insert route"); } ASSERT(table->capacity > initial_capacity, "table should have expanded"); ASSERT_EQ(table->count, (size_t)(initial_capacity + 10), "should have all routes"); route_table_destroy(table); PASS(); } static void test_cache_functionality(void) { TEST("route cache functionality"); struct ROUTE_TABLE *table = route_table_create(); struct ROUTE_ENTRY entry; create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_STATIC, NULL); route_table_insert(table, &entry); uint64_t hits_before = table->stats.routes_lookup_hits; struct ROUTE_ARRAY *result1 = route_table_lookup(table, 0xC0A80101); ASSERT(result1 != NULL, "first lookup should succeed"); struct ROUTE_ARRAY *result2 = route_table_lookup(table, 0xC0A80101); ASSERT(result2 != NULL, "second lookup should succeed"); ASSERT(table->stats.routes_lookup_hits > hits_before, "should increment lookup hits"); route_cache_clear(); route_table_destroy(table); PASS(); } static void test_route_types(void) { TEST("all route types"); struct ROUTE_TABLE *table = route_table_create(); struct ROUTE_ENTRY entry; create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_STATIC, NULL); ASSERT(route_table_insert(table, &entry), "failed to insert static route"); route_add_dynamic_subnet(table, 0x0A000000, 8); create_test_route(&entry, 0x0A000100, 24, ROUTE_TYPE_DYNAMIC, NULL); ASSERT(route_table_insert(table, &entry), "failed to insert dynamic route"); route_add_local_subnet(table, 0xAC100000, 12); create_test_route(&entry, 0xAC100100, 24, ROUTE_TYPE_LOCAL, NULL); ASSERT(route_table_insert(table, &entry), "failed to insert local route"); create_test_route(&entry, 0xC0A80200, 24, ROUTE_TYPE_LEARNED, NULL); ASSERT(route_table_insert(table, &entry), "failed to insert learned route"); ASSERT_EQ(table->count, 4, "should have 4 routes of different types"); ASSERT_EQ(table->stats.local_routes, 1, "should have 1 local route"); ASSERT_EQ(table->stats.learned_routes, 1, "should have 1 learned route"); route_table_destroy(table); PASS(); } static void test_route_delete_entry(void) { TEST("route delete entry"); struct ROUTE_TABLE *table = route_table_create(); struct ROUTE_ENTRY entry; struct ETCP_CONN *conn1 = (struct ETCP_CONN *)0x1000; // Вставляем несколько маршрутов create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_STATIC, conn1); route_table_insert(table, &entry); create_test_route(&entry, 0xC0A80200, 24, ROUTE_TYPE_STATIC, conn1); route_table_insert(table, &entry); create_test_route(&entry, 0xC0A80300, 24, ROUTE_TYPE_STATIC, conn1); route_table_insert(table, &entry); ASSERT_EQ(table->count, 3, "should have 3 routes"); // Удаляем маршрут по network/prefix (next_hop = NULL - любой) ASSERT(route_table_delete_entry(table, 0xC0A80200, 24, NULL), "should delete route 192.168.2.0/24"); ASSERT_EQ(table->count, 2, "should have 2 routes after deletion"); // Проверяем что правильный маршрут удалён struct ROUTE_ARRAY *result = route_table_lookup(table, 0xC0A80201); ASSERT(result == NULL, "should not find deleted route"); result = route_table_lookup(table, 0xC0A80101); ASSERT(result != NULL, "should still find 192.168.1.x route"); route_cache_clear(); // Пытаемся удалить несуществующий маршрут ASSERT(!route_table_delete_entry(table, 0xC0A80500, 24, NULL), "should fail to delete non-existent route"); ASSERT_EQ(table->count, 2, "should still have 2 routes"); route_table_destroy(table); PASS(); } static void test_route_delete_entry_by_nexthop(void) { TEST("route delete entry by next_hop"); struct ROUTE_TABLE *table = route_table_create(); struct ROUTE_ENTRY entry; struct ETCP_CONN *conn1 = (struct ETCP_CONN *)0x1000; struct ETCP_CONN *conn2 = (struct ETCP_CONN *)0x2000; // Одинаковый network/prefix, разные next_hop create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_STATIC, conn1); route_table_insert(table, &entry); create_test_route(&entry, 0xC0A80100, 24, ROUTE_TYPE_STATIC, conn2); route_table_insert(table, &entry); ASSERT_EQ(table->count, 2, "should have 2 routes"); // Удаляем только маршрут через conn1 ASSERT(route_table_delete_entry(table, 0xC0A80100, 24, conn1), "should delete route via conn1"); ASSERT_EQ(table->count, 1, "should have 1 route after deletion"); // Проверяем что остался маршрут через conn2 struct ROUTE_ARRAY *result = route_table_lookup(table, 0xC0A80101); ASSERT(result != NULL, "should still find route via conn2"); ASSERT_EQ(result->entries[0]->next_hop, conn2, "wrong next_hop remaining"); route_cache_clear(); route_table_destroy(table); PASS(); } static void test_route_withdraw_scenario(void) { TEST("BGP withdraw scenario"); struct ROUTE_TABLE *table = route_table_create(); struct ROUTE_ENTRY entry; struct ETCP_CONN *peer = (struct ETCP_CONN *)0x1000; // Имитируем получение маршрута от пира (learned route) memset(&entry, 0, sizeof(entry)); entry.network = 0x0A000000; // 10.0.0.0 entry.prefix_length = 8; entry.next_hop = peer; entry.type = ROUTE_TYPE_LEARNED; entry.flags = ROUTE_FLAG_ACTIVE | ROUTE_FLAG_LEARNED; entry.node_id = 0x1234567890ABCDEFULL; ASSERT(route_table_insert(table, &entry), "failed to insert learned route"); ASSERT_EQ(table->stats.learned_routes, 1, "should have 1 learned route"); // Имитируем получение withdraw ASSERT(route_table_delete_entry(table, 0x0A000000, 8, peer), "should withdraw route from peer"); ASSERT_EQ(table->count, 0, "should have 0 routes after withdraw"); ASSERT_EQ(table->stats.learned_routes, 0, "should have 0 learned routes"); route_table_destroy(table); PASS(); } static void test_stress(void) { TEST("stress 1000 routes"); double start = now_ms(); struct ROUTE_TABLE *table = route_table_create(); struct ROUTE_ENTRY entry; for (int i = 0; i < 1000; i++) { uint32_t network = 0xC0000000 | ((i & 0xFF) << 8) | ((i >> 8) & 0xFF); create_test_route(&entry, network, 24, ROUTE_TYPE_STATIC, NULL); ASSERT(route_table_insert(table, &entry), "failed to insert route in stress test"); stats.ops++; } for (int i = 0; i < 1000; i++) { uint32_t ip = 0xC0000000 | ((i & 0xFF) << 8) | ((i >> 8) & 0xFF) | 1; struct ROUTE_ARRAY *result = route_table_lookup(table, ip); // result находится в кеше, не освобождаем явно (void)result; stats.ops++; } stats.time_ms += now_ms() - start; ASSERT_EQ(table->count, 1000, "should have 1000 routes"); route_cache_clear(); route_table_destroy(table); PASS(); } /* ------------------------------------------------------------------ */ int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); debug_set_categories(DEBUG_CATEGORY_ROUTING); srand(time(NULL)); double t0 = now_ms(); test_table_create_destroy(); test_route_insert(); test_route_update_existing(); test_route_lookup_basic(); test_route_lookup_miss(); test_longest_prefix_match(); test_route_delete_by_conn(); test_parse_subnet(); test_update_quality(); test_dynamic_subnet_validation(); test_local_subnet_validation(); test_get_all_routes(); test_inactive_route(); test_table_expansion(); test_cache_functionality(); test_route_types(); test_route_delete_entry(); test_route_delete_entry_by_nexthop(); test_route_withdraw_scenario(); test_stress(); printf("\n=== SUMMARY ===\n" "run=%d passed=%d failed=%d\n" "ops=%ld time=%.2f ms\n", stats.run, stats.passed, stats.failed, stats.ops, now_ms() - t0); return stats.failed ? 1 : 0; }