agones.dev/agones@v1.53.0/pkg/metrics/controller_test.go (about)

     1  // Copyright 2018 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package metrics
    16  
    17  import (
    18  	"context"
    19  	"strings"
    20  	"testing"
    21  	"time"
    22  
    23  	agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
    24  	agtesting "agones.dev/agones/pkg/testing"
    25  	"agones.dev/agones/pkg/util/runtime"
    26  	"github.com/google/go-cmp/cmp"
    27  	"github.com/pkg/errors"
    28  	"github.com/sirupsen/logrus"
    29  	"github.com/stretchr/testify/assert"
    30  	"github.com/stretchr/testify/require"
    31  	"go.opencensus.io/metric/metricdata"
    32  	"go.opencensus.io/metric/metricexport"
    33  	corev1 "k8s.io/api/core/v1"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  	"k8s.io/apimachinery/pkg/labels"
    36  	k8sruntime "k8s.io/apimachinery/pkg/runtime"
    37  	"k8s.io/apimachinery/pkg/util/intstr"
    38  	k8stesting "k8s.io/client-go/testing"
    39  )
    40  
    41  const defaultNs = "default"
    42  
    43  type metricExporter struct {
    44  	metrics []*metricdata.Metric
    45  }
    46  
    47  func (e *metricExporter) ExportMetrics(_ context.Context, metrics []*metricdata.Metric) error {
    48  	e.metrics = metrics
    49  	return nil
    50  }
    51  
    52  func serialize(args []string) string {
    53  	return strings.Join(args, "|")
    54  }
    55  
    56  type expectedMetricData struct {
    57  	labels []string
    58  	val    interface{}
    59  }
    60  
    61  func assertMetricData(t *testing.T, exporter *metricExporter, metricName string, expected []expectedMetricData) {
    62  
    63  	expectedValuesAsMap := make(map[string]expectedMetricData)
    64  	for _, e := range expected {
    65  		expectedValuesAsMap[serialize(e.labels)] = e
    66  	}
    67  
    68  	var wantedMetric *metricdata.Metric
    69  	for _, m := range exporter.metrics {
    70  		if m.Descriptor.Name == metricName {
    71  			wantedMetric = m
    72  		}
    73  	}
    74  	require.NotNil(t, wantedMetric, "No metric found with name: %s", metricName)
    75  
    76  	assert.Equal(t, len(expectedValuesAsMap), len(expected), "Multiple entries in 'expected' slice have the exact same labels")
    77  	assert.Equalf(t, len(expectedValuesAsMap), len(wantedMetric.TimeSeries), "number of timeseries does not match under metric: %v", metricName)
    78  	for _, tsd := range wantedMetric.TimeSeries {
    79  		actualLabelValues := make([]string, len(tsd.LabelValues))
    80  		for i, k := range tsd.LabelValues {
    81  			actualLabelValues[i] = k.Value
    82  		}
    83  		e, ok := expectedValuesAsMap[serialize(actualLabelValues)]
    84  		assert.True(t, ok, "no TimeSeries found with labels: %v", actualLabelValues)
    85  		assert.Equal(t, e.labels, actualLabelValues, "label values don't match")
    86  		assert.Equal(t, 1, len(tsd.Points), "assertMetricDataValues can only handle a single Point in a TimeSeries")
    87  		assert.Equal(t, e.val, tsd.Points[0].Value, "metric: %s, tags: %v, values don't match; got: %v, want: %v", metricName, tsd.LabelValues, tsd.Points[0].Value, e.val)
    88  	}
    89  }
    90  
    91  func resetMetrics() {
    92  	unRegisterViews()
    93  	registerViews()
    94  }
    95  
    96  func TestControllerGameServerCount(t *testing.T) {
    97  	resetMetrics()
    98  	exporter := &metricExporter{}
    99  	reader := metricexport.NewReader()
   100  
   101  	c := newFakeController()
   102  	defer c.close()
   103  
   104  	c.run(t)
   105  	require.True(t, c.sync())
   106  
   107  	gs1 := gameServerWithFleetAndState("test-fleet", agonesv1.GameServerStateCreating)
   108  	c.gsWatch.Add(gs1)
   109  	require.Eventually(t, func() bool {
   110  		gs, err := c.gameServerLister.GameServers(gs1.ObjectMeta.Namespace).Get(gs1.ObjectMeta.Name)
   111  		assert.NoError(t, err)
   112  		return gs.Status.State == agonesv1.GameServerStateCreating
   113  	}, 5*time.Second, time.Second)
   114  	c.collect()
   115  
   116  	gs1 = gs1.DeepCopy()
   117  	gs1.Status.State = agonesv1.GameServerStateReady
   118  	c.gsWatch.Modify(gs1)
   119  	require.Eventually(t, func() bool {
   120  		gs, err := c.gameServerLister.GameServers(gs1.ObjectMeta.Namespace).Get(gs1.ObjectMeta.Name)
   121  		assert.NoError(t, err)
   122  		return gs.Status.State == agonesv1.GameServerStateReady
   123  	}, 5*time.Second, time.Second)
   124  	c.collect()
   125  
   126  	gs1 = gs1.DeepCopy()
   127  	gs1.Status.State = agonesv1.GameServerStateShutdown
   128  	c.gsWatch.Modify(gs1)
   129  	c.gsWatch.Add(gameServerWithFleetAndState("", agonesv1.GameServerStatePortAllocation))
   130  	c.gsWatch.Add(gameServerWithFleetAndState("", agonesv1.GameServerStatePortAllocation))
   131  
   132  	require.True(t, c.sync())
   133  	// Port allocation is last, so wait for that come to the state we expect
   134  	require.Eventually(t, func() bool {
   135  		c.collect()
   136  		ex := &metricExporter{}
   137  		reader.ReadAndExport(ex)
   138  
   139  		for _, m := range ex.metrics {
   140  			if m.Descriptor.Name == gameServersCountName {
   141  				if len(m.TimeSeries) == 4 {
   142  					return true
   143  				}
   144  				logrus.WithField("m", m).Info("Metrics")
   145  				return false
   146  			}
   147  		}
   148  
   149  		return false
   150  	}, 10*time.Second, time.Second)
   151  
   152  	reader.ReadAndExport(exporter)
   153  	assertMetricData(t, exporter, gameServersCountName, []expectedMetricData{
   154  		{labels: []string{"test-fleet", defaultNs, "Creating"}, val: int64(0)},
   155  		{labels: []string{"test-fleet", defaultNs, "Ready"}, val: int64(0)},
   156  		{labels: []string{"test-fleet", defaultNs, "Shutdown"}, val: int64(1)},
   157  		{labels: []string{"none", defaultNs, "PortAllocation"}, val: int64(2)},
   158  	})
   159  }
   160  
   161  func TestControllerGameServerPlayerConnectedCount(t *testing.T) {
   162  	runtime.FeatureTestMutex.Lock()
   163  	defer runtime.FeatureTestMutex.Unlock()
   164  	runtime.EnableAllFeatures()
   165  	resetMetrics()
   166  	exporter := &metricExporter{}
   167  	reader := metricexport.NewReader()
   168  
   169  	c := newFakeController()
   170  	defer c.close()
   171  
   172  	gs1 := gameServerWithFleetAndState("test-fleet", agonesv1.GameServerStateReady)
   173  	gs1.Status.Players = &agonesv1.PlayerStatus{
   174  		Count: 0,
   175  	}
   176  	c.gsWatch.Add(gs1)
   177  	gs1 = gs1.DeepCopy()
   178  	gs1.Status.Players.Count = 1
   179  	c.gsWatch.Modify(gs1)
   180  
   181  	c.run(t)
   182  	require.True(t, c.sync())
   183  	require.Eventually(t, func() bool {
   184  		gs, err := c.gameServerLister.GameServers(gs1.ObjectMeta.Namespace).Get(gs1.ObjectMeta.Name)
   185  		assert.NoError(t, err)
   186  		return gs.Status.Players.Count == 1
   187  	}, 5*time.Second, time.Second)
   188  	c.collect()
   189  
   190  	gs1 = gs1.DeepCopy()
   191  	gs1.Status.Players.Count = 4
   192  	c.gsWatch.Modify(gs1)
   193  
   194  	c.run(t)
   195  	require.True(t, c.sync())
   196  	require.Eventually(t, func() bool {
   197  		gs, err := c.gameServerLister.GameServers(gs1.ObjectMeta.Namespace).Get(gs1.ObjectMeta.Name)
   198  		assert.NoError(t, err)
   199  		return gs.Status.Players.Count == 4
   200  	}, 5*time.Second, time.Second)
   201  	c.collect()
   202  
   203  	reader.ReadAndExport(exporter)
   204  	assertMetricData(t, exporter, gameServersPlayerConnectedTotalName, []expectedMetricData{
   205  		{labels: []string{"test-fleet", gs1.GetName(), defaultNs}, val: int64(4)},
   206  	})
   207  }
   208  
   209  func TestControllerGameServerPlayerCapacityCount(t *testing.T) {
   210  	runtime.FeatureTestMutex.Lock()
   211  	defer runtime.FeatureTestMutex.Unlock()
   212  	runtime.EnableAllFeatures()
   213  	resetMetrics()
   214  	exporter := &metricExporter{}
   215  	reader := metricexport.NewReader()
   216  
   217  	c := newFakeController()
   218  	defer c.close()
   219  
   220  	gs1 := gameServerWithFleetAndState("test-fleet", agonesv1.GameServerStateReady)
   221  	gs1.Status.Players = &agonesv1.PlayerStatus{
   222  		Capacity: 4,
   223  		Count:    0,
   224  	}
   225  	c.gsWatch.Add(gs1)
   226  	gs1 = gs1.DeepCopy()
   227  	gs1.Status.Players.Count = 1
   228  	c.gsWatch.Modify(gs1)
   229  
   230  	c.run(t)
   231  	require.True(t, c.sync())
   232  	require.Eventually(t, func() bool {
   233  		gs, err := c.gameServerLister.GameServers(gs1.ObjectMeta.Namespace).Get(gs1.ObjectMeta.Name)
   234  		assert.NoError(t, err)
   235  		return gs.Status.Players.Count == 1
   236  	}, 5*time.Second, time.Second)
   237  	c.collect()
   238  
   239  	reader.ReadAndExport(exporter)
   240  	assertMetricData(t, exporter, gameServersPlayerCapacityTotalName, []expectedMetricData{
   241  		{labels: []string{"test-fleet", gs1.GetName(), defaultNs}, val: int64(3)},
   242  	})
   243  }
   244  
   245  func TestControllerGameServersTotal(t *testing.T) {
   246  	resetMetrics()
   247  	exporter := &metricExporter{}
   248  	reader := metricexport.NewReader()
   249  	c := newFakeController()
   250  	defer c.close()
   251  	c.run(t)
   252  
   253  	// deleted gs should not be counted
   254  	gs := gameServerWithFleetAndState("deleted", agonesv1.GameServerStateCreating)
   255  	c.gsWatch.Add(gs)
   256  	c.gsWatch.Delete(gs)
   257  
   258  	generateGsEvents(16, agonesv1.GameServerStateCreating, "test", c.gsWatch)
   259  	generateGsEvents(15, agonesv1.GameServerStateScheduled, "test", c.gsWatch)
   260  	generateGsEvents(10, agonesv1.GameServerStateStarting, "test", c.gsWatch)
   261  	generateGsEvents(1, agonesv1.GameServerStateUnhealthy, "test", c.gsWatch)
   262  	generateGsEvents(19, agonesv1.GameServerStateCreating, "", c.gsWatch)
   263  	generateGsEvents(18, agonesv1.GameServerStateScheduled, "", c.gsWatch)
   264  	generateGsEvents(16, agonesv1.GameServerStateStarting, "", c.gsWatch)
   265  	generateGsEvents(1, agonesv1.GameServerStateUnhealthy, "", c.gsWatch)
   266  
   267  	expected := 96
   268  	assert.Eventually(t, func() bool {
   269  		list, err := c.gameServerLister.GameServers(gs.ObjectMeta.Namespace).List(labels.Everything())
   270  		require.NoError(t, err)
   271  		return len(list) == expected
   272  	}, 5*time.Second, time.Second)
   273  	// While these values are tested above, the following test checks will provide a more detailed diff output
   274  	// in the case where the assert.Eventually(...) case fails, which makes failing tests easier to debug.
   275  	list, err := c.gameServerLister.GameServers(gs.ObjectMeta.Namespace).List(labels.Everything())
   276  	require.NoError(t, err)
   277  	require.Len(t, list, expected)
   278  
   279  	reader.ReadAndExport(exporter)
   280  	assertMetricData(t, exporter, gameServersTotalName, []expectedMetricData{
   281  		{labels: []string{"test", defaultNs, "Creating"}, val: int64(16)},
   282  		{labels: []string{"test", defaultNs, "Scheduled"}, val: int64(15)},
   283  		{labels: []string{"test", defaultNs, "Starting"}, val: int64(10)},
   284  		{labels: []string{"test", defaultNs, "Unhealthy"}, val: int64(1)},
   285  		{labels: []string{"none", defaultNs, "Creating"}, val: int64(19)},
   286  		{labels: []string{"none", defaultNs, "Scheduled"}, val: int64(18)},
   287  		{labels: []string{"none", defaultNs, "Starting"}, val: int64(16)},
   288  		{labels: []string{"none", defaultNs, "Unhealthy"}, val: int64(1)},
   289  	})
   290  }
   291  
   292  func TestControllerFleetOnDeleting(t *testing.T) {
   293  
   294  	resetMetrics()
   295  	exporter := &metricExporter{}
   296  	reader := metricexport.NewReader()
   297  	c := newFakeController()
   298  	defer c.close()
   299  	c.run(t)
   300  
   301  	deletionTime := metav1.NewTime(time.Now())
   302  
   303  	fd := fleet("fleet-deleting", 100, 100, 100, 100, 100)
   304  	ft := fleet("fleet-test", 8, 2, 5, 1, 1)
   305  	c.fleetWatch.Add(fd)
   306  
   307  	fd = fd.DeepCopy()
   308  	fd.DeletionTimestamp = &deletionTime
   309  	c.fleetWatch.Modify(fd)
   310  
   311  	c.fleetWatch.Add(ft)
   312  	ft = ft.DeepCopy()
   313  	ft.Status.Replicas = 15
   314  	c.fleetWatch.Modify(ft)
   315  
   316  	// wait until the fleet-deleting exists and total value equal 15.
   317  	require.Eventually(t, func() bool {
   318  		ex := &metricExporter{}
   319  		reader.ReadAndExport(ex)
   320  
   321  		for _, m := range ex.metrics {
   322  			if m.Descriptor.Name == fleetReplicaCountName {
   323  				for _, d := range m.TimeSeries {
   324  					name := d.LabelValues[0].Value
   325  					val := d.Points[0].Value
   326  					if len(name) > 0 && name == "fleet-test" && val == int64(15) {
   327  						return true
   328  					}
   329  				}
   330  			}
   331  		}
   332  
   333  		return false
   334  	}, 5*time.Second, time.Second)
   335  
   336  	reader.ReadAndExport(exporter)
   337  	assertMetricData(t, exporter, fleetReplicaCountName, []expectedMetricData{
   338  		{labels: []string{"fleet-deleting", defaultNs, "total"}, val: int64(100)},
   339  		{labels: []string{"fleet-deleting", defaultNs, "allocated"}, val: int64(100)},
   340  		{labels: []string{"fleet-deleting", defaultNs, "ready"}, val: int64(100)},
   341  		{labels: []string{"fleet-deleting", defaultNs, "desired"}, val: int64(100)},
   342  		{labels: []string{"fleet-deleting", defaultNs, "reserved"}, val: int64(100)},
   343  
   344  		{labels: []string{"fleet-test", defaultNs, "total"}, val: int64(15)},
   345  		{labels: []string{"fleet-test", defaultNs, "allocated"}, val: int64(2)},
   346  		{labels: []string{"fleet-test", defaultNs, "ready"}, val: int64(5)},
   347  		{labels: []string{"fleet-test", defaultNs, "desired"}, val: int64(1)},
   348  		{labels: []string{"fleet-test", defaultNs, "reserved"}, val: int64(1)},
   349  	})
   350  }
   351  
   352  func TestControllerFleetReplicasCount_ResetMetricsOnDelete(t *testing.T) {
   353  	runtime.FeatureTestMutex.Lock()
   354  	defer runtime.FeatureTestMutex.Unlock()
   355  
   356  	resetMetrics()
   357  	exporter := &metricExporter{}
   358  	reader := metricexport.NewReader()
   359  	c := newFakeController()
   360  	defer c.close()
   361  	c.run(t)
   362  
   363  	f := fleet("fleet-test", 8, 2, 5, 1, 1)
   364  	fd := fleet("fleet-deleted", 100, 100, 100, 100, 100)
   365  	c.fleetWatch.Add(f)
   366  	f = f.DeepCopy()
   367  	f.Status.ReadyReplicas = 1
   368  	f.Spec.Replicas = 5
   369  	c.fleetWatch.Modify(f)
   370  	c.fleetWatch.Add(fd)
   371  	c.fleetWatch.Delete(fd)
   372  
   373  	// wait until the fleet-deleted no longer exists
   374  	require.Eventually(t, func() bool {
   375  		ex := &metricExporter{}
   376  		reader.ReadAndExport(ex)
   377  
   378  		for _, m := range ex.metrics {
   379  			if m.Descriptor.Name == fleetReplicaCountName {
   380  				for _, d := range m.TimeSeries {
   381  					value := d.LabelValues[0].Value
   382  					if len(value) > 0 && value != "fleet-deleted" {
   383  						return true
   384  					}
   385  				}
   386  			}
   387  		}
   388  
   389  		return false
   390  	}, 5*time.Second, time.Second)
   391  
   392  	reader.ReadAndExport(exporter)
   393  	assertMetricData(t, exporter, fleetReplicaCountName, []expectedMetricData{
   394  		{labels: []string{"fleet-test", defaultNs, "reserved"}, val: int64(1)},
   395  		{labels: []string{"fleet-test", defaultNs, "allocated"}, val: int64(2)},
   396  		{labels: []string{"fleet-test", defaultNs, "desired"}, val: int64(5)},
   397  		{labels: []string{"fleet-test", defaultNs, "ready"}, val: int64(1)},
   398  		{labels: []string{"fleet-test", defaultNs, "total"}, val: int64(8)},
   399  	})
   400  }
   401  
   402  func TestControllerFleetAutoScalerOnDeleting(t *testing.T) {
   403  
   404  	resetMetrics()
   405  	exporter := &metricExporter{}
   406  	reader := metricexport.NewReader()
   407  	c := newFakeController()
   408  	defer c.close()
   409  	c.run(t)
   410  
   411  	deletionTime := metav1.NewTime(time.Now())
   412  
   413  	fas := fleetAutoScaler("fleet-deleting", "fas-deleting")
   414  	fast := fleetAutoScaler("fleet-test", "fas-test")
   415  	c.fasWatch.Add(fas)
   416  
   417  	fas = fas.DeepCopy()
   418  	fas.Status.CurrentReplicas = 15
   419  	c.fasWatch.Modify(fas)
   420  	fas = fas.DeepCopy()
   421  	fas.DeletionTimestamp = &deletionTime
   422  	c.fasWatch.Modify(fas)
   423  
   424  	c.fasWatch.Add(fast)
   425  
   426  	fast = fast.DeepCopy()
   427  	fast.Status.CurrentReplicas = 5
   428  	c.fasWatch.Modify(fast)
   429  
   430  	// wait until the fas-test exists and current-replicas's value euqal 5.
   431  	require.Eventually(t, func() bool {
   432  		ex := &metricExporter{}
   433  		reader.ReadAndExport(ex)
   434  
   435  		for _, m := range ex.metrics {
   436  			if m.Descriptor.Name == fleetAutoscalerCurrentReplicaCountName {
   437  				for _, d := range m.TimeSeries {
   438  					name := d.LabelValues[1].Value
   439  					val := d.Points[0].Value
   440  					if len(name) > 0 && name == "fas-test" && val == int64(5) {
   441  						return true
   442  					}
   443  				}
   444  			}
   445  		}
   446  
   447  		return false
   448  	}, 5*time.Second, time.Second)
   449  
   450  	reader.ReadAndExport(exporter)
   451  	assertMetricData(t, exporter, fleetAutoscalerCurrentReplicaCountName, []expectedMetricData{
   452  		{labels: []string{"fleet-deleting", "fas-deleting", defaultNs}, val: int64(15)},
   453  		{labels: []string{"fleet-test", "fas-test", defaultNs}, val: int64(5)},
   454  	})
   455  }
   456  
   457  func TestControllerFleetAutoScalerState_ResetMetricsOnDelete(t *testing.T) {
   458  	runtime.FeatureTestMutex.Lock()
   459  	defer runtime.FeatureTestMutex.Unlock()
   460  
   461  	resetMetrics()
   462  	exporter := &metricExporter{}
   463  	reader := metricexport.NewReader()
   464  	c := newFakeController()
   465  	defer c.close()
   466  	c.run(t)
   467  
   468  	// testing fleet name change
   469  	fasFleetNameChange := fleetAutoScaler("first-fleet", "name-switch")
   470  	c.fasWatch.Add(fasFleetNameChange)
   471  	fasFleetNameChange = fasFleetNameChange.DeepCopy()
   472  	fasFleetNameChange.Spec.Policy.Buffer.BufferSize = intstr.FromInt(10)
   473  	fasFleetNameChange.Spec.Policy.Buffer.MaxReplicas = 50
   474  	fasFleetNameChange.Spec.Policy.Buffer.MinReplicas = 10
   475  	fasFleetNameChange.Status.CurrentReplicas = 20
   476  	fasFleetNameChange.Status.DesiredReplicas = 10
   477  	fasFleetNameChange.Status.ScalingLimited = true
   478  	c.fasWatch.Modify(fasFleetNameChange)
   479  	fasFleetNameChange = fasFleetNameChange.DeepCopy()
   480  	fasFleetNameChange.Spec.FleetName = "second-fleet"
   481  	c.fasWatch.Modify(fasFleetNameChange)
   482  	// testing deletion
   483  	fasDeleted := fleetAutoScaler("deleted-fleet", "deleted")
   484  	fasDeleted.Spec.Policy.Buffer.BufferSize = intstr.FromString("50%")
   485  	fasDeleted.Spec.Policy.Buffer.MaxReplicas = 150
   486  	fasDeleted.Spec.Policy.Buffer.MinReplicas = 15
   487  	c.fasWatch.Add(fasDeleted)
   488  	c.fasWatch.Delete(fasDeleted)
   489  
   490  	c.sync()
   491  	// wait until the fleet-deleted no longer exists
   492  	require.Eventually(t, func() bool {
   493  		ex := &metricExporter{}
   494  		reader.ReadAndExport(ex)
   495  
   496  		for _, m := range ex.metrics {
   497  			if m.Descriptor.Name == fleetAutoscalersLimitedName {
   498  				for _, d := range m.TimeSeries {
   499  					values := d.LabelValues
   500  					if len(values[0].Value) > 0 && values[0].Value != "deleted-fleet" && values[1].Value != "deleted" && values[2].Value == defaultNs {
   501  						return true
   502  					}
   503  				}
   504  			}
   505  		}
   506  
   507  		return false
   508  	}, 5*time.Second, time.Second)
   509  
   510  	reader.ReadAndExport(exporter)
   511  	assertMetricData(t, exporter, fleetAutoscalersAbleToScaleName, []expectedMetricData{
   512  		{labels: []string{"second-fleet", "name-switch", defaultNs}, val: int64(1)},
   513  	})
   514  	assertMetricData(t, exporter, fleetAutoscalerBufferLimitName, []expectedMetricData{
   515  		{labels: []string{"second-fleet", "name-switch", defaultNs, "max"}, val: int64(50)},
   516  		{labels: []string{"second-fleet", "name-switch", defaultNs, "min"}, val: int64(10)},
   517  	})
   518  	assertMetricData(t, exporter, fleetAutoscalterBufferSizeName, []expectedMetricData{
   519  		{labels: []string{"second-fleet", "name-switch", defaultNs, "count"}, val: int64(10)},
   520  	})
   521  	assertMetricData(t, exporter, fleetAutoscalerCurrentReplicaCountName, []expectedMetricData{
   522  		{labels: []string{"second-fleet", "name-switch", defaultNs}, val: int64(20)},
   523  	})
   524  	assertMetricData(t, exporter, fleetAutoscalersDesiredReplicaCountName, []expectedMetricData{
   525  		{labels: []string{"second-fleet", "name-switch", defaultNs}, val: int64(10)},
   526  	})
   527  	assertMetricData(t, exporter, fleetAutoscalersLimitedName, []expectedMetricData{
   528  		{labels: []string{"second-fleet", "name-switch", defaultNs}, val: int64(1)},
   529  	})
   530  }
   531  
   532  func TestControllerGameServersNodeState(t *testing.T) {
   533  	resetMetrics()
   534  	m := agtesting.NewMocks()
   535  
   536  	m.KubeClient.AddReactor("list", "nodes", func(_ k8stesting.Action) (bool, k8sruntime.Object, error) {
   537  		n1 := nodeWithName("node1")
   538  		n2 := nodeWithName("node2")
   539  		n3 := nodeWithName("node3")
   540  		return true, &corev1.NodeList{Items: []corev1.Node{*n1, *n2, *n3}}, nil
   541  	})
   542  	m.AgonesClient.AddReactor("list", "gameservers", func(_ k8stesting.Action) (bool, k8sruntime.Object, error) {
   543  		gs1 := gameServerWithNode("node1")
   544  		gs2 := gameServerWithNode("node2")
   545  		gs3 := gameServerWithNode("node2")
   546  		return true, &agonesv1.GameServerList{Items: []agonesv1.GameServer{*gs1, *gs2, *gs3}}, nil
   547  	})
   548  
   549  	c := newFakeControllerWithMock(m)
   550  	defer c.close()
   551  	require.True(t, c.sync())
   552  	c.collect()
   553  	reader := metricexport.NewReader()
   554  
   555  	// wait until we have some nodes and gameservers in metrics
   556  	var exporter *metricExporter
   557  	assert.Eventually(t, func() bool {
   558  		exporter = &metricExporter{}
   559  		reader.ReadAndExport(exporter)
   560  
   561  		check := 0
   562  		for _, m := range exporter.metrics {
   563  			switch m.Descriptor.Name {
   564  			case nodeCountName:
   565  				check++
   566  			case gameServersNodeCountName:
   567  				check++
   568  			}
   569  		}
   570  
   571  		return check == 2
   572  	}, 10*time.Second, time.Second)
   573  
   574  	// check the details
   575  	assertMetricData(t, exporter, gameServersNodeCountName, []expectedMetricData{
   576  		{labels: []string{}, val: &metricdata.Distribution{
   577  			Count:                 3,
   578  			Sum:                   3,
   579  			SumOfSquaredDeviation: 2,
   580  			BucketOptions:         &metricdata.BucketOptions{Bounds: []float64{0.00001, 1.00001, 2.00001, 3.00001, 4.00001, 5.00001, 6.00001, 7.00001, 8.00001, 9.00001, 10.00001, 11.00001, 12.00001, 13.00001, 14.00001, 15.00001, 16.00001, 32.00001, 40.00001, 50.00001, 60.00001, 70.00001, 80.00001, 90.00001, 100.00001, 110.00001, 120.00001}},
   581  			Buckets:               []metricdata.Bucket{{Count: 1}, {Count: 1}, {Count: 1}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}, {Count: 0}}}},
   582  	})
   583  	assertMetricData(t, exporter, nodeCountName, []expectedMetricData{
   584  		{labels: []string{"true"}, val: int64(1)},
   585  		{labels: []string{"false"}, val: int64(2)},
   586  	})
   587  }
   588  
   589  func TestFleetCountersAndListsMetrics(t *testing.T) {
   590  	runtime.FeatureTestMutex.Lock()
   591  	defer runtime.FeatureTestMutex.Unlock()
   592  	assert.NoError(t, runtime.ParseFeatures(string(runtime.FeatureCountsAndLists)+"=true"))
   593  
   594  	resetMetrics()
   595  	exporter := &metricExporter{}
   596  	reader := metricexport.NewReader()
   597  	c := newFakeController()
   598  	defer c.close()
   599  
   600  	fleetName := "cl-fleet-test"
   601  	counterName := "players"
   602  	counters := map[string]agonesv1.AggregatedCounterStatus{
   603  		counterName: {
   604  			AllocatedCount:    24,
   605  			AllocatedCapacity: 30,
   606  			Count:             28,
   607  			Capacity:          50,
   608  		},
   609  	}
   610  	listName := "rooms"
   611  	lists := map[string]agonesv1.AggregatedListStatus{
   612  		listName: {
   613  			AllocatedCount:    4,
   614  			AllocatedCapacity: 6,
   615  			Count:             1,
   616  			Capacity:          100,
   617  		},
   618  	}
   619  
   620  	f := fleet(fleetName, 8, 3, 5, 8, 0)
   621  	c.fleetWatch.Add(f)
   622  	f = f.DeepCopy()
   623  	f.Status.Lists = lists
   624  	f.Status.Counters = counters
   625  	c.fleetWatch.Modify(f)
   626  
   627  	c.run(t)
   628  	require.True(t, c.sync())
   629  	require.Eventually(t, func() bool {
   630  		fl, err := c.fleetLister.Fleets(f.GetObjectMeta().GetNamespace()).Get(fleetName)
   631  		assert.NoError(t, err)
   632  		return cmp.Equal(counters, fl.Status.Counters) && cmp.Equal(lists, fl.Status.Lists)
   633  	}, 5*time.Second, time.Second)
   634  	c.collect()
   635  
   636  	reader.ReadAndExport(exporter)
   637  	assertMetricData(t, exporter, fleetCountersName, []expectedMetricData{
   638  		// keyCounter, keyName, keyNamespace, keyType
   639  		{labels: []string{counterName, fleetName, defaultNs, "allocated_count"}, val: int64(24)},
   640  		{labels: []string{counterName, fleetName, defaultNs, "allocated_capacity"}, val: int64(30)},
   641  		{labels: []string{counterName, fleetName, defaultNs, "total_count"}, val: int64(28)},
   642  		{labels: []string{counterName, fleetName, defaultNs, "total_capacity"}, val: int64(50)},
   643  	})
   644  	assertMetricData(t, exporter, fleetListsName, []expectedMetricData{
   645  		// keyList, keyName, keyNamespace, keyType
   646  		{labels: []string{listName, fleetName, defaultNs, "allocated_count"}, val: int64(4)},
   647  		{labels: []string{listName, fleetName, defaultNs, "allocated_capacity"}, val: int64(6)},
   648  		{labels: []string{listName, fleetName, defaultNs, "total_count"}, val: int64(1)},
   649  		{labels: []string{listName, fleetName, defaultNs, "total_capacity"}, val: int64(100)},
   650  	})
   651  }
   652  
   653  func TestCalcDuration(t *testing.T) {
   654  	m := agtesting.NewMocks()
   655  	c := NewController(
   656  		m.KubeClient,
   657  		m.AgonesClient,
   658  		m.KubeInformerFactory,
   659  		m.AgonesInformerFactory)
   660  	creationTimestamp := metav1.Now()
   661  	futureTimestamp := metav1.NewTime(time.Now().Add(24 * time.Hour))
   662  	gsName1 := "exampleGameServer1"
   663  	gsName2 := "exampleGameServer2"
   664  	currentTime := creationTimestamp.Local()
   665  	// Add one second each time Duration is calculated
   666  	c.now = func() time.Time {
   667  		currentTime = currentTime.Add(1 * time.Second)
   668  		return currentTime
   669  	}
   670  	type result struct {
   671  		duration float64
   672  		err      error
   673  	}
   674  	fleet1 := "test-fleet"
   675  	fleet2 := ""
   676  	var testCases = []struct {
   677  		description string
   678  		gs1         *agonesv1.GameServer
   679  		gs2         *agonesv1.GameServer
   680  		expected    result
   681  	}{
   682  		{
   683  			description: "GameServer creating - first measurement",
   684  			gs1:         gameServerWithFleetStateCreationTimestamp(fleet1, gsName1, "", creationTimestamp),
   685  			gs2:         gameServerWithFleetStateCreationTimestamp(fleet1, gsName1, agonesv1.GameServerStateCreating, creationTimestamp),
   686  			expected: result{
   687  				err:      nil,
   688  				duration: 1,
   689  			},
   690  		},
   691  		{
   692  			description: "Test state change of a GameServer",
   693  			gs1:         gameServerWithFleetStateCreationTimestamp(fleet1, gsName1, agonesv1.GameServerStateCreating, creationTimestamp),
   694  			gs2:         gameServerWithFleetStateCreationTimestamp(fleet1, gsName1, agonesv1.GameServerStateRequestReady, creationTimestamp),
   695  			expected: result{
   696  				err:      nil,
   697  				duration: 1,
   698  			},
   699  		},
   700  		{
   701  			description: "gs1 state should already be deleted, error should be generated (emulation of evicted key for gs1)",
   702  			gs1:         gameServerWithFleetStateCreationTimestamp(fleet1, gsName1, "", creationTimestamp),
   703  			gs2:         gameServerWithFleetStateCreationTimestamp(fleet1, gsName1, agonesv1.GameServerStateRequestReady, creationTimestamp),
   704  			expected: result{
   705  				err:      errors.Errorf("unable to calculate '' state duration of '%s' GameServer", gsName1),
   706  				duration: 0,
   707  			},
   708  		},
   709  		{
   710  			description: "Shutdown state should remove the key in LRU cache",
   711  			gs1:         gameServerWithFleetStateCreationTimestamp(fleet1, gsName1, agonesv1.GameServerStateRequestReady, creationTimestamp),
   712  			gs2:         gameServerWithFleetStateCreationTimestamp(fleet1, gsName1, agonesv1.GameServerStateShutdown, creationTimestamp),
   713  			expected: result{
   714  				err:      nil,
   715  				duration: 2,
   716  			},
   717  		},
   718  		{
   719  			description: "Cache miss, no key in LRU cache",
   720  			gs1:         gameServerWithFleetStateCreationTimestamp(fleet1, gsName1, agonesv1.GameServerStateRequestReady, creationTimestamp),
   721  			gs2:         gameServerWithFleetStateCreationTimestamp(fleet1, gsName1, agonesv1.GameServerStateShutdown, creationTimestamp),
   722  			expected: result{
   723  				err:      errors.Errorf("unable to calculate 'RequestReady' state duration of '%s' GameServer", gsName1),
   724  				duration: 0,
   725  			},
   726  		},
   727  		{
   728  			description: "Future timestamp was used",
   729  			gs1:         gameServerWithFleetStateCreationTimestamp(fleet2, gsName2, "", futureTimestamp),
   730  			gs2:         gameServerWithFleetStateCreationTimestamp(fleet2, gsName2, agonesv1.GameServerStateCreating, futureTimestamp),
   731  			expected: result{
   732  				err:      errors.Errorf("negative duration for '' state of '%s' GameServer", gsName2),
   733  				duration: 0,
   734  			},
   735  		},
   736  		{
   737  			description: "Shutdown state - remove a key from the LRU",
   738  			gs1:         gameServerWithFleetStateCreationTimestamp(fleet2, gsName2, agonesv1.GameServerStateCreating, futureTimestamp),
   739  			gs2:         gameServerWithFleetStateCreationTimestamp(fleet2, gsName2, agonesv1.GameServerStateShutdown, futureTimestamp),
   740  			expected: result{
   741  				err:      nil,
   742  				duration: 1,
   743  			},
   744  		},
   745  	}
   746  
   747  	for _, tc := range testCases {
   748  		t.Run(tc.description, func(t *testing.T) {
   749  			// Do not use t.Parallel(), because test cases should be executed as serial tests
   750  			// Test case 3 depends on key eviction in test case 2
   751  			duration, err := c.calcDuration(tc.gs1, tc.gs2)
   752  			if tc.expected.err != nil {
   753  				assert.EqualError(t, err, tc.expected.err.Error(), "We should receive an error, metric should not be measured")
   754  			} else {
   755  				assert.NoError(t, err, "Unable to caculate duration of a particular state")
   756  			}
   757  			assert.Equal(t, tc.expected.duration, duration, "Time diff should be calculated properly")
   758  		})
   759  	}
   760  	assert.Len(t, c.gameServerStateLastChange.Keys(), 0, "We should not have any keys after the test")
   761  }
   762  
   763  func TestIsSystemNode(t *testing.T) {
   764  	cases := []struct {
   765  		desc     string
   766  		node     *corev1.Node
   767  		expected bool
   768  	}{
   769  		{
   770  			desc: "Is system node, true expected",
   771  			node: &corev1.Node{
   772  				Spec: corev1.NodeSpec{
   773  					Taints: []corev1.Taint{{Key: "agones.dev/test"}},
   774  				},
   775  			},
   776  			expected: true,
   777  		},
   778  		{
   779  			desc: "Not a system node, false expected",
   780  			node: &corev1.Node{
   781  				Spec: corev1.NodeSpec{
   782  					Taints: []corev1.Taint{{Key: "qwerty.dev/test"}},
   783  				},
   784  			},
   785  			expected: false,
   786  		},
   787  		{
   788  			desc: "Empty taints, false expected",
   789  			node: &corev1.Node{
   790  				Spec: corev1.NodeSpec{
   791  					Taints: []corev1.Taint{},
   792  				},
   793  			},
   794  			expected: false,
   795  		},
   796  	}
   797  
   798  	for _, tc := range cases {
   799  		t.Run(tc.desc, func(t *testing.T) {
   800  			res := isSystemNode(tc.node)
   801  			assert.Equal(t, tc.expected, res)
   802  		})
   803  	}
   804  }