sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/tree/tree_test.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package tree
    18  
    19  import (
    20  	"strings"
    21  	"testing"
    22  	"time"
    23  
    24  	. "github.com/onsi/gomega"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    27  	"k8s.io/apimachinery/pkg/types"
    28  	"sigs.k8s.io/controller-runtime/pkg/client"
    29  
    30  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    31  	"sigs.k8s.io/cluster-api/util/conditions"
    32  )
    33  
    34  func Test_hasSameReadyStatusSeverityAndReason(t *testing.T) {
    35  	readyTrue := conditions.TrueCondition(clusterv1.ReadyCondition)
    36  	readyFalseReasonInfo := conditions.FalseCondition(clusterv1.ReadyCondition, "Reason", clusterv1.ConditionSeverityInfo, "message falseInfo1")
    37  	readyFalseAnotherReasonInfo := conditions.FalseCondition(clusterv1.ReadyCondition, "AnotherReason", clusterv1.ConditionSeverityInfo, "message falseInfo1")
    38  	readyFalseReasonWarning := conditions.FalseCondition(clusterv1.ReadyCondition, "Reason", clusterv1.ConditionSeverityWarning, "message falseInfo1")
    39  
    40  	type args struct {
    41  		a *clusterv1.Condition
    42  		b *clusterv1.Condition
    43  	}
    44  	tests := []struct {
    45  		name string
    46  		args args
    47  		want bool
    48  	}{
    49  		{
    50  			name: "Objects without conditions are the same",
    51  			args: args{
    52  				a: nil,
    53  				b: nil,
    54  			},
    55  			want: true,
    56  		},
    57  		{
    58  			name: "Objects with same Ready condition are the same",
    59  			args: args{
    60  				a: readyTrue,
    61  				b: readyTrue,
    62  			},
    63  			want: true,
    64  		},
    65  		{
    66  			name: "Objects with different Ready.Status are not the same",
    67  			args: args{
    68  				a: readyTrue,
    69  				b: readyFalseReasonInfo,
    70  			},
    71  			want: false,
    72  		},
    73  		{
    74  			name: "Objects with different Ready.Reason are not the same",
    75  			args: args{
    76  				a: readyFalseReasonInfo,
    77  				b: readyFalseAnotherReasonInfo,
    78  			},
    79  			want: false,
    80  		},
    81  		{
    82  			name: "Objects with different Ready.Severity are not the same",
    83  			args: args{
    84  				a: readyFalseReasonInfo,
    85  				b: readyFalseReasonWarning,
    86  			},
    87  			want: false,
    88  		},
    89  	}
    90  	for _, tt := range tests {
    91  		t.Run(tt.name, func(t *testing.T) {
    92  			g := NewWithT(t)
    93  
    94  			got := hasSameReadyStatusSeverityAndReason(tt.args.a, tt.args.b)
    95  			g.Expect(got).To(Equal(tt.want))
    96  		})
    97  	}
    98  }
    99  
   100  func Test_minLastTransitionTime(t *testing.T) {
   101  	now := &clusterv1.Condition{Type: "now", LastTransitionTime: metav1.Now()}
   102  	beforeNow := &clusterv1.Condition{Type: "beforeNow", LastTransitionTime: metav1.Time{Time: now.LastTransitionTime.Time.Add(-1 * time.Hour)}}
   103  	type args struct {
   104  		a *clusterv1.Condition
   105  		b *clusterv1.Condition
   106  	}
   107  	tests := []struct {
   108  		name string
   109  		args args
   110  		want metav1.Time
   111  	}{
   112  		{
   113  			name: "nil, nil should return empty time",
   114  			args: args{
   115  				a: nil,
   116  				b: nil,
   117  			},
   118  			want: metav1.Time{},
   119  		},
   120  		{
   121  			name: "nil, now should return now",
   122  			args: args{
   123  				a: nil,
   124  				b: now,
   125  			},
   126  			want: now.LastTransitionTime,
   127  		},
   128  		{
   129  			name: "now, nil should return now",
   130  			args: args{
   131  				a: now,
   132  				b: nil,
   133  			},
   134  			want: now.LastTransitionTime,
   135  		},
   136  		{
   137  			name: "now, beforeNow should return beforeNow",
   138  			args: args{
   139  				a: now,
   140  				b: beforeNow,
   141  			},
   142  			want: beforeNow.LastTransitionTime,
   143  		},
   144  		{
   145  			name: "beforeNow, now should return beforeNow",
   146  			args: args{
   147  				a: now,
   148  				b: beforeNow,
   149  			},
   150  			want: beforeNow.LastTransitionTime,
   151  		},
   152  	}
   153  	for _, tt := range tests {
   154  		t.Run(tt.name, func(t *testing.T) {
   155  			g := NewWithT(t)
   156  
   157  			got := minLastTransitionTime(tt.args.a, tt.args.b)
   158  			g.Expect(got.Time).To(BeTemporally("~", tt.want.Time))
   159  		})
   160  	}
   161  }
   162  
   163  func Test_isObjDebug(t *testing.T) {
   164  	obj := fakeMachine("my-machine")
   165  	type args struct {
   166  		filter string
   167  	}
   168  	tests := []struct {
   169  		name string
   170  		args args
   171  		want bool
   172  	}{
   173  		{
   174  			name: "empty filter should return false",
   175  			args: args{
   176  				filter: "",
   177  			},
   178  			want: false,
   179  		},
   180  		{
   181  			name: "all filter should return true",
   182  			args: args{
   183  				filter: "all",
   184  			},
   185  			want: true,
   186  		},
   187  		{
   188  			name: "kind filter should return true",
   189  			args: args{
   190  				filter: "Machine",
   191  			},
   192  			want: true,
   193  		},
   194  		{
   195  			name: "another kind filter should return false",
   196  			args: args{
   197  				filter: "AnotherKind",
   198  			},
   199  			want: false,
   200  		},
   201  		{
   202  			name: "kind/name filter should return true",
   203  			args: args{
   204  				filter: "Machine/my-machine",
   205  			},
   206  			want: true,
   207  		},
   208  		{
   209  			name: "kind/wrong name filter should return false",
   210  			args: args{
   211  				filter: "Cluster/another-cluster",
   212  			},
   213  			want: false,
   214  		},
   215  	}
   216  	for _, tt := range tests {
   217  		t.Run(tt.name, func(t *testing.T) {
   218  			g := NewWithT(t)
   219  
   220  			got := isObjDebug(obj, tt.args.filter)
   221  			g.Expect(got).To(Equal(tt.want))
   222  		})
   223  	}
   224  }
   225  
   226  func Test_createGroupNode(t *testing.T) {
   227  	now := metav1.Now()
   228  	beforeNow := metav1.Time{Time: now.Time.Add(-1 * time.Hour)}
   229  
   230  	obj := &clusterv1.Machine{
   231  		TypeMeta: metav1.TypeMeta{
   232  			Kind: "Machine",
   233  		},
   234  		ObjectMeta: metav1.ObjectMeta{
   235  			Namespace: "ns",
   236  			Name:      "my-machine",
   237  		},
   238  		Status: clusterv1.MachineStatus{
   239  			Conditions: clusterv1.Conditions{
   240  				clusterv1.Condition{Type: clusterv1.ReadyCondition, LastTransitionTime: now},
   241  			},
   242  		},
   243  	}
   244  
   245  	sibling := &clusterv1.Machine{
   246  		TypeMeta: metav1.TypeMeta{
   247  			Kind: "Machine",
   248  		},
   249  		ObjectMeta: metav1.ObjectMeta{
   250  			Namespace: "ns",
   251  			Name:      "sibling-machine",
   252  		},
   253  		Status: clusterv1.MachineStatus{
   254  			Conditions: clusterv1.Conditions{
   255  				clusterv1.Condition{Type: clusterv1.ReadyCondition, LastTransitionTime: beforeNow},
   256  			},
   257  		},
   258  	}
   259  
   260  	want := &unstructured.Unstructured{
   261  		Object: map[string]interface{}{
   262  			"apiVersion": "virtual.cluster.x-k8s.io/v1beta1",
   263  			"kind":       "MachineGroup",
   264  			"metadata": map[string]interface{}{
   265  				"namespace": "ns",
   266  				"name":      "", // random string
   267  				"annotations": map[string]interface{}{
   268  					VirtualObjectAnnotation: "True",
   269  					GroupObjectAnnotation:   "True",
   270  					GroupItemsAnnotation:    "my-machine, sibling-machine",
   271  				},
   272  				"uid": "", // random string
   273  			},
   274  			"status": map[string]interface{}{
   275  				"conditions": []interface{}{
   276  					map[string]interface{}{
   277  						"status":             "",
   278  						"lastTransitionTime": beforeNow.Time.UTC().Format(time.RFC3339),
   279  						"type":               "Ready",
   280  					},
   281  				},
   282  			},
   283  		},
   284  	}
   285  
   286  	g := NewWithT(t)
   287  	got := createGroupNode(sibling, GetReadyCondition(sibling), obj, GetReadyCondition(obj))
   288  
   289  	// Some values are generated randomly, so pick up them.
   290  	want.SetName(got.GetName())
   291  	want.SetUID(got.GetUID())
   292  
   293  	g.Expect(got).To(BeComparableTo(want))
   294  }
   295  
   296  func Test_updateGroupNode(t *testing.T) {
   297  	now := metav1.Now()
   298  	beforeNow := metav1.Time{Time: now.Time.Add(-1 * time.Hour)}
   299  
   300  	group := &unstructured.Unstructured{
   301  		Object: map[string]interface{}{
   302  			"apiVersion": "virtual.cluster.x-k8s.io/v1beta1",
   303  			"kind":       "MachineGroup",
   304  			"metadata": map[string]interface{}{
   305  				"namespace": "ns",
   306  				"name":      "random-name",
   307  				"annotations": map[string]interface{}{
   308  					VirtualObjectAnnotation: "True",
   309  					GroupObjectAnnotation:   "True",
   310  					GroupItemsAnnotation:    "my-machine, sibling-machine",
   311  				},
   312  				"uid": "random-uid",
   313  			},
   314  			"status": map[string]interface{}{
   315  				"conditions": []interface{}{
   316  					map[string]interface{}{
   317  						"status":             "",
   318  						"lastTransitionTime": beforeNow.Time.UTC().Format(time.RFC3339),
   319  						"type":               "Ready",
   320  					},
   321  				},
   322  			},
   323  		},
   324  	}
   325  
   326  	obj := &clusterv1.Machine{
   327  		TypeMeta: metav1.TypeMeta{
   328  			Kind: "Machine",
   329  		},
   330  		ObjectMeta: metav1.ObjectMeta{
   331  			Namespace: "ns",
   332  			Name:      "another-machine",
   333  		},
   334  		Status: clusterv1.MachineStatus{
   335  			Conditions: clusterv1.Conditions{
   336  				clusterv1.Condition{Type: clusterv1.ReadyCondition, LastTransitionTime: now},
   337  			},
   338  		},
   339  	}
   340  
   341  	want := &unstructured.Unstructured{
   342  		Object: map[string]interface{}{
   343  			"apiVersion": "virtual.cluster.x-k8s.io/v1beta1",
   344  			"kind":       "MachineGroup",
   345  			"metadata": map[string]interface{}{
   346  				"namespace": "ns",
   347  				"name":      "random-name",
   348  				"annotations": map[string]interface{}{
   349  					VirtualObjectAnnotation: "True",
   350  					GroupObjectAnnotation:   "True",
   351  					GroupItemsAnnotation:    "another-machine, my-machine, sibling-machine",
   352  				},
   353  				"uid": "random-uid",
   354  			},
   355  			"status": map[string]interface{}{
   356  				"conditions": []interface{}{
   357  					map[string]interface{}{
   358  						"status":             "",
   359  						"lastTransitionTime": beforeNow.Time.UTC().Format(time.RFC3339),
   360  						"type":               "Ready",
   361  					},
   362  				},
   363  			},
   364  		},
   365  	}
   366  
   367  	g := NewWithT(t)
   368  	updateGroupNode(group, GetReadyCondition(group), obj, GetReadyCondition(obj))
   369  
   370  	g.Expect(group).To(BeComparableTo(want))
   371  }
   372  
   373  func Test_Add_setsShowObjectConditionsAnnotation(t *testing.T) {
   374  	parent := fakeCluster("parent")
   375  	obj := fakeMachine("my-machine")
   376  
   377  	type args struct {
   378  		treeOptions ObjectTreeOptions
   379  	}
   380  	tests := []struct {
   381  		name string
   382  		args args
   383  		want bool
   384  	}{
   385  		{
   386  			name: "filter selecting my machine should not add the annotation",
   387  			args: args{
   388  				treeOptions: ObjectTreeOptions{ShowOtherConditions: "all"},
   389  			},
   390  			want: true,
   391  		},
   392  		{
   393  			name: "filter not selecting my machine should not add the annotation",
   394  			args: args{
   395  				treeOptions: ObjectTreeOptions{ShowOtherConditions: ""},
   396  			},
   397  			want: false,
   398  		},
   399  	}
   400  	for _, tt := range tests {
   401  		t.Run(tt.name, func(t *testing.T) {
   402  			root := parent.DeepCopy()
   403  			tree := NewObjectTree(root, tt.args.treeOptions)
   404  
   405  			g := NewWithT(t)
   406  			getAdded, gotVisible := tree.Add(root, obj.DeepCopy())
   407  			g.Expect(getAdded).To(BeTrue())
   408  			g.Expect(gotVisible).To(BeTrue())
   409  
   410  			gotObj := tree.GetObject("my-machine")
   411  			g.Expect(gotObj).ToNot(BeNil())
   412  			switch tt.want {
   413  			case true:
   414  				g.Expect(gotObj.GetAnnotations()).To(HaveKey(ShowObjectConditionsAnnotation))
   415  				g.Expect(gotObj.GetAnnotations()[ShowObjectConditionsAnnotation]).To(Equal("True"))
   416  			case false:
   417  				g.Expect(gotObj.GetAnnotations()).ToNot(HaveKey(ShowObjectConditionsAnnotation))
   418  			}
   419  		})
   420  	}
   421  }
   422  
   423  func Test_Add_setsGroupingObjectAnnotation(t *testing.T) {
   424  	parent := fakeCluster("parent")
   425  	obj := fakeMachine("my-machine")
   426  
   427  	type args struct {
   428  		treeOptions ObjectTreeOptions
   429  		addOptions  []AddObjectOption
   430  	}
   431  	tests := []struct {
   432  		name string
   433  		args args
   434  		want bool
   435  	}{
   436  		{
   437  			name: "should not add the annotation if not requested to",
   438  			args: args{
   439  				treeOptions: ObjectTreeOptions{},
   440  				addOptions:  nil, // without GroupingObject option
   441  			},
   442  			want: false,
   443  		},
   444  		{
   445  			name: "should add the annotation if requested to and grouping is enabled",
   446  			args: args{
   447  				treeOptions: ObjectTreeOptions{Grouping: true},
   448  				addOptions:  []AddObjectOption{GroupingObject(true)},
   449  			},
   450  			want: true,
   451  		},
   452  		{
   453  			name: "should not add the annotation if requested to, but grouping is disabled",
   454  			args: args{
   455  				treeOptions: ObjectTreeOptions{Grouping: false},
   456  				addOptions:  []AddObjectOption{GroupingObject(true)},
   457  			},
   458  			want: false,
   459  		},
   460  	}
   461  	for _, tt := range tests {
   462  		t.Run(tt.name, func(t *testing.T) {
   463  			root := parent.DeepCopy()
   464  			tree := NewObjectTree(root, tt.args.treeOptions)
   465  
   466  			g := NewWithT(t)
   467  			getAdded, gotVisible := tree.Add(root, obj.DeepCopy(), tt.args.addOptions...)
   468  			g.Expect(getAdded).To(BeTrue())
   469  			g.Expect(gotVisible).To(BeTrue())
   470  
   471  			gotObj := tree.GetObject("my-machine")
   472  			g.Expect(gotObj).ToNot(BeNil())
   473  			switch tt.want {
   474  			case true:
   475  				g.Expect(gotObj.GetAnnotations()).To(HaveKey(GroupingObjectAnnotation))
   476  				g.Expect(gotObj.GetAnnotations()[GroupingObjectAnnotation]).To(Equal("True"))
   477  			case false:
   478  				g.Expect(gotObj.GetAnnotations()).ToNot(HaveKey(GroupingObjectAnnotation))
   479  			}
   480  		})
   481  	}
   482  }
   483  
   484  func Test_Add_setsObjectMetaNameAnnotation(t *testing.T) {
   485  	parent := fakeCluster("parent")
   486  	obj := fakeMachine("my-machine")
   487  
   488  	type args struct {
   489  		addOptions []AddObjectOption
   490  	}
   491  	tests := []struct {
   492  		name string
   493  		args args
   494  		want bool
   495  	}{
   496  		{
   497  			name: "should not add the annotation if not requested to",
   498  			args: args{
   499  				addOptions: nil, // without ObjectMetaName option
   500  			},
   501  			want: false,
   502  		},
   503  		{
   504  			name: "should add the annotation if requested to",
   505  			args: args{
   506  				addOptions: []AddObjectOption{ObjectMetaName("MetaName")},
   507  			},
   508  			want: true,
   509  		},
   510  	}
   511  	for _, tt := range tests {
   512  		t.Run(tt.name, func(t *testing.T) {
   513  			root := parent.DeepCopy()
   514  			tree := NewObjectTree(root, ObjectTreeOptions{})
   515  
   516  			g := NewWithT(t)
   517  			getAdded, gotVisible := tree.Add(root, obj.DeepCopy(), tt.args.addOptions...)
   518  			g.Expect(getAdded).To(BeTrue())
   519  			g.Expect(gotVisible).To(BeTrue())
   520  
   521  			gotObj := tree.GetObject("my-machine")
   522  			g.Expect(gotObj).ToNot(BeNil())
   523  			switch tt.want {
   524  			case true:
   525  				g.Expect(gotObj.GetAnnotations()).To(HaveKey(ObjectMetaNameAnnotation))
   526  				g.Expect(gotObj.GetAnnotations()[ObjectMetaNameAnnotation]).To(Equal("MetaName"))
   527  			case false:
   528  				g.Expect(gotObj.GetAnnotations()).ToNot(HaveKey(ObjectMetaNameAnnotation))
   529  			}
   530  		})
   531  	}
   532  }
   533  
   534  func Test_Add_NoEcho(t *testing.T) {
   535  	parent := fakeCluster("parent",
   536  		withClusterCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
   537  	)
   538  
   539  	type args struct {
   540  		treeOptions ObjectTreeOptions
   541  		addOptions  []AddObjectOption
   542  		obj         *clusterv1.Machine
   543  	}
   544  	tests := []struct {
   545  		name     string
   546  		args     args
   547  		wantNode bool
   548  	}{
   549  		{
   550  			name: "should always add if NoEcho option is not present",
   551  			args: args{
   552  				treeOptions: ObjectTreeOptions{},
   553  				addOptions:  nil,
   554  				obj: fakeMachine("my-machine",
   555  					withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
   556  				),
   557  			},
   558  			wantNode: true,
   559  		},
   560  		{
   561  			name: "should not add if NoEcho option is present and objects have same ReadyCondition",
   562  			args: args{
   563  				treeOptions: ObjectTreeOptions{},
   564  				addOptions:  []AddObjectOption{NoEcho(true)},
   565  				obj: fakeMachine("my-machine",
   566  					withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
   567  				),
   568  			},
   569  			wantNode: false,
   570  		},
   571  		{
   572  			name: "should add if NoEcho option is present but objects have not same ReadyCondition",
   573  			args: args{
   574  				treeOptions: ObjectTreeOptions{},
   575  				addOptions:  []AddObjectOption{NoEcho(true)},
   576  				obj: fakeMachine("my-machine",
   577  					withMachineCondition(conditions.FalseCondition(clusterv1.ReadyCondition, "", clusterv1.ConditionSeverityInfo, "")),
   578  				),
   579  			},
   580  			wantNode: true,
   581  		},
   582  		{
   583  			name: "should add if NoEcho option is present, objects have same ReadyCondition, but NoEcho is disabled",
   584  			args: args{
   585  				treeOptions: ObjectTreeOptions{Echo: true},
   586  				addOptions:  []AddObjectOption{NoEcho(true)},
   587  				obj: fakeMachine("my-machine",
   588  					withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
   589  				),
   590  			},
   591  			wantNode: true,
   592  		},
   593  	}
   594  	for _, tt := range tests {
   595  		t.Run(tt.name, func(t *testing.T) {
   596  			root := parent.DeepCopy()
   597  			tree := NewObjectTree(root, tt.args.treeOptions)
   598  
   599  			g := NewWithT(t)
   600  			getAdded, gotVisible := tree.Add(root, tt.args.obj, tt.args.addOptions...)
   601  			g.Expect(getAdded).To(Equal(tt.wantNode))
   602  			g.Expect(gotVisible).To(Equal(tt.wantNode))
   603  
   604  			gotObj := tree.GetObject("my-machine")
   605  			switch tt.wantNode {
   606  			case true:
   607  				g.Expect(gotObj).ToNot(BeNil())
   608  			case false:
   609  				g.Expect(gotObj).To(BeNil())
   610  			}
   611  		})
   612  	}
   613  }
   614  
   615  func Test_Add_Grouping(t *testing.T) {
   616  	parent := fakeCluster("parent",
   617  		withClusterAnnotation(GroupingObjectAnnotation, "True"),
   618  	)
   619  
   620  	type args struct {
   621  		addOptions []AddObjectOption
   622  		siblings   []client.Object
   623  		obj        client.Object
   624  	}
   625  	tests := []struct {
   626  		name            string
   627  		args            args
   628  		wantNodesPrefix []string
   629  		wantVisible     bool
   630  		wantItems       string
   631  	}{
   632  		{
   633  			name: "should never group the first child object",
   634  			args: args{
   635  				obj: fakeMachine("my-machine"),
   636  			},
   637  			wantNodesPrefix: []string{"my-machine"},
   638  			wantVisible:     true,
   639  		},
   640  		{
   641  			name: "should group child node if it has same kind and conditions of an existing one",
   642  			args: args{
   643  				siblings: []client.Object{
   644  					fakeMachine("first-machine",
   645  						withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
   646  					),
   647  				},
   648  				obj: fakeMachine("second-machine",
   649  					withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
   650  				),
   651  			},
   652  			wantNodesPrefix: []string{"zz_True"},
   653  			wantVisible:     false,
   654  			wantItems:       "first-machine, second-machine",
   655  		},
   656  		{
   657  			name: "should group child node if it has same kind and conditions of an existing group",
   658  			args: args{
   659  				siblings: []client.Object{
   660  					fakeMachine("first-machine",
   661  						withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
   662  					),
   663  					fakeMachine("second-machine",
   664  						withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
   665  					),
   666  				},
   667  				obj: fakeMachine("third-machine",
   668  					withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
   669  				),
   670  			},
   671  			wantNodesPrefix: []string{"zz_True"},
   672  			wantVisible:     false,
   673  			wantItems:       "first-machine, second-machine, third-machine",
   674  		},
   675  		{
   676  			name: "should not group child node if it has different kind",
   677  			args: args{
   678  				siblings: []client.Object{
   679  					fakeMachine("first-machine",
   680  						withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
   681  					),
   682  					fakeMachine("second-machine",
   683  						withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
   684  					),
   685  				},
   686  				obj: VirtualObject("ns", "NotAMachine", "other-object"),
   687  			},
   688  			wantNodesPrefix: []string{"zz_True", "other-object"},
   689  			wantVisible:     true,
   690  			wantItems:       "first-machine, second-machine",
   691  		},
   692  	}
   693  	for _, tt := range tests {
   694  		t.Run(tt.name, func(t *testing.T) {
   695  			root := parent.DeepCopy()
   696  			tree := NewObjectTree(root, ObjectTreeOptions{})
   697  
   698  			for i := range tt.args.siblings {
   699  				tree.Add(parent, tt.args.siblings[i], tt.args.addOptions...)
   700  			}
   701  
   702  			g := NewWithT(t)
   703  			getAdded, gotVisible := tree.Add(root, tt.args.obj, tt.args.addOptions...)
   704  			g.Expect(getAdded).To(BeTrue())
   705  			g.Expect(gotVisible).To(Equal(tt.wantVisible))
   706  
   707  			gotObjs := tree.GetObjectsByParent("parent")
   708  			g.Expect(gotObjs).To(HaveLen(len(tt.wantNodesPrefix)))
   709  			for _, obj := range gotObjs {
   710  				found := false
   711  				for _, prefix := range tt.wantNodesPrefix {
   712  					if strings.HasPrefix(obj.GetName(), prefix) {
   713  						found = true
   714  						break
   715  					}
   716  				}
   717  				g.Expect(found).To(BeTrue(), "Found object with name %q, waiting for one of %s", obj.GetName(), tt.wantNodesPrefix)
   718  
   719  				if strings.HasPrefix(obj.GetName(), "zz_") {
   720  					g.Expect(GetGroupItems(obj)).To(Equal(tt.wantItems))
   721  				}
   722  			}
   723  		})
   724  	}
   725  }
   726  
   727  type clusterOption func(*clusterv1.Cluster)
   728  
   729  func fakeCluster(name string, options ...clusterOption) *clusterv1.Cluster {
   730  	c := &clusterv1.Cluster{
   731  		TypeMeta: metav1.TypeMeta{
   732  			Kind: "Cluster",
   733  		},
   734  		ObjectMeta: metav1.ObjectMeta{
   735  			Namespace: "ns",
   736  			Name:      name,
   737  			UID:       types.UID(name),
   738  		},
   739  	}
   740  	for _, opt := range options {
   741  		opt(c)
   742  	}
   743  	return c
   744  }
   745  
   746  func withClusterAnnotation(name, value string) func(*clusterv1.Cluster) {
   747  	return func(c *clusterv1.Cluster) {
   748  		if c.Annotations == nil {
   749  			c.Annotations = map[string]string{}
   750  		}
   751  		c.Annotations[name] = value
   752  	}
   753  }
   754  
   755  func withClusterCondition(c *clusterv1.Condition) func(*clusterv1.Cluster) {
   756  	return func(m *clusterv1.Cluster) {
   757  		conditions.Set(m, c)
   758  	}
   759  }
   760  
   761  type machineOption func(*clusterv1.Machine)
   762  
   763  func fakeMachine(name string, options ...machineOption) *clusterv1.Machine {
   764  	m := &clusterv1.Machine{
   765  		TypeMeta: metav1.TypeMeta{
   766  			Kind: "Machine",
   767  		},
   768  		ObjectMeta: metav1.ObjectMeta{
   769  			Namespace: "ns",
   770  			Name:      name,
   771  			UID:       types.UID(name),
   772  		},
   773  	}
   774  	for _, opt := range options {
   775  		opt(m)
   776  	}
   777  	return m
   778  }
   779  
   780  func withMachineCondition(c *clusterv1.Condition) func(*clusterv1.Machine) {
   781  	return func(m *clusterv1.Machine) {
   782  		conditions.Set(m, c)
   783  	}
   784  }