diff --git a/pkg/lib/cpu/quotaturbo/client_test.go b/pkg/lib/cpu/quotaturbo/client_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9389fa147c45045abdd2e6760d1e0c5c12808b9e --- /dev/null +++ b/pkg/lib/cpu/quotaturbo/client_test.go @@ -0,0 +1,111 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023. All rights reserved. +// rubik licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +// Author: Jiaqi Yang +// Date: 2023-03-09 +// Description: This file is used for testing quota turbo client + +// Package quotaturbo is for Quota Turbo feature +package quotaturbo + +import ( + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + + "isula.org/rubik/pkg/common/constant" + "isula.org/rubik/pkg/core/typedef/cgroup" + "isula.org/rubik/test/try" +) + +// TestClient_AdjustQuota tests AdjustQuota of Client +func TestClient_AdjustQuota(t *testing.T) { + const ( + contPath = "kubepods/testPod1/testCon1" + podPath = "kubepods/testPod1" + cpuPeriodFile = "cpu.cfs_period_us" + cpuQuotaFile = "cpu.cfs_quota_us" + cpuUsageFile = "cpuacct.usage" + cpuStatFile = "cpu.stat" + stat = `nr_periods 1 + nr_throttled 1 + throttled_time 1 + ` + quota = "200000" + period = "100000" + usage = "1234567" + ) + tests := []struct { + name string + wantErr bool + pre func(t *testing.T, c *Client) + post func(t *testing.T) + }{ + { + name: "TC1-empty CPUQuotas", + wantErr: false, + }, + { + name: "TC2-fail to updateCPUQuota causing absent of path", + pre: func(t *testing.T, c *Client) { + c.SetCgroupRoot(constant.TmpTestDir) + try.RemoveAll(filepath.Join(constant.TmpTestDir, "cpu", contPath)) + try.MkdirAll(filepath.Join(constant.TmpTestDir, "cpu", contPath), constant.DefaultDirMode) + + assert.Equal(t, 0, len(c.cpuQuotas)) + c.AddCgroup(contPath, float64(runtime.NumCPU())) + c.cpuQuotas[contPath] = &CPUQuota{ + Hierarchy: &cgroup.Hierarchy{ + MountPoint: c.CgroupRoot, + Path: contPath, + }, + cpuLimit: float64(runtime.NumCPU()) - 1, + curThrottle: &cgroup.CPUStat{}, + preThrottle: &cgroup.CPUStat{}, + } + assert.Equal(t, 1, len(c.cpuQuotas)) + }, + post: func(t *testing.T) { + try.RemoveAll(filepath.Join(constant.TmpTestDir, "cpu", contPath)) + }, + wantErr: true, + }, + { + name: "TC3-success", + pre: func(t *testing.T, c *Client) { + c.SetCgroupRoot(constant.TmpTestDir) + try.WriteFile(filepath.Join(constant.TmpTestDir, "cpu", contPath, cpuPeriodFile), period) + try.WriteFile(filepath.Join(constant.TmpTestDir, "cpu", contPath, cpuQuotaFile), quota) + try.WriteFile(filepath.Join(constant.TmpTestDir, "cpuacct", contPath, cpuUsageFile), usage) + try.WriteFile(filepath.Join(constant.TmpTestDir, "cpu", contPath, cpuStatFile), stat) + try.WriteFile(filepath.Join(constant.TmpTestDir, "cpu", podPath, cpuPeriodFile), period) + try.WriteFile(filepath.Join(constant.TmpTestDir, "cpu", podPath, cpuQuotaFile), quota) + assert.NoError(t, c.AddCgroup(contPath, float64(runtime.NumCPU())-1)) + assert.Equal(t, 1, len(c.cpuQuotas)) + }, + post: func(t *testing.T) { + try.RemoveAll(constant.TmpTestDir) + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := NewClient() + if tt.pre != nil { + tt.pre(t, c) + } + if err := c.AdjustQuota(); (err != nil) != tt.wantErr { + t.Errorf("Client.AdjustQuota() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/lib/cpu/quotaturbo/config_test.go b/pkg/lib/cpu/quotaturbo/config_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e41c4878a419de224a4092c155f1e81ed5f8455d --- /dev/null +++ b/pkg/lib/cpu/quotaturbo/config_test.go @@ -0,0 +1,282 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023. All rights reserved. +// rubik licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +// Author: Jiaqi Yang +// Date: 2023-03-07 +// Description: This file is used for testing quota turbo config + +// Package quotaturbo is for Quota Turbo feature +package quotaturbo + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + + "isula.org/rubik/pkg/common/constant" +) + +func TestConfig_SetAlarmWaterMark(t *testing.T) { + type fields struct { + HighWaterMark int + } + type args struct { + arg int + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "TC1-set alarmWaterMark successfully", + fields: fields{ + HighWaterMark: 60, + }, + args: args{ + arg: 100, + }, + wantErr: false, + }, + { + name: "TC2-alarmWaterMark = highwatermark", + fields: fields{ + HighWaterMark: 60, + }, + args: args{ + arg: 60, + }, + wantErr: true, + }, + { + name: "TC2.1-alarmWaterMark < highwatermark", + fields: fields{ + HighWaterMark: 60, + }, + args: args{ + arg: 59, + }, + wantErr: true, + }, + { + name: "TC3-alarmWaterMark > 100", + fields: fields{ + HighWaterMark: 60, + }, + args: args{ + arg: 101, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Config{ + HighWaterMark: tt.fields.HighWaterMark, + } + if err := c.SetAlarmWaterMark(tt.args.arg); (err != nil) != tt.wantErr { + t.Errorf("Config.SetAlarmWaterMark() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +// TestConfig_SetHighWaterMark tests SetHighWaterMark of Config +func TestConfig_SetHighWaterMark(t *testing.T) { + type fields struct { + AlarmWaterMark int + } + type args struct { + arg int + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "TC1-set highWaterMark successfully", + fields: fields{ + AlarmWaterMark: 80, + }, + args: args{ + arg: 10, + }, + wantErr: false, + }, + { + name: "TC2-alarmWaterMark = highwatermark", + fields: fields{ + AlarmWaterMark: 80, + }, + args: args{ + arg: 80, + }, + wantErr: true, + }, + { + name: "TC2.1-alarmWaterMark < highwatermark", + fields: fields{ + AlarmWaterMark: 80, + }, + args: args{ + arg: 81, + }, + wantErr: true, + }, + { + name: "TC3-highWaterMark < 0", + fields: fields{ + AlarmWaterMark: 60, + }, + args: args{ + arg: -1, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Config{ + AlarmWaterMark: tt.fields.AlarmWaterMark, + } + if err := c.SetHighWaterMark(tt.args.arg); (err != nil) != tt.wantErr { + t.Errorf("Config.SetHighWaterMark() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +// TestConfig_SetlEvateLimit tests SetlEvateLimit of Config +func TestConfig_SetlEvateLimit(t *testing.T) { + const ( + normal = 2.0 + larger = 100.01 + negative = -0.01 + ) + tests := []struct { + name string + arg float64 + wantErr bool + }{ + { + name: "TC1-set EvateLimit successfully", + arg: normal, + wantErr: false, + }, + { + name: "TC2-too large EvateLimit", + arg: larger, + wantErr: true, + }, + { + name: "TC3-negative EvateLimit", + arg: negative, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := NewConfig() + err := c.SetlEvateLimit(tt.arg) + if (err != nil) != tt.wantErr { + t.Errorf("Config.SetlEvateLimit() error = %v, wantErr %v", err, tt.wantErr) + } + if err == nil { + assert.Equal(t, tt.arg, c.ElevateLimit) + } else { + assert.Equal(t, defaultElevateLimit, c.ElevateLimit) + } + }) + } +} + +// TestConfig_SetCPUFloatingLimit tests SetCPUFloatingLimit of Config +func TestConfig_SetCPUFloatingLimit(t *testing.T) { + const ( + normal = 20.0 + larger = 100.01 + negative = -0.01 + ) + tests := []struct { + name string + arg float64 + wantErr bool + }{ + { + name: "TC1-set CPUFloatingLimit successfully", + arg: normal, + wantErr: false, + }, + { + name: "TC2-too large CPUFloatingLimit", + arg: larger, + wantErr: true, + }, + { + name: "TC3-negative CPUFloatingLimit", + arg: negative, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := NewConfig() + err := c.SetCPUFloatingLimit(tt.arg) + if (err != nil) != tt.wantErr { + t.Errorf("Config.SetCPUFloatingLimit() error = %v, wantErr %v", err, tt.wantErr) + } + if err == nil { + assert.Equal(t, tt.arg, c.CPUFloatingLimit) + } else { + assert.Equal(t, defaultCPUFloatingLimit, c.CPUFloatingLimit) + } + }) + } +} + +// TestOther tests other function of Config +func TestOther(t *testing.T) { + tests := []struct { + name string + want *Config + }{ + { + name: "TC1-test other", + want: &Config{ + HighWaterMark: defaultHighWaterMark, + AlarmWaterMark: defaultAlarmWaterMark, + CgroupRoot: constant.DefaultCgroupRoot, + ElevateLimit: defaultElevateLimit, + SlowFallbackRatio: defaultSlowFallbackRatio, + CPUFloatingLimit: defaultCPUFloatingLimit, + }, + }, + } + const slowFallback = 3.0 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewConfig() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewConfig() = %v, want %v", got, tt.want) + } + got.SetCgroupRoot(constant.TmpTestDir) + assert.Equal(t, got.CgroupRoot, constant.TmpTestDir) + got.SetSlowFallbackRatio(slowFallback) + assert.Equal(t, got.SlowFallbackRatio, slowFallback) + copyConf := got.GetConfig() + if !reflect.DeepEqual(got, copyConf) { + t.Errorf("GetConfig() = %v, want %v", got, copyConf) + } + }) + } +} diff --git a/pkg/lib/cpu/quotaturbo/cpu_test.go b/pkg/lib/cpu/quotaturbo/cpu_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ead2d058baf3669678329eac49d920abe83eb2bb --- /dev/null +++ b/pkg/lib/cpu/quotaturbo/cpu_test.go @@ -0,0 +1,57 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023. All rights reserved. +// rubik licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +// Author: Jiaqi Yang +// Date: 2023-02-20 +// Description: This file is used for testing cpu.go + +// Package quotaturbo is for Quota Turbo feature +package quotaturbo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestCalculateUtils tests calculateUtils +func TestCalculateUtils(t *testing.T) { + var ( + n1 float64 = 1 + n2 float64 = 2 + n3 float64 = 3 + n4 float64 = 4 + ) + + var ( + t1 = ProcStat{ + total: n2, + busy: n1, + } + t2 = ProcStat{ + total: n4, + busy: n2, + } + t3 = ProcStat{ + total: n3, + busy: n3, + } + ) + // normal return result + const ( + util float64 = 50 + minimumUtilization float64 = 0 + maximumUtilization float64 = 100 + ) + assert.Equal(t, util, calculateUtils(t1, t2)) + // busy errors + assert.Equal(t, minimumUtilization, calculateUtils(t2, t1)) + // total errors + assert.Equal(t, maximumUtilization, calculateUtils(t2, t3)) +} diff --git a/pkg/lib/cpu/quotaturbo/cpuquota_test.go b/pkg/lib/cpu/quotaturbo/cpuquota_test.go new file mode 100644 index 0000000000000000000000000000000000000000..feba814da457d248ce39394ba0a26bbe1acfe8bd --- /dev/null +++ b/pkg/lib/cpu/quotaturbo/cpuquota_test.go @@ -0,0 +1,305 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023. All rights reserved. +// rubik licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +// Author: Jiaqi Yang +// Date: 2023-02-20 +// Description: This file is used for testing cpu_quota + +// Package quotaturbo is for Quota Turbo feature +package quotaturbo + +import ( + "path" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "isula.org/rubik/pkg/common/constant" + "isula.org/rubik/pkg/common/util" + "isula.org/rubik/pkg/core/typedef/cgroup" + "isula.org/rubik/test/try" +) + +// TestNewCPUQuota tests NewCPUQuota +func TestNewCPUQuota(t *testing.T) { + const ( + cpuPeriodFile = "cpu.cfs_period_us" + cpuQuotaFile = "cpu.cfs_quota_us" + cpuUsageFile = "cpuacct.usage" + cpuStatFile = "cpu.stat" + validStat = `nr_periods 1 + nr_throttled 1 + throttled_time 1 + ` + throttleTime int64 = 1 + quota = "200000" + quotaValue int64 = 200000 + period = "100000" + periodValue int64 = 100000 + usage = "1234567" + usageValue int64 = 1234567 + ) + + var ( + cgPath = "kubepods/testPod1/testCon1" + h = &cgroup.Hierarchy{ + MountPoint: constant.TmpTestDir, + Path: cgPath, + } + + contPath = filepath.Join(constant.TmpTestDir, "cpu", cgPath, "") + contPeriodPath = filepath.Join(contPath, cpuPeriodFile) + contQuotaPath = filepath.Join(contPath, cpuQuotaFile) + contUsagePath = filepath.Join(constant.TmpTestDir, "cpuacct", cgPath, cpuUsageFile) + contStatPath = filepath.Join(contPath, cpuStatFile) + ) + + try.RemoveAll(constant.TmpTestDir) + try.MkdirAll(contPath, constant.DefaultDirMode) + try.MkdirAll(path.Dir(contUsagePath), constant.DefaultDirMode) + defer try.RemoveAll(constant.TmpTestDir) + + const cpuLimit = 2.0 + // absent of period file + try.RemoveAll(contPeriodPath) + _, err := NewCPUQuota(h, cpuLimit) + assert.Error(t, err, "should lacking of period file") + try.WriteFile(contPeriodPath, period) + + // absent of throttle file + try.RemoveAll(contStatPath) + _, err = NewCPUQuota(h, cpuLimit) + assert.Error(t, err, "should lacking of throttle file") + try.WriteFile(contStatPath, validStat) + + // absent of quota file + try.RemoveAll(contQuotaPath) + _, err = NewCPUQuota(h, cpuLimit) + assert.Error(t, err, "should lacking of quota file") + try.WriteFile(contQuotaPath, quota) + + // absent of usage file + try.RemoveAll(contUsagePath) + _, err = NewCPUQuota(h, cpuLimit) + assert.Error(t, err, "should lacking of usage file") + try.WriteFile(contUsagePath, usage) + + cq, err := NewCPUQuota(h, cpuLimit) + assert.NoError(t, err) + assert.Equal(t, usageValue, cq.cpuUsages[0].usage) + assert.Equal(t, quotaValue, cq.curQuota) + assert.Equal(t, periodValue, cq.period) + + cu := make([]cpuUsage, numberOfRestrictedCycles) + for i := 0; i < numberOfRestrictedCycles; i++ { + cu[i] = cpuUsage{} + } + cq.cpuUsages = cu + assert.NoError(t, cq.updateUsage()) +} + +// TestCPUQuota_WriteQuota tests WriteQuota of CPUQuota +func TestCPUQuota_WriteQuota(t *testing.T) { + const ( + largerQuota = "200000" + largerQuotaVal int64 = 200000 + smallerQuota = "100000" + smallerQuotaVal int64 = 100000 + unlimitedQuota = "-1" + unlimitedQuotaVal int64 = -1 + periodUs = "100000" + cpuPeriodFile = "cpu.cfs_period_us" + cpuQuotaFile = "cpu.cfs_quota_us" + ) + + var ( + cgPath = "kubepods/testPod1/testCon1" + contPath = filepath.Join(constant.TmpTestDir, "cpu", cgPath, "") + podPeriodPath = filepath.Join(filepath.Dir(contPath), cpuPeriodFile) + podQuotaPath = filepath.Join(filepath.Dir(contPath), cpuQuotaFile) + contPeriodPath = filepath.Join(contPath, cpuPeriodFile) + contQuotaPath = filepath.Join(contPath, cpuQuotaFile) + assertValue = func(t *testing.T, paths []string, value string) { + for _, p := range paths { + data, err := util.ReadFile(p) + assert.NoError(t, err) + assert.Equal(t, value, strings.TrimSpace(string(data))) + } + } + ) + + try.RemoveAll(constant.TmpTestDir) + defer try.RemoveAll(constant.TmpTestDir) + + type fields struct { + Hierarchy *cgroup.Hierarchy + curQuota int64 + nextQuota int64 + } + tests := []struct { + name string + pre func() + fields fields + post func(t *testing.T, cq *CPUQuota) + wantErr bool + }{ + { + name: "TC1-empty cgroup path", + fields: fields{ + Hierarchy: &cgroup.Hierarchy{}, + nextQuota: largerQuotaVal, + curQuota: smallerQuotaVal, + }, + wantErr: true, + }, + { + name: "TC2-fail to get paths", + fields: fields{ + Hierarchy: &cgroup.Hierarchy{ + Path: "/", + }, + nextQuota: largerQuotaVal, + curQuota: smallerQuotaVal, + }, + wantErr: true, + }, + { + name: "TC3-None of the paths exist", + fields: fields{ + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.TmpTestDir, + Path: "kubepods/testPod1/testCon1", + }, + nextQuota: largerQuotaVal, + curQuota: smallerQuotaVal, + }, + wantErr: true, + }, + { + name: "TC4-Only pod path existed", + fields: fields{ + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.TmpTestDir, + Path: "kubepods/testPod1/testCon1", + }, + // write the pod first and then write the container + nextQuota: largerQuotaVal, + curQuota: smallerQuotaVal, + }, + pre: func() { + try.WriteFile(podQuotaPath, smallerQuota) + try.WriteFile(podPeriodPath, periodUs) + try.RemoveAll(contQuotaPath) + try.RemoveAll(contPeriodPath) + }, + post: func(t *testing.T, cq *CPUQuota) { + // Unable to write to container, so restore pod as it is + assertValue(t, []string{podQuotaPath}, smallerQuota) + assert.Equal(t, smallerQuotaVal, cq.curQuota) + }, + wantErr: true, + }, + { + name: "TC5-success delta > 0", + fields: fields{ + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.TmpTestDir, + Path: "kubepods/testPod1/testCon1", + }, + nextQuota: largerQuotaVal, + curQuota: smallerQuotaVal, + }, + pre: func() { + try.WriteFile(podQuotaPath, smallerQuota) + try.WriteFile(podPeriodPath, periodUs) + try.WriteFile(contQuotaPath, smallerQuota) + try.WriteFile(contPeriodPath, periodUs) + }, + post: func(t *testing.T, cq *CPUQuota) { + assertValue(t, []string{podQuotaPath, contQuotaPath}, largerQuota) + assert.Equal(t, largerQuotaVal, cq.curQuota) + }, + wantErr: false, + }, + { + name: "TC6-success delta < 0", + fields: fields{ + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.TmpTestDir, + Path: "kubepods/testPod1/testCon1", + }, + nextQuota: smallerQuotaVal, + curQuota: largerQuotaVal, + }, + pre: func() { + try.WriteFile(podQuotaPath, largerQuota) + try.WriteFile(podPeriodPath, periodUs) + try.WriteFile(contQuotaPath, largerQuota) + try.WriteFile(contPeriodPath, periodUs) + }, + post: func(t *testing.T, cq *CPUQuota) { + assertValue(t, []string{podQuotaPath, contQuotaPath}, smallerQuota) + assert.Equal(t, smallerQuotaVal, cq.curQuota) + }, + wantErr: false, + }, + { + name: "TC6.1-success delta < 0 unlimited pod", + fields: fields{ + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.TmpTestDir, + Path: "kubepods/testPod1/testCon1", + }, + nextQuota: smallerQuotaVal, + curQuota: largerQuotaVal, + }, + pre: func() { + try.WriteFile(podQuotaPath, unlimitedQuota) + try.WriteFile(podPeriodPath, periodUs) + try.WriteFile(contQuotaPath, largerQuota) + try.WriteFile(contPeriodPath, periodUs) + }, + post: func(t *testing.T, cq *CPUQuota) { + assertValue(t, []string{contQuotaPath}, smallerQuota) + assertValue(t, []string{podQuotaPath}, unlimitedQuota) + assert.Equal(t, smallerQuotaVal, cq.curQuota) + }, + wantErr: false, + }, + { + name: "TC7-success delta = 0", + fields: fields{ + Hierarchy: &cgroup.Hierarchy{}, + nextQuota: smallerQuotaVal, + curQuota: smallerQuotaVal, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &CPUQuota{ + Hierarchy: tt.fields.Hierarchy, + curQuota: tt.fields.curQuota, + nextQuota: tt.fields.nextQuota, + } + if tt.pre != nil { + tt.pre() + } + if err := c.writeQuota(); (err != nil) != tt.wantErr { + t.Errorf("CPUQuota.WriteQuota() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.post != nil { + tt.post(t, c) + } + }) + } +} diff --git a/pkg/lib/cpu/quotaturbo/driverevent_test.go b/pkg/lib/cpu/quotaturbo/driverevent_test.go new file mode 100644 index 0000000000000000000000000000000000000000..926b4570066aa255bb968425ea22c704fc73725d --- /dev/null +++ b/pkg/lib/cpu/quotaturbo/driverevent_test.go @@ -0,0 +1,604 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023. All rights reserved. +// rubik licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +// Author: Jiaqi Yang +// Date: 2023-02-20 +// Description: This file is used for testing driverevent.go + +// Package quotaturbo is for Quota Turbo feature +package quotaturbo + +import ( + "math" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + + "isula.org/rubik/pkg/common/constant" + "isula.org/rubik/pkg/common/util" + "isula.org/rubik/pkg/core/typedef/cgroup" +) + +// TestEventDriverElevate tests elevate of EventDriver +func TestEventDriverElevate(t *testing.T) { + var elevateTests = []struct { + status *StatusStore + judgements func(t *testing.T, status *StatusStore) + name string + }{ + { + name: "TC1 - CPU usage >= the alarmWaterMark.", + status: &StatusStore{ + Config: &Config{ + AlarmWaterMark: 60, + }, + cpuQuotas: map[string]*CPUQuota{ + "testCon1": {}, + }, + cpuUtils: []cpuUtil{ + { + util: 90, + }, + }, + }, + judgements: func(t *testing.T, status *StatusStore) { + var delta float64 = 0 + conID := "testCon1" + assert.Equal(t, delta, status.cpuQuotas[conID].quotaDelta) + }, + }, + { + name: "TC2 - the container is not suppressed.", + status: &StatusStore{ + Config: &Config{ + AlarmWaterMark: 70, + ElevateLimit: defaultElevateLimit, + }, + cpuQuotas: map[string]*CPUQuota{ + "testCon2": { + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.DefaultCgroupRoot, + Path: "kubepods/testPod2/testCon2", + }, + // currently not suppressed + curThrottle: &cgroup.CPUStat{ + NrThrottled: 1, + ThrottledTime: 10, + }, + preThrottle: &cgroup.CPUStat{ + NrThrottled: 1, + ThrottledTime: 10, + }, + period: 100000, + }, + }, + cpuUtils: []cpuUtil{ + { + util: 60, + }, + }, + }, + judgements: func(t *testing.T, status *StatusStore) { + var delta float64 = 0 + conID := "testCon2" + assert.Equal(t, delta, status.cpuQuotas[conID].quotaDelta) + }, + }, + { + name: "TC3 - increase the quota of the suppressed container", + status: &StatusStore{ + Config: &Config{ + AlarmWaterMark: 60, + ElevateLimit: defaultElevateLimit, + }, + cpuQuotas: map[string]*CPUQuota{ + "testCon3": { + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.DefaultCgroupRoot, + Path: "kubepods/testPod3/testCon3", + }, + curThrottle: &cgroup.CPUStat{ + NrThrottled: 50, + ThrottledTime: 200000, + }, + preThrottle: &cgroup.CPUStat{ + NrThrottled: 40, + ThrottledTime: 100000, + }, + period: 100000, + }, + }, + cpuUtils: []cpuUtil{ + { + util: 40, + }, + }, + }, + judgements: func(t *testing.T, status *StatusStore) { + conID := "testCon3" + c := status.cpuQuotas[conID] + coefficient := math.Min(float64(0.0001), + util.PercentageToDecimal(status.ElevateLimit)*float64(runtime.NumCPU())) / + float64(0.0001) + delta := coefficient * float64(0.0001) * float64(c.period) + assert.True(t, status.cpuQuotas[conID].quotaDelta == delta) + }, + }, + } + + e := &EventDriver{} + for _, tt := range elevateTests { + t.Run(tt.name, func(t *testing.T) { + e.elevate(tt.status) + tt.judgements(t, tt.status) + }) + } +} + +// TestSlowFallback tests slowFallback of EventDriver +func TestSlowFallback(t *testing.T) { + var slowFallBackTests = []struct { + status *StatusStore + judgements func(t *testing.T, status *StatusStore) + name string + }{ + { + name: "TC1-CPU usage <= the highWaterMark.", + status: &StatusStore{ + Config: &Config{ + HighWaterMark: 60, + SlowFallbackRatio: defaultSlowFallbackRatio, + }, + cpuQuotas: map[string]*CPUQuota{ + "testCon4": { + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.DefaultCgroupRoot, + Path: "kubepods/testPod4/testCon4", + }, + }, + }, + cpuUtils: []cpuUtil{ + { + util: 40, + }, + }, + }, + judgements: func(t *testing.T, status *StatusStore) { + conID := "testCon4" + var delta float64 = 0 + assert.Equal(t, delta, status.cpuQuotas[conID].quotaDelta) + }, + }, + { + name: "TC2-the container is suppressed.", + status: &StatusStore{ + Config: &Config{ + AlarmWaterMark: 80, + HighWaterMark: 50, + SlowFallbackRatio: defaultSlowFallbackRatio, + }, + cpuQuotas: map[string]*CPUQuota{ + "testCon5": { + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.DefaultCgroupRoot, + Path: "kubepods/testPod5/testCon5", + }, + cpuLimit: 1, + curThrottle: &cgroup.CPUStat{ + NrThrottled: 10, + }, + preThrottle: &cgroup.CPUStat{ + NrThrottled: 0, + }, + period: 100000, + curQuota: 200000, + }, + }, + cpuUtils: []cpuUtil{ + { + util: 70, + }, + }, + }, + judgements: func(t *testing.T, status *StatusStore) { + var delta float64 = 0 + conID := "testCon5" + assert.Equal(t, delta, status.cpuQuotas[conID].quotaDelta) + }, + }, + { + name: "TC3-decrease the quota of the uncompressed containers", + status: &StatusStore{ + Config: &Config{ + AlarmWaterMark: 90, + HighWaterMark: 40, + SlowFallbackRatio: defaultSlowFallbackRatio, + }, + cpuQuotas: map[string]*CPUQuota{ + "testCon6": { + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.DefaultCgroupRoot, + Path: "kubepods/testPod6/testCon6", + }, + cpuLimit: 2, + // currently not suppressed + curThrottle: &cgroup.CPUStat{ + NrThrottled: 10, + ThrottledTime: 100000, + }, + preThrottle: &cgroup.CPUStat{ + NrThrottled: 10, + ThrottledTime: 100000, + }, + period: 100000, + curQuota: 400000, + }, + }, + cpuUtils: []cpuUtil{ + { + util: 60.0, + }, + }, + }, + judgements: func(t *testing.T, status *StatusStore) { + conID := "testCon6" + c := status.cpuQuotas[conID] + coefficient := (status.getLastCPUUtil() - float64(status.HighWaterMark)) / + float64(status.AlarmWaterMark-status.HighWaterMark) * status.SlowFallbackRatio + delta := coefficient * + ((float64(c.cpuLimit) * float64(c.period)) - float64(c.curQuota)) + assert.Equal(t, delta, status.cpuQuotas[conID].quotaDelta) + }, + }, + } + e := &EventDriver{} + for _, tt := range slowFallBackTests { + t.Run(tt.name, func(t *testing.T) { + e.slowFallback(tt.status) + tt.judgements(t, tt.status) + }) + } +} + +// TestFastFallback tests fastFallback of EventDriver +func TestFastFallback(t *testing.T) { + var fastFallBackTests = []struct { + status *StatusStore + judgements func(t *testing.T, status *StatusStore) + name string + }{ + { + name: "TC1-CPU usage <= the AlarmWaterMark.", + status: &StatusStore{ + Config: &Config{ + AlarmWaterMark: 30, + }, + cpuQuotas: map[string]*CPUQuota{ + "testCon7": { + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.DefaultCgroupRoot, + Path: "kubepods/testPod7/testCon7", + }, + }, + }, + cpuUtils: []cpuUtil{ + { + util: 10, + }, + }, + }, + judgements: func(t *testing.T, status *StatusStore) { + conID := "testCon7" + var delta float64 = 0 + assert.Equal(t, delta, status.cpuQuotas[conID].quotaDelta) + }, + }, + { + name: "TC2-the quota of container is not increased.", + status: &StatusStore{ + Config: &Config{ + AlarmWaterMark: 30, + }, + cpuQuotas: map[string]*CPUQuota{ + "testCon8": { + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.DefaultCgroupRoot, + Path: "kubepods/testPod8/testCon8", + }, + cpuLimit: 1, + period: 100, + curQuota: 100, + }, + }, + cpuUtils: []cpuUtil{ + { + util: 48, + }, + }, + }, + judgements: func(t *testing.T, status *StatusStore) { + var delta float64 = 0 + conID := "testCon8" + assert.Equal(t, delta, status.cpuQuotas[conID].quotaDelta) + }, + }, + { + name: "TC3-decrease the quota of the containers", + status: &StatusStore{ + Config: &Config{ + AlarmWaterMark: 65, + }, + cpuQuotas: map[string]*CPUQuota{ + "testCon9": { + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.DefaultCgroupRoot, + Path: "kubepods/testPod9/testCon9", + }, + cpuLimit: 3, + period: 10000, + curQuota: 40000, + }, + }, + cpuUtils: []cpuUtil{ + { + util: 90, + }, + }, + }, + judgements: func(t *testing.T, status *StatusStore) { + conID := "testCon9" + c := status.cpuQuotas[conID] + delta := util.PercentageToDecimal(float64(status.AlarmWaterMark)-status.getLastCPUUtil()) * + float64(runtime.NumCPU()) * float64(c.period) + assert.Equal(t, delta, status.cpuQuotas[conID].quotaDelta) + }, + }, + } + e := &EventDriver{} + for _, tt := range fastFallBackTests { + t.Run(tt.name, func(t *testing.T) { + e.fastFallback(tt.status) + tt.judgements(t, tt.status) + }) + } +} + +// TestSharpFluctuates tests sharpFluctuates +func TestSharpFluctuates(t *testing.T) { + const cpuUtil90 = 90 + var sharpFluctuatesTests = []struct { + status *StatusStore + want bool + name string + }{ + { + name: "TC1-the cpu changes rapidly", + status: &StatusStore{ + Config: &Config{ + CPUFloatingLimit: defaultCPUFloatingLimit, + }, + cpuUtils: []cpuUtil{ + { + util: cpuUtil90, + }, + { + util: cpuUtil90 - defaultCPUFloatingLimit - 1, + }, + }, + }, + want: true, + }, + { + name: "TC2-the cpu changes steadily", + status: &StatusStore{ + Config: &Config{ + CPUFloatingLimit: defaultCPUFloatingLimit, + }, + cpuUtils: []cpuUtil{ + { + util: cpuUtil90, + }, + { + util: cpuUtil90 - defaultCPUFloatingLimit + 1, + }, + }, + }, + want: false, + }, + } + for _, tt := range sharpFluctuatesTests { + t.Run(tt.name, func(t *testing.T) { + assert.True(t, sharpFluctuates(tt.status) == tt.want) + }) + } +} + +// TestEventDriverAdjustQuota tests adjustQuota of EventDriver +func TestEventDriverAdjustQuota(t *testing.T) { + var eDriverAdjustQuotaTests = []struct { + status *StatusStore + judgements func(t *testing.T, status *StatusStore) + name string + }{ + { + name: "TC1-no promotion", + status: &StatusStore{ + Config: &Config{ + AlarmWaterMark: 80, + HighWaterMark: 73, + }, + cpuQuotas: map[string]*CPUQuota{ + "testCon10": { + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.DefaultCgroupRoot, + Path: "kubepods/testPod10/testCon10", + }, + cpuLimit: 1, + period: 80, + curQuota: 100, + }, + }, + cpuUtils: []cpuUtil{ + { + util: 1, + }, + { + util: -defaultCPUFloatingLimit, + }, + }, + }, + judgements: func(t *testing.T, status *StatusStore) { + var delta float64 = 0 + conID := "testCon10" + assert.Equal(t, delta, status.cpuQuotas[conID].quotaDelta) + }, + }, + { + name: "TC2-make a promotion", + status: &StatusStore{ + Config: &Config{ + AlarmWaterMark: 97, + HighWaterMark: 73, + }, + cpuQuotas: map[string]*CPUQuota{ + "testCon11": { + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.DefaultCgroupRoot, + Path: "kubepods/testPod11/testCon11", + }, + cpuLimit: 2, + curThrottle: &cgroup.CPUStat{ + NrThrottled: 1, + ThrottledTime: 200, + }, + preThrottle: &cgroup.CPUStat{ + NrThrottled: 0, + ThrottledTime: 100, + }, + period: 2000, + curQuota: 5000, + }, + }, + cpuUtils: []cpuUtil{ + { + util: 10, + }, + }, + }, + judgements: func(t *testing.T, status *StatusStore) { + conID := "testCon11" + c := status.cpuQuotas[conID] + coefficient := math.Min(float64(0.00005), util.PercentageToDecimal(status.ElevateLimit)* + float64(runtime.NumCPU())) / float64(0.00005) + delta := coefficient * float64(0.00005) * float64(c.period) + assert.Equal(t, delta, status.cpuQuotas[conID].quotaDelta) + }, + }, + } + e := &EventDriver{} + for _, tt := range eDriverAdjustQuotaTests { + t.Run(tt.name, func(t *testing.T) { + e.adjustQuota(tt.status) + tt.judgements(t, tt.status) + }) + } +} + +// TestGetMaxQuota tests getMaxQuota +func TestGetMaxQuota(t *testing.T) { + var getMaxQuotaTests = []struct { + cq *CPUQuota + judgements func(t *testing.T, cq *CPUQuota) + name string + }{ + { + name: "TC1-empty cpu usage", + cq: &CPUQuota{ + heightLimit: 100, + cpuUsages: []cpuUsage{}, + }, + judgements: func(t *testing.T, cq *CPUQuota) { + var res float64 = 100 + assert.Equal(t, res, getMaxQuota(cq)) + }, + }, + { + name: "TC2-The remaining value is less than 3 times the upper limit.", + cq: &CPUQuota{ + cpuUsages: []cpuUsage{ + {100000, 100000}, + {200000, 200000}, + }, + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.DefaultCgroupRoot, + Path: "kubepods/testPod1/testCon1", + }, + cpuLimit: 4, + period: 100, + heightLimit: 800, + }, + judgements: func(t *testing.T, cq *CPUQuota) { + const res = 400 + float64(400*700)/float64(3*800) + assert.Equal(t, res, getMaxQuota(cq)) + }, + }, + { + name: "TC3-The remaining value is greater than 3 times the limit height.", + cq: &CPUQuota{ + cpuUsages: []cpuUsage{ + {10000, 0}, + {20000, 0}, + {30000, 0}, + {40000, 0}, + {50000, 0}, + {60000, 0}, + {70000, 0}, + {80000, 100}, + }, + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.DefaultCgroupRoot, + Path: "kubepods/testPod1/testCon1", + }, + cpuLimit: 1, + period: 100, + heightLimit: 200, + }, + judgements: func(t *testing.T, cq *CPUQuota) { + var res float64 = 200 + assert.Equal(t, res, getMaxQuota(cq)) + }, + }, + { + name: "TC4-The remaining value is less than the initial value.", + cq: &CPUQuota{ + cpuUsages: []cpuUsage{ + {100, 0}, + {200, 1000000}, + }, + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.DefaultCgroupRoot, + Path: "kubepods/testPod1/testCon1", + }, + cpuLimit: 10, + period: 10, + heightLimit: 150, + }, + judgements: func(t *testing.T, cq *CPUQuota) { + var res float64 = 100 + assert.Equal(t, res, getMaxQuota(cq)) + }, + }, + } + for _, tt := range getMaxQuotaTests { + t.Run(tt.name, func(t *testing.T) { + tt.judgements(t, tt.cq) + }) + } +} diff --git a/pkg/lib/cpu/quotaturbo/statusstore_test.go b/pkg/lib/cpu/quotaturbo/statusstore_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a69fac4e91302f5a7ea27f28e0868c82accd1a73 --- /dev/null +++ b/pkg/lib/cpu/quotaturbo/statusstore_test.go @@ -0,0 +1,568 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023. All rights reserved. +// rubik licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +// Author: Jiaqi Yang +// Date: 2023-02-16 +// Description: This file is used for testing statusstore.go + +// Package quotaturbo is for Quota Turbo feature +package quotaturbo + +import ( + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "isula.org/rubik/pkg/common/constant" + "isula.org/rubik/pkg/core/typedef/cgroup" + "isula.org/rubik/test/try" +) + +// TestIsAdjustmentAllowed tests isAdjustmentAllowed +func TestIsAdjustmentAllowed(t *testing.T) { + const contPath1 = "kubepods/testPod1/testCon1" + + try.RemoveAll(constant.TmpTestDir) + defer try.RemoveAll(constant.TmpTestDir) + + tests := []struct { + h *cgroup.Hierarchy + cpuLimit float64 + pre func() + post func() + name string + want bool + }{ + { + name: "TC1-allow adjustment", + h: &cgroup.Hierarchy{ + MountPoint: constant.TmpTestDir, + Path: contPath1, + }, + cpuLimit: float64(runtime.NumCPU()) - 1, + pre: func() { + try.MkdirAll(filepath.Join(constant.TmpTestDir, "cpu", contPath1, "cpu.cfs_quota_us"), + constant.DefaultFileMode) + }, + post: func() { + try.RemoveAll(filepath.Join(constant.TmpTestDir, "cpu", contPath1)) + }, + want: true, + }, + { + name: "TC2-cgroup path is not existed", + h: &cgroup.Hierarchy{ + MountPoint: constant.TmpTestDir, + Path: contPath1, + }, + cpuLimit: float64(runtime.NumCPU()) - 1, + pre: func() { + try.RemoveAll(filepath.Join(constant.TmpTestDir, "cpu", contPath1)) + }, + want: false, + }, + { + name: "TC3-cpulimit = 0", + h: &cgroup.Hierarchy{ + MountPoint: constant.TmpTestDir, + Path: contPath1, + }, + cpuLimit: 0, + pre: func() { + try.MkdirAll(filepath.Join(constant.TmpTestDir, "cpu", contPath1, "cpu.cfs_quota_us"), + constant.DefaultFileMode) + }, + post: func() { + try.RemoveAll(filepath.Join(constant.TmpTestDir, "cpu", contPath1)) + }, + want: false, + }, + { + name: "TC4-cpulimit over max", + h: &cgroup.Hierarchy{ + MountPoint: constant.TmpTestDir, + Path: contPath1, + }, + cpuLimit: float64(runtime.NumCPU()) + 1, + pre: func() { + try.MkdirAll(filepath.Join(constant.TmpTestDir, "cpu", contPath1, "cpu.cfs_quota_us"), + constant.DefaultFileMode) + }, + post: func() { + try.RemoveAll(filepath.Join(constant.TmpTestDir, "cpu", contPath1)) + }, + want: false, + }, + { + name: "TC5-cpurequest over max", + h: &cgroup.Hierarchy{ + MountPoint: constant.TmpTestDir, + Path: contPath1, + }, + cpuLimit: 0, + pre: func() { + try.MkdirAll(filepath.Join(constant.TmpTestDir, "cpu", contPath1, "cpu.cfs_quota_us"), + constant.DefaultFileMode) + }, + post: func() { + try.RemoveAll(filepath.Join(constant.TmpTestDir, "cpu", contPath1)) + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.pre != nil { + tt.pre() + } + assert.Equal(t, isAdjustmentAllowed(tt.h, tt.cpuLimit), tt.want) + if tt.post != nil { + tt.post() + } + }) + } +} + +// TestStatusStore_RemoveCgroup tests RemoveCgroup of StatusStore +func TestStatusStore_RemoveCgroup(t *testing.T) { + const ( + podPath = "kubepods/testPod1" + contPath = "kubepods/testPod1/testCon1" + ) + type fields struct { + Config *Config + cpuQuotas map[string]*CPUQuota + } + type args struct { + cgroupPath string + } + tests := []struct { + name string + fields fields + args args + wantErr bool + pre func() + post func(t *testing.T, d *StatusStore) + }{ + { + name: "TC1-empty cgroupPath", + args: args{ + cgroupPath: "", + }, + fields: fields{ + cpuQuotas: make(map[string]*CPUQuota), + }, + wantErr: false, + }, + { + name: "TC2-cgroupPath is not existed", + args: args{ + cgroupPath: contPath, + }, + fields: fields{ + cpuQuotas: map[string]*CPUQuota{ + contPath: { + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.TmpTestDir, + Path: contPath, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "TC3-cgroupPath existed but can not set", + args: args{ + cgroupPath: contPath, + }, + fields: fields{ + Config: &Config{ + CgroupRoot: constant.TmpTestDir, + }, + cpuQuotas: map[string]*CPUQuota{ + contPath: { + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.TmpTestDir, + Path: contPath, + }, + curQuota: 100000, + nextQuota: 200000, + }, + }, + }, + pre: func() { + try.MkdirAll(filepath.Join(constant.TmpTestDir, "cpu", contPath), constant.DefaultDirMode) + }, + post: func(t *testing.T, d *StatusStore) { + try.RemoveAll(filepath.Join(constant.TmpTestDir, "cpu", contPath)) + }, + wantErr: true, + }, + { + name: "TC4-remove cgroupPath successfully", + args: args{ + cgroupPath: contPath, + }, + fields: fields{ + cpuQuotas: map[string]*CPUQuota{ + contPath: { + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.TmpTestDir, + Path: contPath, + }, + cpuLimit: 2, + period: 100000, + curQuota: 250000, + nextQuota: 240000, + }, + }, + }, + pre: func() { + try.WriteFile(filepath.Join(constant.TmpTestDir, "cpu", contPath, "cpu.cfs_quota_us"), "250000") + try.WriteFile(filepath.Join(constant.TmpTestDir, "cpu", podPath, "cpu.cfs_quota_us"), "-1") + }, + post: func(t *testing.T, d *StatusStore) { + val := strings.TrimSpace(try.ReadFile( + filepath.Join(constant.TmpTestDir, "cpu", contPath, "cpu.cfs_quota_us")).String()) + assert.Equal(t, "200000", val) + val = strings.TrimSpace(try.ReadFile( + filepath.Join(constant.TmpTestDir, "cpu", podPath, "cpu.cfs_quota_us")).String()) + assert.Equal(t, "-1", val) + assert.Equal(t, 0, len(d.cpuQuotas)) + try.RemoveAll(constant.TmpTestDir) + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &StatusStore{ + Config: tt.fields.Config, + cpuQuotas: tt.fields.cpuQuotas, + } + if tt.pre != nil { + tt.pre() + } + if err := d.RemoveCgroup(tt.args.cgroupPath); (err != nil) != tt.wantErr { + t.Errorf("StatusStore.RemoveCgroup() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.post != nil { + tt.post(t, d) + } + }) + } +} + +// TestStatusStore_AddCgroup tests AddCgroup of StatusStore +func TestStatusStore_AddCgroup(t *testing.T) { + const ( + contPath = "kubepods/testPod1/testCon1" + cpuPeriodFile = "cpu.cfs_period_us" + cpuQuotaFile = "cpu.cfs_quota_us" + cpuUsageFile = "cpuacct.usage" + cpuStatFile = "cpu.stat" + stat = `nr_periods 1 + nr_throttled 1 + throttled_time 1 + ` + quota = "200000" + period = "100000" + usage = "1234567" + ) + type fields struct { + Config *Config + cpuQuotas map[string]*CPUQuota + } + type args struct { + cgroupPath string + cpuLimit float64 + cpuRequest float64 + } + tests := []struct { + name string + fields fields + args args + wantErr bool + pre func(t *testing.T, d *StatusStore) + post func(t *testing.T, d *StatusStore) + }{ + { + name: "TC1-empty cgroup path", + args: args{ + cgroupPath: "", + }, + fields: fields{ + cpuQuotas: make(map[string]*CPUQuota), + }, + wantErr: true, + }, + { + name: "TC2-empty cgroup mount point", + args: args{ + cgroupPath: contPath, + }, + fields: fields{ + Config: &Config{ + CgroupRoot: "", + }, + }, + wantErr: true, + }, + { + name: "TC3-cgroup not allow to adjust", + args: args{ + cgroupPath: contPath, + cpuLimit: 3, + }, + fields: fields{ + Config: &Config{ + CgroupRoot: constant.TmpTestDir, + }, + }, + wantErr: true, + }, + { + name: "TC4-failed to create CPUQuota", + args: args{ + cgroupPath: contPath, + cpuLimit: 3, + }, + fields: fields{ + Config: &Config{ + CgroupRoot: constant.TmpTestDir, + }, + cpuQuotas: make(map[string]*CPUQuota), + }, + pre: func(t *testing.T, d *StatusStore) { + try.WriteFile(filepath.Join(constant.TmpTestDir, "cpu", contPath, cpuPeriodFile), period) + }, + post: func(t *testing.T, d *StatusStore) { + try.RemoveAll(constant.TmpTestDir) + }, + wantErr: true, + }, + { + name: "TC5-add successfully", + args: args{ + cgroupPath: contPath, + cpuLimit: 2, + }, + fields: fields{ + Config: &Config{ + CgroupRoot: constant.TmpTestDir, + }, + cpuQuotas: make(map[string]*CPUQuota), + }, + pre: func(t *testing.T, d *StatusStore) { + try.WriteFile(filepath.Join(constant.TmpTestDir, "cpu", contPath, cpuPeriodFile), period) + try.WriteFile(filepath.Join(constant.TmpTestDir, "cpu", contPath, cpuQuotaFile), quota) + try.WriteFile(filepath.Join(constant.TmpTestDir, "cpuacct", contPath, cpuUsageFile), usage) + try.WriteFile(filepath.Join(constant.TmpTestDir, "cpu", contPath, cpuStatFile), stat) + }, + post: func(t *testing.T, d *StatusStore) { + assert.Equal(t, 1, len(d.cpuQuotas)) + try.RemoveAll(constant.TmpTestDir) + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &StatusStore{ + Config: tt.fields.Config, + cpuQuotas: tt.fields.cpuQuotas, + } + if tt.pre != nil { + tt.pre(t, d) + } + if err := d.AddCgroup(tt.args.cgroupPath, tt.args.cpuLimit); (err != nil) != tt.wantErr { + t.Errorf("StatusStore.AddCgroup() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.post != nil { + tt.post(t, d) + } + }) + } +} + +// TestStatusStoreGetLastCPUUtil tests getLastCPUUtil of StatusStore +func TestStatusStore_getLastCPUUtil(t *testing.T) { + // 1. empty CPU Utils + d := &StatusStore{} + t.Run("TC1-empty CPU Util", func(t *testing.T) { + util := float64(0.0) + assert.Equal(t, util, d.getLastCPUUtil()) + }) + // 2. CPU Utils + cpuUtil20 := 20 + d = &StatusStore{cpuUtils: []cpuUtil{{ + util: float64(cpuUtil20), + }}} + t.Run("TC2-CPU Util is 20", func(t *testing.T) { + util := float64(20.0) + assert.Equal(t, util, d.getLastCPUUtil()) + }) +} + +// TestQuotaTurboUpdateCPUUtils tests updateCPUUtils of QuotaTurbo and NewProcStat +func TestStatusStore_updateCPUUtils(t *testing.T) { + status := NewStatusStore() + // 1. obtain the cpu usage for the first time + if err := status.updateCPUUtils(); err != nil { + assert.NoError(t, err) + } + num1 := 1 + assert.Equal(t, num1, len(status.cpuUtils)) + // 2. obtain the cpu usage for the second time + if err := status.updateCPUUtils(); err != nil { + assert.NoError(t, err) + } + num2 := 2 + assert.Equal(t, num2, len(status.cpuUtils)) + // 3. obtain the cpu usage after 1 minute + var minuteTimeDelta int64 = 60000000001 + status.cpuUtils[0].timestamp -= minuteTimeDelta + if err := status.updateCPUUtils(); err != nil { + assert.NoError(t, err) + } + assert.Equal(t, num2, len(status.cpuUtils)) +} + +// TestStatusStore_updateCPUQuotas tests updateCPUQuotas of StatusStore +func TestStatusStore_updateCPUQuotas(t *testing.T) { + const ( + contPath = "kubepods/testPod1/testCon1" + cpuPeriodFile = "cpu.cfs_period_us" + cpuQuotaFile = "cpu.cfs_quota_us" + cpuUsageFile = "cpuacct.usage" + cpuStatFile = "cpu.stat" + stat = `nr_periods 1 + nr_throttled 1 + throttled_time 1 + ` + quota = "200000" + period = "100000" + usage = "1234567" + ) + type fields struct { + Config *Config + cpuQuotas map[string]*CPUQuota + cpuUtils []cpuUtil + } + tests := []struct { + name string + fields fields + wantErr bool + pre func() + post func() + }{ + { + name: "TC1-fail to get CPUQuota", + fields: fields{ + Config: &Config{ + CgroupRoot: constant.TmpTestDir, + }, + cpuQuotas: map[string]*CPUQuota{ + contPath: { + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.TmpTestDir, + Path: contPath, + }, + }, + }, + cpuUtils: make([]cpuUtil, 0), + }, + wantErr: true, + }, + { + name: "TC2-update successfully", + fields: fields{ + Config: &Config{ + CgroupRoot: constant.TmpTestDir, + }, + cpuQuotas: map[string]*CPUQuota{ + contPath: { + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.TmpTestDir, + Path: contPath, + }, + }, + }, + cpuUtils: make([]cpuUtil, 0), + }, + pre: func() { + try.WriteFile(filepath.Join(constant.TmpTestDir, "cpu", contPath, cpuPeriodFile), period) + try.WriteFile(filepath.Join(constant.TmpTestDir, "cpu", contPath, cpuQuotaFile), quota) + try.WriteFile(filepath.Join(constant.TmpTestDir, "cpuacct", contPath, cpuUsageFile), usage) + try.WriteFile(filepath.Join(constant.TmpTestDir, "cpu", contPath, cpuStatFile), stat) + }, + post: func() { + try.RemoveAll(constant.TmpTestDir) + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &StatusStore{ + Config: tt.fields.Config, + cpuQuotas: tt.fields.cpuQuotas, + cpuUtils: tt.fields.cpuUtils, + } + if tt.pre != nil { + tt.pre() + } + if err := d.updateCPUQuotas(); (err != nil) != tt.wantErr { + t.Errorf("StatusStore.update() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.post != nil { + tt.post() + } + }) + } +} + +// TestStatusStore_writeQuota tests writeQuota of StatusStore +func TestStatusStore_writeQuota(t *testing.T) { + const contPath = "kubepods/testPod1/testCon1" + tests := []struct { + name string + cpuQuotas map[string]*CPUQuota + wantErr bool + }{ + { + name: "TC1-empty cgroup path", + cpuQuotas: map[string]*CPUQuota{ + contPath: { + Hierarchy: &cgroup.Hierarchy{ + MountPoint: constant.TmpTestDir, + Path: contPath, + }, + curQuota: 100000, + nextQuota: 200000, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &StatusStore{ + cpuQuotas: tt.cpuQuotas, + } + if err := d.writeQuota(); (err != nil) != tt.wantErr { + t.Errorf("StatusStore.writeQuota() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}