...

Source file src/sigs.k8s.io/cli-utils/pkg/testutil/events.go

Documentation: sigs.k8s.io/cli-utils/pkg/testutil

     1  // Copyright 2020 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package testutil
     5  
     6  import (
     7  	"fmt"
     8  	"sort"
     9  
    10  	"github.com/google/go-cmp/cmp"
    11  	"github.com/google/go-cmp/cmp/cmpopts"
    12  	"sigs.k8s.io/cli-utils/pkg/apply/event"
    13  	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
    14  	"sigs.k8s.io/cli-utils/pkg/object"
    15  )
    16  
    17  type ExpEvent struct {
    18  	EventType event.Type
    19  
    20  	InitEvent        *ExpInitEvent
    21  	ErrorEvent       *ExpErrorEvent
    22  	ActionGroupEvent *ExpActionGroupEvent
    23  	ApplyEvent       *ExpApplyEvent
    24  	StatusEvent      *ExpStatusEvent
    25  	PruneEvent       *ExpPruneEvent
    26  	DeleteEvent      *ExpDeleteEvent
    27  	WaitEvent        *ExpWaitEvent
    28  	ValidationEvent  *ExpValidationEvent
    29  }
    30  
    31  type ExpInitEvent struct {
    32  	// TODO: enable if we want to more thuroughly test InitEvents
    33  	// ActionGroups []event.ActionGroup
    34  }
    35  
    36  type ExpErrorEvent struct {
    37  	Err error
    38  }
    39  
    40  type ExpActionGroupEvent struct {
    41  	GroupName string
    42  	Action    event.ResourceAction
    43  	Type      event.ActionGroupEventStatus
    44  }
    45  
    46  type ExpApplyEvent struct {
    47  	GroupName  string
    48  	Status     event.ApplyEventStatus
    49  	Identifier object.ObjMetadata
    50  	Error      error
    51  }
    52  
    53  type ExpStatusEvent struct {
    54  	Status     status.Status
    55  	Identifier object.ObjMetadata
    56  	Error      error
    57  }
    58  
    59  type ExpPruneEvent struct {
    60  	GroupName  string
    61  	Status     event.PruneEventStatus
    62  	Identifier object.ObjMetadata
    63  	Error      error
    64  }
    65  
    66  type ExpDeleteEvent struct {
    67  	GroupName  string
    68  	Status     event.DeleteEventStatus
    69  	Identifier object.ObjMetadata
    70  	Error      error
    71  }
    72  
    73  type ExpWaitEvent struct {
    74  	GroupName  string
    75  	Status     event.WaitEventStatus
    76  	Identifier object.ObjMetadata
    77  }
    78  
    79  type ExpValidationEvent struct {
    80  	Identifiers object.ObjMetadataSet
    81  	Error       error
    82  }
    83  
    84  func VerifyEvents(expEvents []ExpEvent, events []event.Event) error {
    85  	if len(expEvents) == 0 && len(events) == 0 {
    86  		return nil
    87  	}
    88  	expEventIndex := 0
    89  	for i := range events {
    90  		e := events[i]
    91  		ee := expEvents[expEventIndex]
    92  		if isMatch(ee, e) {
    93  			expEventIndex++
    94  			if expEventIndex >= len(expEvents) {
    95  				return nil
    96  			}
    97  		}
    98  	}
    99  	return fmt.Errorf("event %s not found", expEvents[expEventIndex].EventType)
   100  }
   101  
   102  // nolint:gocyclo
   103  // TODO(mortent): This function is pretty complex and with quite a bit of
   104  // duplication. We should see if there is a better way to provide a flexible
   105  // way to verify that we go the expected events.
   106  func isMatch(ee ExpEvent, e event.Event) bool {
   107  	if ee.EventType != e.Type {
   108  		return false
   109  	}
   110  
   111  	// nolint:gocritic
   112  	switch e.Type {
   113  	case event.ErrorType:
   114  		a := ee.ErrorEvent
   115  
   116  		if a == nil {
   117  			return true
   118  		}
   119  
   120  		b := e.ErrorEvent
   121  
   122  		if a.Err != nil {
   123  			if !cmp.Equal(a.Err, b.Err, cmpopts.EquateErrors()) {
   124  				return false
   125  			}
   126  		}
   127  		return true
   128  
   129  	case event.ActionGroupType:
   130  		agee := ee.ActionGroupEvent
   131  
   132  		if agee == nil {
   133  			return true
   134  		}
   135  
   136  		age := e.ActionGroupEvent
   137  
   138  		if agee.GroupName != age.GroupName {
   139  			return false
   140  		}
   141  
   142  		if agee.Action != age.Action {
   143  			return false
   144  		}
   145  
   146  		if agee.Type != age.Status {
   147  			return false
   148  		}
   149  		return true
   150  
   151  	case event.ApplyType:
   152  		aee := ee.ApplyEvent
   153  		// If no more information is specified, we consider it a match.
   154  		if aee == nil {
   155  			return true
   156  		}
   157  		ae := e.ApplyEvent
   158  
   159  		if aee.Identifier != object.NilObjMetadata {
   160  			if aee.Identifier != ae.Identifier {
   161  				return false
   162  			}
   163  		}
   164  
   165  		if aee.GroupName != "" {
   166  			if aee.GroupName != ae.GroupName {
   167  				return false
   168  			}
   169  		}
   170  
   171  		if aee.Status != ae.Status {
   172  			return false
   173  		}
   174  
   175  		if aee.Error != nil {
   176  			return ae.Error != nil
   177  		}
   178  		return ae.Error == nil
   179  
   180  	case event.StatusType:
   181  		see := ee.StatusEvent
   182  		if see == nil {
   183  			return true
   184  		}
   185  		se := e.StatusEvent
   186  
   187  		if see.Identifier != se.Identifier {
   188  			return false
   189  		}
   190  
   191  		if see.Status != se.PollResourceInfo.Status {
   192  			return false
   193  		}
   194  
   195  		if see.Error != nil {
   196  			return se.Error != nil
   197  		}
   198  		return se.Error == nil
   199  
   200  	case event.PruneType:
   201  		pee := ee.PruneEvent
   202  		if pee == nil {
   203  			return true
   204  		}
   205  		pe := e.PruneEvent
   206  
   207  		if pee.Identifier != object.NilObjMetadata {
   208  			if pee.Identifier != pe.Identifier {
   209  				return false
   210  			}
   211  		}
   212  
   213  		if pee.GroupName != "" {
   214  			if pee.GroupName != pe.GroupName {
   215  				return false
   216  			}
   217  		}
   218  
   219  		if pee.Status != pe.Status {
   220  			return false
   221  		}
   222  
   223  		if pee.Error != nil {
   224  			return pe.Error != nil
   225  		}
   226  		return pe.Error == nil
   227  
   228  	case event.DeleteType:
   229  		dee := ee.DeleteEvent
   230  		if dee == nil {
   231  			return true
   232  		}
   233  		de := e.DeleteEvent
   234  
   235  		if dee.Identifier != object.NilObjMetadata {
   236  			if dee.Identifier != de.Identifier {
   237  				return false
   238  			}
   239  		}
   240  
   241  		if dee.GroupName != "" {
   242  			if dee.GroupName != de.GroupName {
   243  				return false
   244  			}
   245  		}
   246  
   247  		if dee.Status != de.Status {
   248  			return false
   249  		}
   250  
   251  		if dee.Error != nil {
   252  			return de.Error != nil
   253  		}
   254  		return de.Error == nil
   255  
   256  	case event.WaitType:
   257  		wee := ee.WaitEvent
   258  		if wee == nil {
   259  			return true
   260  		}
   261  		we := e.WaitEvent
   262  
   263  		if wee.Identifier != object.NilObjMetadata {
   264  			if wee.Identifier != we.Identifier {
   265  				return false
   266  			}
   267  		}
   268  
   269  		if wee.GroupName != "" {
   270  			if wee.GroupName != we.GroupName {
   271  				return false
   272  			}
   273  		}
   274  
   275  		if wee.Status != we.Status {
   276  			return false
   277  		}
   278  		return true
   279  
   280  	case event.ValidationType:
   281  		vee := ee.ValidationEvent
   282  		if vee == nil {
   283  			return true
   284  		}
   285  		ve := e.ValidationEvent
   286  
   287  		if vee.Identifiers != nil {
   288  			if !vee.Identifiers.Equal(ve.Identifiers) {
   289  				return false
   290  			}
   291  		}
   292  
   293  		if vee.Error != nil {
   294  			return ve.Error != nil
   295  		}
   296  		return ve.Error == nil
   297  
   298  	default:
   299  		return true
   300  	}
   301  }
   302  
   303  func EventsToExpEvents(events []event.Event) []ExpEvent {
   304  	result := make([]ExpEvent, 0, len(events))
   305  	for _, event := range events {
   306  		result = append(result, EventToExpEvent(event))
   307  	}
   308  	return result
   309  }
   310  
   311  func EventToExpEvent(e event.Event) ExpEvent {
   312  	switch e.Type {
   313  	case event.InitType:
   314  		return ExpEvent{
   315  			EventType: event.InitType,
   316  			InitEvent: &ExpInitEvent{
   317  				// TODO: enable if we want to more thuroughly test InitEvents
   318  				// ActionGroups: e.InitEvent.ActionGroups,
   319  			},
   320  		}
   321  
   322  	case event.ErrorType:
   323  		return ExpEvent{
   324  			EventType: event.ErrorType,
   325  			ErrorEvent: &ExpErrorEvent{
   326  				Err: e.ErrorEvent.Err,
   327  			},
   328  		}
   329  
   330  	case event.ActionGroupType:
   331  		return ExpEvent{
   332  			EventType: event.ActionGroupType,
   333  			ActionGroupEvent: &ExpActionGroupEvent{
   334  				GroupName: e.ActionGroupEvent.GroupName,
   335  				Action:    e.ActionGroupEvent.Action,
   336  				Type:      e.ActionGroupEvent.Status,
   337  			},
   338  		}
   339  
   340  	case event.ApplyType:
   341  		return ExpEvent{
   342  			EventType: event.ApplyType,
   343  			ApplyEvent: &ExpApplyEvent{
   344  				GroupName:  e.ApplyEvent.GroupName,
   345  				Identifier: e.ApplyEvent.Identifier,
   346  				Status:     e.ApplyEvent.Status,
   347  				Error:      e.ApplyEvent.Error,
   348  			},
   349  		}
   350  
   351  	case event.StatusType:
   352  		return ExpEvent{
   353  			EventType: event.StatusType,
   354  			StatusEvent: &ExpStatusEvent{
   355  				Identifier: e.StatusEvent.Identifier,
   356  				Status:     e.StatusEvent.PollResourceInfo.Status,
   357  				Error:      e.StatusEvent.Error,
   358  			},
   359  		}
   360  
   361  	case event.PruneType:
   362  		return ExpEvent{
   363  			EventType: event.PruneType,
   364  			PruneEvent: &ExpPruneEvent{
   365  				GroupName:  e.PruneEvent.GroupName,
   366  				Identifier: e.PruneEvent.Identifier,
   367  				Status:     e.PruneEvent.Status,
   368  				Error:      e.PruneEvent.Error,
   369  			},
   370  		}
   371  
   372  	case event.DeleteType:
   373  		return ExpEvent{
   374  			EventType: event.DeleteType,
   375  			DeleteEvent: &ExpDeleteEvent{
   376  				GroupName:  e.DeleteEvent.GroupName,
   377  				Identifier: e.DeleteEvent.Identifier,
   378  				Status:     e.DeleteEvent.Status,
   379  				Error:      e.DeleteEvent.Error,
   380  			},
   381  		}
   382  
   383  	case event.WaitType:
   384  		return ExpEvent{
   385  			EventType: event.WaitType,
   386  			WaitEvent: &ExpWaitEvent{
   387  				GroupName:  e.WaitEvent.GroupName,
   388  				Identifier: e.WaitEvent.Identifier,
   389  				Status:     e.WaitEvent.Status,
   390  			},
   391  		}
   392  
   393  	case event.ValidationType:
   394  		return ExpEvent{
   395  			EventType: event.ValidationType,
   396  			ValidationEvent: &ExpValidationEvent{
   397  				Identifiers: e.ValidationEvent.Identifiers,
   398  				Error:       e.ValidationEvent.Error,
   399  			},
   400  		}
   401  	}
   402  	return ExpEvent{}
   403  }
   404  
   405  func RemoveEqualEvents(in []ExpEvent, expected ExpEvent) ([]ExpEvent, int) {
   406  	matches := 0
   407  	for i := 0; i < len(in); i++ {
   408  		if cmp.Equal(in[i], expected, cmpopts.EquateErrors()) {
   409  			// remove event at index i
   410  			in = append(in[:i], in[i+1:]...)
   411  			matches++
   412  			i--
   413  		}
   414  	}
   415  	return in, matches
   416  }
   417  
   418  // SortExpEvents sorts a list of ExpEvents so they can be compared for equality.
   419  //
   420  // This is a stable sort which only sorts nearly identical contiguous events by
   421  // object identifier, to make the full list easier to validate.
   422  //
   423  // You may need to remove StatusEvents from the list before comparing, because
   424  // these events are fully asynchronous and non-contiguous.
   425  //
   426  // Comparison Options:
   427  // A) Expect(received).To(testutil.Equal(expected))
   428  // B) testutil.assertEqual(t, expected, received)
   429  func SortExpEvents(events []ExpEvent) {
   430  	sort.SliceStable(events, GroupedEventsByID(events).Less)
   431  }
   432  
   433  // GroupedEventsByID implements sort.Interface for []ExpEvent based on
   434  // the serialized ObjMetadata of Apply, Prune, and Delete events within the same
   435  // task group.
   436  // This makes testing events easier, because apply/prune/delete order is
   437  // non-deterministic within each task group.
   438  // This is only needed if you expect to have multiple apply/prune/delete events
   439  // in the same task group.
   440  type GroupedEventsByID []ExpEvent
   441  
   442  func (ape GroupedEventsByID) Len() int      { return len(ape) }
   443  func (ape GroupedEventsByID) Swap(i, j int) { ape[i], ape[j] = ape[j], ape[i] }
   444  func (ape GroupedEventsByID) Less(i, j int) bool {
   445  	if ape[i].EventType != ape[j].EventType {
   446  		// don't change order if not the same type
   447  		return i < j
   448  	}
   449  	switch ape[i].EventType {
   450  	case event.ValidationType:
   451  		// Validation events are predictable ordered by input object set order.
   452  	case event.ApplyType:
   453  		// Apply events are are predictably ordered by ordering.SortableMetas.
   454  	case event.PruneType:
   455  		// Prune events are predictably ordered in reverse apply order.
   456  	case event.DeleteType:
   457  		// Delete events are predictably ordered in reverse apply order.
   458  	case event.WaitType:
   459  		// Wait events are unpredictably ordered, because the status may
   460  		// reconcile before or after the WaitTask starts, and status event
   461  		// order after starting is dependent on remote controller behavior.
   462  		// So here we sort status groups explicitly:
   463  		// Pending > Skipped > Successful > Failed > Timeout.
   464  		// Each status group is then sorted by Identifier:
   465  		// Group > Kind > Namespace > Name.
   466  		// Note that the Pending status is always optional.
   467  		if ape[i].WaitEvent.GroupName == ape[j].WaitEvent.GroupName {
   468  			if ape[i].WaitEvent.Status != ape[j].WaitEvent.Status {
   469  				return lessWaitStatus(ape[i].WaitEvent.Status, ape[j].WaitEvent.Status)
   470  			}
   471  			return ape[i].WaitEvent.Identifier.String() < ape[j].WaitEvent.Identifier.String()
   472  		}
   473  	}
   474  	return i < j
   475  }
   476  
   477  var waitStatusWeight = map[event.WaitEventStatus]int{
   478  	event.ReconcilePending:    0,
   479  	event.ReconcileSkipped:    1,
   480  	event.ReconcileSuccessful: 2,
   481  	event.ReconcileFailed:     3,
   482  	event.ReconcileTimeout:    4,
   483  }
   484  
   485  func lessWaitStatus(x, y event.WaitEventStatus) bool {
   486  	return waitStatusWeight[x] < waitStatusWeight[y]
   487  }
   488  

View as plain text