@@ -119,6 +119,8 @@ type fixture struct {
119119 // Objects from here preloaded into NewSimpleFake.
120120 kubeobjects []runtime.Object
121121 objects []runtime.Object
122+ // dynamicOnlyObjects: added to dynamic client only (not Argo client). Use for Istio VS/DR so listers don't see unstructured as rollout.
123+ dynamicOnlyObjects []runtime.Object
122124 // Acquire 'enqueuedObjectsLock' before accessing enqueuedObjects
123125 enqueuedObjects map [string ]int
124126 enqueuedObjectsLock sync.Mutex
@@ -127,6 +129,10 @@ type fixture struct {
127129 // events holds all the K8s Event Reasons emitted during the run
128130 events []string
129131 fakeTrafficRouting * mocks.TrafficRoutingReconciler
132+ // reseedRolloutMutator, if set, is applied to the rollout when re-seeding between syncs (for multi-sync tests).
133+ reseedRolloutMutator func (* v1alpha1.Rollout )
134+ // allowErrorOnLastSync, if set, do not fail the test when the final sync returns an error (e.g. "delaying destination rule switch").
135+ allowErrorOnLastSync bool
130136}
131137
132138func newFixture (t * testing.T ) * fixture {
@@ -562,6 +568,11 @@ func (f *fixture) newController(resync resyncFunc) (*Controller, informers.Share
562568 f .client = fake .NewSimpleClientset (f .objects ... )
563569 f .kubeclient = k8sfake .NewSimpleClientset (f .kubeobjects ... )
564570
571+ dynamicClientObjects := f .objects
572+ if len (f .dynamicOnlyObjects ) > 0 {
573+ dynamicClientObjects = append (append ([]runtime.Object {}, f .objects ... ), f .dynamicOnlyObjects ... )
574+ }
575+
565576 i := informers .NewSharedInformerFactory (f .client , resync ())
566577 k8sI := kubeinformers .NewSharedInformerFactory (f .kubeclient , resync ())
567578
@@ -581,7 +592,7 @@ func (f *fixture) newController(resync resyncFunc) (*Controller, informers.Share
581592 destGVR : destGVR .Resource + "List" ,
582593 }
583594
584- dynamicClient := dynamicfake .NewSimpleDynamicClientWithCustomListKinds (scheme , listMapping , f . objects ... )
595+ dynamicClient := dynamicfake .NewSimpleDynamicClientWithCustomListKinds (scheme , listMapping , dynamicClientObjects ... )
585596 dynamicInformerFactory := dynamicinformer .NewDynamicSharedInformerFactory (dynamicClient , 0 )
586597 istioVirtualServiceInformer := dynamicInformerFactory .ForResource (istioutil .GetIstioVirtualServiceGVR ()).Informer ()
587598 istioDestinationRuleInformer := dynamicInformerFactory .ForResource (istioutil .GetIstioDestinationRuleGVR ()).Informer ()
@@ -647,13 +658,15 @@ func (f *fixture) newController(resync resyncFunc) (*Controller, informers.Share
647658 c .enqueueRolloutAfter = func (obj any , duration time.Duration ) {
648659 c .enqueueRollout (obj )
649660 }
650- c .newTrafficRoutingReconciler = func (roCtx * rolloutContext ) ([]trafficrouting.TrafficRoutingReconciler , error ) {
651- if roCtx .rollout .Spec .Strategy .Canary == nil || roCtx .rollout .Spec .Strategy .Canary .TrafficRouting == nil {
652- return nil , nil
661+ if f .fakeTrafficRouting != nil {
662+ c .newTrafficRoutingReconciler = func (roCtx * rolloutContext ) ([]trafficrouting.TrafficRoutingReconciler , error ) {
663+ if roCtx .rollout .Spec .Strategy .Canary == nil || roCtx .rollout .Spec .Strategy .Canary .TrafficRouting == nil {
664+ return nil , nil
665+ }
666+ var reconcilers = []trafficrouting.TrafficRoutingReconciler {}
667+ reconcilers = append (reconcilers , f .fakeTrafficRouting )
668+ return reconcilers , nil
653669 }
654- var reconcilers = []trafficrouting.TrafficRoutingReconciler {}
655- reconcilers = append (reconcilers , f .fakeTrafficRouting )
656- return reconcilers , nil
657670 }
658671
659672 for _ , r := range f .rolloutLister {
@@ -698,11 +711,68 @@ func (f *fixture) run(rolloutName string) {
698711 f .runController (rolloutName , true , false , c , i , k8sI )
699712}
700713
714+ // runWithSyncs runs the controller syncHandler n times (e.g. 2 for two reconciliation loops) then verifies actions.
715+ func (f * fixture ) runWithSyncs (rolloutName string , syncs int ) {
716+ c , i , k8sI := f .newController (noResyncPeriodFunc )
717+ f .runControllerWithSyncs (rolloutName , syncs , true , false , c , i , k8sI )
718+ }
719+
701720func (f * fixture ) runExpectError (rolloutName string , startInformers bool ) {
702721 c , i , k8sI := f .newController (noResyncPeriodFunc )
703722 f .runController (rolloutName , startInformers , true , c , i , k8sI )
704723}
705724
725+ func (f * fixture ) runControllerWithSyncs (rolloutName string , syncs int , startInformers bool , expectError bool , c * Controller , i informers.SharedInformerFactory , k8sI kubeinformers.SharedInformerFactory ) * Controller {
726+ if startInformers {
727+ stopCh := make (chan struct {})
728+ defer close (stopCh )
729+ i .Start (stopCh )
730+ k8sI .Start (stopCh )
731+ assert .True (f .t , cache .WaitForCacheSync (stopCh , c .replicaSetSynced , c .rolloutsSynced ))
732+ }
733+
734+ for n := 0 ; n < syncs ; n ++ {
735+ err := c .syncHandler (context .Background (), rolloutName )
736+ allowErr := f .allowErrorOnLastSync && (n == syncs - 1 )
737+ f .assertSyncHandlerResult (n , syncs , err , expectError , allowErr )
738+ f .reseedRolloutInInformerIfNeeded (c , rolloutName , n , syncs )
739+ }
740+
741+ return f .verifyActionsAndReturn (c )
742+ }
743+
744+ func (f * fixture ) assertSyncHandlerResult (n , syncs int , err error , expectError , allowErr bool ) {
745+ if ! expectError && err != nil && ! allowErr {
746+ f .t .Errorf ("error syncing rollout (sync %d/%d): %v" , n + 1 , syncs , err )
747+ return
748+ }
749+ if expectError && err == nil {
750+ f .t .Error ("expected error syncing rollout, got nil" )
751+ }
752+ }
753+
754+ // reseedRolloutInInformer re-seeds the rollout in the informer so the next sync sees a typed Rollout
755+ // (controller writes Unstructured via persistRolloutToInformer). No-op when syncs <= 1 or on the last sync.
756+ func (f * fixture ) reseedRolloutInInformerIfNeeded (c * Controller , rolloutName string , n , syncs int ) {
757+ if syncs <= 1 || n >= syncs - 1 {
758+ return
759+ }
760+ namespace , name , err := cache .SplitMetaNamespaceKey (rolloutName )
761+ if err != nil {
762+ f .t .Fatalf ("re-seed rollout: split key: %v" , err )
763+ }
764+ ro , err := f .client .ArgoprojV1alpha1 ().Rollouts (namespace ).Get (context .Background (), name , metav1.GetOptions {})
765+ if err != nil {
766+ f .t .Fatalf ("re-seed rollout: get: %v" , err )
767+ }
768+ if f .reseedRolloutMutator != nil {
769+ f .reseedRolloutMutator (ro )
770+ }
771+ if err := c .rolloutsIndexer .Update (ro ); err != nil {
772+ f .t .Fatalf ("re-seed rollout: update indexer: %v" , err )
773+ }
774+ }
775+
706776func (f * fixture ) runController (rolloutName string , startInformers bool , expectError bool , c * Controller , i informers.SharedInformerFactory , k8sI kubeinformers.SharedInformerFactory ) * Controller {
707777 if startInformers {
708778 stopCh := make (chan struct {})
@@ -720,6 +790,10 @@ func (f *fixture) runController(rolloutName string, startInformers bool, expectE
720790 f .t .Error ("expected error syncing rollout, got nil" )
721791 }
722792
793+ return f .verifyActionsAndReturn (c )
794+ }
795+
796+ func (f * fixture ) verifyActionsAndReturn (c * Controller ) * Controller {
723797 actions := filterInformerActions (f .client .Actions ())
724798 for i , action := range actions {
725799 if len (f .actions ) < i + 1 {
@@ -752,7 +826,6 @@ func (f *fixture) runController(rolloutName string, startInformers bool, expectE
752826 f .t .Errorf ("%d expected actions did not happen:%+v" , len (f .kubeactions )- len (k8sActions ), f .kubeactions [len (k8sActions ):])
753827 }
754828 fakeRecorder := c .recorder .(* record.FakeEventRecorder )
755-
756829 f .events = fakeRecorder .Events ()
757830 return c
758831}
@@ -850,6 +923,11 @@ func (f *fixture) expectUpdatePodAction(p *corev1.Pod) int {
850923 return len
851924}
852925
926+ func (f * fixture ) expectGetRolloutAction (rollout * v1alpha1.Rollout ) int {
927+ len := len (f .actions )
928+ f .actions = append (f .actions , core .NewGetAction (v1alpha1 .RolloutGVR , rollout .Namespace , rollout .Name ))
929+ return len
930+ }
853931func (f * fixture ) expectCreateExperimentAction (ex * v1alpha1.Experiment ) int {
854932 action := core .NewCreateAction (schema.GroupVersionResource {Resource : "experiments" }, ex .Namespace , ex )
855933 len := len (f .actions )
0 commit comments