@@ -26,6 +26,38 @@ namespace routeorch_test
2626 using ::testing::Return;
2727 using ::testing::DoAll;
2828
29+ static bool stateRouteStateFieldExists (swss::DBConnector* state_db,
30+ const std::string& prefix)
31+ {
32+ Table stateRoute (state_db, " ROUTE_TABLE" );
33+ std::vector<FieldValueTuple> fvs;
34+ stateRoute.get (prefix, fvs);
35+ for (const auto &fv : fvs)
36+ if (fvField (fv) == " state" )
37+ return true ;
38+ return false ;
39+ }
40+
41+ static bool waitStateRouteState (swss::DBConnector* state_db,
42+ const std::string& prefix,
43+ const std::string& want,
44+ int attempts = 30 )
45+ {
46+ Table stateRoute (state_db, " ROUTE_TABLE" );
47+ for (int i = 0 ; i < attempts; ++i)
48+ {
49+ std::vector<FieldValueTuple> fvs;
50+ stateRoute.get (prefix, fvs);
51+ for (const auto &fv : fvs)
52+ if (fvField (fv) == " state" && fvValue (fv) == want)
53+ return true ;
54+
55+ // Let orch process any pending work again
56+ static_cast <Orch *>(gRouteOrch )->doTask ();
57+ }
58+ return false ;
59+ }
60+
2961 DEFINE_SAI_API_MOCK_SPECIFY_ENTRY_WITH_SET (route, route);
3062
3163 shared_ptr<swss::DBConnector> m_app_db;
@@ -397,6 +429,209 @@ namespace routeorch_test
397429 }
398430 };
399431
432+ TEST_F (RouteOrchTest, RouteOrch_AddDeleteIPv6)
433+ {
434+ // Add IPv6 interface IPs (like the pytest does) and an IPv6 neighbor.
435+ {
436+ Table intfTable (m_app_db.get (), APP_INTF_TABLE_NAME);
437+ intfTable.set (" Ethernet0:2000::1/64" , { {" scope" ," global" }, {" family" ," IPv6" } });
438+ intfTable.set (" Ethernet4:2001::1/64" , { {" scope" ," global" }, {" family" ," IPv6" } });
439+ gIntfsOrch ->addExistingData (&intfTable);
440+ static_cast <Orch *>(gIntfsOrch )->doTask ();
441+
442+ Table neighborTable (m_app_db.get (), APP_NEIGH_TABLE_NAME);
443+ neighborTable.set (" Ethernet0:2000::2" , { {" neigh" ," 00:00:00:00:00:22" }, {" family" ," IPv6" } });
444+ gNeighOrch ->addExistingData (&neighborTable);
445+ static_cast <Orch *>(gNeighOrch )->doTask ();
446+ }
447+
448+ auto *routeConsumer = dynamic_cast <Consumer *>(gRouteOrch ->getExecutor (APP_ROUTE_TABLE_NAME));
449+ ASSERT_NE (routeConsumer, nullptr );
450+
451+ // PART A: Add/Remove IPv6 prefix 3000::/64 via 2000::2 on Ethernet0
452+ {
453+ std::deque<KeyOpFieldsValuesTuple> entries;
454+ entries.push_back ({ " 3000::/64" , " SET" ,
455+ { {" ifname" ," Ethernet0" }, {" nexthop" ," 2000::2" } }});
456+ routeConsumer->addToSync (entries);
457+
458+ auto base_create = create_route_count;
459+ auto base_set = set_route_count;
460+ auto base_remove = remove_route_count;
461+
462+ static_cast <Orch *>(gRouteOrch )->doTask ();
463+
464+ // Expect CREATE +1 for new route, no SET/REMOVE yet
465+ ASSERT_EQ (base_create + 1 , create_route_count);
466+ ASSERT_EQ (base_set, set_route_count);
467+ ASSERT_EQ (base_remove, remove_route_count);
468+
469+ // Remove the route
470+ entries.clear ();
471+ entries.push_back ({ " 3000::/64" , " DEL" , {} });
472+ routeConsumer->addToSync (entries);
473+
474+ base_create = create_route_count;
475+ base_set = set_route_count;
476+ base_remove = remove_route_count;
477+
478+ static_cast <Orch *>(gRouteOrch )->doTask ();
479+
480+ // Expect REMOVE +1, create/set unchanged
481+ ASSERT_EQ (base_create, create_route_count);
482+ ASSERT_EQ (base_set, set_route_count);
483+ ASSERT_EQ (base_remove + 1 , remove_route_count);
484+ }
485+
486+ // PART B: IPv6 default route (::/0): SET to add (state -> ok), DEL to remove (state -> na)
487+ {
488+ const std::string def6 = " ::/0" ;
489+ const bool hasStateField = stateRouteStateFieldExists (m_state_db.get (), def6);
490+
491+ // Add default v6 route (::/0) via SET path
492+ std::deque<KeyOpFieldsValuesTuple> entries;
493+ entries.push_back ({ def6, " SET" , { {" ifname" ," Ethernet0" }, {" nexthop" ," 2000::2" } }});
494+ routeConsumer->addToSync (entries);
495+
496+ auto base_create = create_route_count;
497+ auto base_set = set_route_count;
498+ auto base_remove = remove_route_count;
499+
500+ static_cast <Orch *>(gRouteOrch )->doTask ();
501+
502+ // Default route typically programs via attribute SET (no create/remove)
503+ ASSERT_EQ (base_create, create_route_count);
504+ ASSERT_EQ (base_remove, remove_route_count);
505+ ASSERT_EQ (base_set + 1 , set_route_count);
506+ ASSERT_EQ (sai_fail_count, 0 );
507+
508+ if (hasStateField)
509+ {
510+ ASSERT_TRUE (waitStateRouteState (m_state_db.get (), def6, " ok" ))
511+ << " Expected IPv6 default-route state to become 'ok' after SET." ;
512+ }
513+
514+ // Now delete the default v6 route
515+ entries.clear ();
516+ entries.push_back ({ def6, " DEL" , {} });
517+ routeConsumer->addToSync (entries);
518+
519+ base_create = create_route_count;
520+ base_set = set_route_count;
521+ base_remove = remove_route_count;
522+
523+ static_cast <Orch *>(gRouteOrch )->doTask ();
524+
525+ // Expect another SET (no create/remove), and no invalid SAI programming
526+ ASSERT_EQ (base_create, create_route_count);
527+ ASSERT_EQ (base_remove, remove_route_count);
528+ ASSERT_EQ (base_set + 1 , set_route_count);
529+ ASSERT_EQ (sai_fail_count, 0 );
530+
531+ if (hasStateField)
532+ {
533+ ASSERT_TRUE (waitStateRouteState (m_state_db.get (), def6, " na" ))
534+ << " Expected IPv6 default-route state to become 'na' after DEL." ;
535+ }
536+ }
537+ }
538+
539+ TEST_F (RouteOrchTest, RouteOrch_AddDeleteIPv4)
540+ {
541+ auto *routeConsumer = dynamic_cast <Consumer *>(gRouteOrch ->getExecutor (APP_ROUTE_TABLE_NAME));
542+ ASSERT_NE (routeConsumer, nullptr );
543+
544+ // PART A: Regular prefix add/remove (2.2.2.0/24)
545+ {
546+ std::deque<KeyOpFieldsValuesTuple> entries;
547+ entries.push_back ({ " 2.2.2.0/24" , " SET" ,
548+ { {" ifname" ," Ethernet0" }, {" nexthop" ," 10.0.0.2" } }});
549+ routeConsumer->addToSync (entries);
550+
551+ auto base_create = create_route_count;
552+ auto base_set = set_route_count;
553+ auto base_remove = remove_route_count;
554+
555+ static_cast <Orch *>(gRouteOrch )->doTask ();
556+
557+ // Expect create +1, set unchanged, remove unchanged
558+ ASSERT_EQ (base_create + 1 , create_route_count);
559+ ASSERT_EQ (base_set, set_route_count);
560+ ASSERT_EQ (base_remove, remove_route_count);
561+
562+ // Now remove the route
563+ entries.clear ();
564+ entries.push_back ({ " 2.2.2.0/24" , " DEL" , {} });
565+ routeConsumer->addToSync (entries);
566+
567+ base_create = create_route_count;
568+ base_set = set_route_count;
569+ base_remove = remove_route_count;
570+
571+ static_cast <Orch *>(gRouteOrch )->doTask ();
572+
573+ // Expect remove +1, create/set unchanged
574+ ASSERT_EQ (base_create, create_route_count);
575+ ASSERT_EQ (base_set, set_route_count);
576+ ASSERT_EQ (base_remove + 1 , remove_route_count);
577+ }
578+
579+ // PART B: Default route DEL -> state 'na' -> SET -> state 'ok'
580+ {
581+ const std::string def = " 0.0.0.0/0" ;
582+ ASSERT_TRUE (stateRouteStateFieldExists (m_state_db.get (), def))
583+ << " Expected STATE_DB:ROUTE_TABLE to expose 'state' for the default route." ;
584+
585+ // SetUp() seeds a default route; if state is exposed, it should become 'ok'
586+
587+ ASSERT_TRUE (waitStateRouteState (m_state_db.get (), def, " ok" ))
588+ << " Expected initial default-route state to become 'ok'." ;
589+
590+
591+ // DEL default route
592+ std::deque<KeyOpFieldsValuesTuple> entries;
593+ entries.push_back ({ def, " DEL" , {} });
594+ routeConsumer->addToSync (entries);
595+
596+ auto base_create = create_route_count;
597+ auto base_set = set_route_count;
598+ auto base_remove = remove_route_count;
599+
600+ static_cast <Orch *>(gRouteOrch )->doTask ();
601+
602+ // For default route, expect attribute SET path (no create/remove), set +1
603+ ASSERT_EQ (base_create, create_route_count);
604+ ASSERT_EQ (base_remove, remove_route_count);
605+ ASSERT_EQ (base_set + 1 , set_route_count);
606+ ASSERT_EQ (sai_fail_count, 0 );
607+
608+ ASSERT_TRUE (waitStateRouteState (m_state_db.get (), def, " na" ))
609+ << " Expected default-route state to become 'na' after DEL." ;
610+
611+
612+ // Re-SET default route
613+ entries.clear ();
614+ entries.push_back ({ def, " SET" , { {" ifname" ," Ethernet0" }, {" nexthop" ," 10.0.0.2" } }});
615+ routeConsumer->addToSync (entries);
616+
617+ base_create = create_route_count;
618+ base_set = set_route_count;
619+ base_remove = remove_route_count;
620+
621+ static_cast <Orch *>(gRouteOrch )->doTask ();
622+
623+ // Expect another SET (no create/remove)
624+ ASSERT_EQ (base_create, create_route_count);
625+ ASSERT_EQ (base_remove, remove_route_count);
626+ ASSERT_EQ (base_set + 1 , set_route_count);
627+ ASSERT_EQ (sai_fail_count, 0 );
628+
629+ ASSERT_TRUE (waitStateRouteState (m_state_db.get (), def, " ok" ))
630+ << " Expected default-route state to return to 'ok' after re-SET." ;
631+
632+ }
633+ }
634+
400635 TEST_F (RouteOrchTest, RouteOrchTestDelSetSameNexthop)
401636 {
402637 std::deque<KeyOpFieldsValuesTuple> entries;
0 commit comments