2
0

costmodel_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. package costmodel
  2. import (
  3. "slices"
  4. "testing"
  5. "github.com/opencost/opencost/core/pkg/util"
  6. "github.com/opencost/opencost/pkg/clustercache"
  7. "github.com/stretchr/testify/assert"
  8. v1 "k8s.io/api/core/v1"
  9. "k8s.io/apimachinery/pkg/api/resource"
  10. )
  11. func TestPruneDuplicates(t *testing.T) {
  12. tests := []struct {
  13. name string
  14. input []string
  15. expected []string
  16. }{
  17. {
  18. name: "empty slice",
  19. input: []string{},
  20. expected: []string{},
  21. },
  22. {
  23. name: "single element slice",
  24. input: []string{"test1"},
  25. expected: []string{"test1"},
  26. },
  27. {
  28. name: "basic duplicate",
  29. input: []string{"test1", "test1"},
  30. expected: []string{"test1"},
  31. },
  32. {
  33. name: "compound duplicate",
  34. input: []string{"test1", "test2", "test1", "test2"},
  35. expected: []string{"test1", "test2"},
  36. },
  37. {
  38. name: "mixture of duplicate/ no duplicate",
  39. input: []string{"test1", "test2", "test1", "test2", "test3"},
  40. expected: []string{"test1", "test2", "test3"},
  41. },
  42. {
  43. name: "underscore sanitization",
  44. input: []string{"test_1", "test_2", "test_1", "test_2", "test_3"},
  45. expected: []string{"test-1", "test-2", "test-3"},
  46. },
  47. {
  48. name: "underscore sanitization II",
  49. input: []string{"test-1", "test_2", "test_1", "test_2", "test_3"},
  50. expected: []string{"test-1", "test-2", "test-3"},
  51. },
  52. }
  53. for _, tt := range tests {
  54. t.Run(tt.name, func(t *testing.T) {
  55. actual := pruneDuplicates(tt.expected)
  56. slices.Sort(actual)
  57. expected := tt.expected
  58. slices.Sort(expected)
  59. if !slices.Equal(actual, expected) {
  60. t.Fatalf("test failuire for case %s. Expected %v, got %v", tt.name, expected, actual)
  61. }
  62. })
  63. }
  64. }
  65. func TestGetGPUCount(t *testing.T) {
  66. tests := []struct {
  67. name string
  68. node *clustercache.Node
  69. expectedGPU float64
  70. expectedVGPU float64
  71. expectedError bool
  72. }{
  73. {
  74. name: "Standard NVIDIA GPU",
  75. node: &clustercache.Node{
  76. Status: v1.NodeStatus{
  77. Capacity: v1.ResourceList{
  78. "nvidia.com/gpu": resource.MustParse("2"),
  79. },
  80. },
  81. },
  82. expectedGPU: 2.0,
  83. expectedVGPU: 2.0,
  84. },
  85. {
  86. name: "NVIDIA GPU with GFD - renameByDefault=true",
  87. node: &clustercache.Node{
  88. Labels: map[string]string{
  89. "nvidia.com/gpu.replicas": "4",
  90. "nvidia.com/gpu.count": "1",
  91. },
  92. Status: v1.NodeStatus{
  93. Capacity: v1.ResourceList{
  94. "nvidia.com/gpu.shared": resource.MustParse("4"),
  95. },
  96. },
  97. },
  98. expectedGPU: 1.0,
  99. expectedVGPU: 4.0,
  100. },
  101. {
  102. name: "NVIDIA GPU with GFD - renameByDefault=false",
  103. node: &clustercache.Node{
  104. Labels: map[string]string{
  105. "nvidia.com/gpu.replicas": "4",
  106. "nvidia.com/gpu.count": "1",
  107. },
  108. Status: v1.NodeStatus{
  109. Capacity: v1.ResourceList{
  110. "nvidia.com/gpu": resource.MustParse("4"),
  111. },
  112. },
  113. },
  114. expectedGPU: 1.0,
  115. expectedVGPU: 4.0,
  116. },
  117. {
  118. name: "No GPU",
  119. node: &clustercache.Node{
  120. Status: v1.NodeStatus{
  121. Capacity: v1.ResourceList{},
  122. },
  123. },
  124. expectedGPU: -1.0,
  125. expectedVGPU: -1.0,
  126. },
  127. }
  128. for _, tt := range tests {
  129. t.Run(tt.name, func(t *testing.T) {
  130. gpu, vgpu, err := getGPUCount(nil, tt.node)
  131. if tt.expectedError {
  132. assert.Error(t, err)
  133. } else {
  134. assert.NoError(t, err)
  135. assert.Equal(t, tt.expectedGPU, gpu)
  136. assert.Equal(t, tt.expectedVGPU, vgpu)
  137. }
  138. })
  139. }
  140. }
  141. func Test_CostData_GetController_CronJob(t *testing.T) {
  142. cases := []struct {
  143. name string
  144. cd CostData
  145. expectedName string
  146. expectedKind string
  147. expectedHasController bool
  148. }{
  149. {
  150. name: "batch/v1beta1 CronJob Job name",
  151. cd: CostData{
  152. // batch/v1beta1 CronJobs create Jobs with a 10 character
  153. // timestamp appended to the end of the name.
  154. //
  155. // It looks like this:
  156. // CronJob: cronjob-1
  157. // Job: cronjob-1-1651057200
  158. // Pod: cronjob-1-1651057200-mf5c9
  159. Jobs: []string{"cronjob-1-1651057200"},
  160. },
  161. expectedName: "cronjob-1",
  162. expectedKind: "job",
  163. expectedHasController: true,
  164. },
  165. {
  166. name: "batch/v1 CronJob Job name",
  167. cd: CostData{
  168. // batch/v1CronJobs create Jobs with an 8 character timestamp
  169. // appended to the end of the name.
  170. //
  171. // It looks like this:
  172. // CronJob: cj-v1
  173. // Job: cj-v1-27517770
  174. // Pod: cj-v1-27517770-xkrgn
  175. Jobs: []string{"cj-v1-27517770"},
  176. },
  177. expectedName: "cj-v1",
  178. expectedKind: "job",
  179. expectedHasController: true,
  180. },
  181. }
  182. for _, c := range cases {
  183. t.Run(c.name, func(t *testing.T) {
  184. name, kind, hasController := c.cd.GetController()
  185. if name != c.expectedName {
  186. t.Errorf("Name mismatch. Expected: %s. Got: %s", c.expectedName, name)
  187. }
  188. if kind != c.expectedKind {
  189. t.Errorf("Kind mismatch. Expected: %s. Got: %s", c.expectedKind, kind)
  190. }
  191. if hasController != c.expectedHasController {
  192. t.Errorf("HasController mismatch. Expected: %t. Got: %t", c.expectedHasController, hasController)
  193. }
  194. })
  195. }
  196. }
  197. func Test_getContainerAllocation(t *testing.T) {
  198. cases := []struct {
  199. name string
  200. cd CostData
  201. expectedCPUAllocation []*util.Vector
  202. expectedRAMAllocation []*util.Vector
  203. }{
  204. {
  205. name: "Requests greater than usage",
  206. cd: CostData{
  207. CPUReq: []*util.Vector{{Value: 1.0, Timestamp: 1686929350}},
  208. CPUUsed: []*util.Vector{{Value: .01, Timestamp: 1686929350}},
  209. RAMReq: []*util.Vector{{Value: 10000000, Timestamp: 1686929350}},
  210. RAMUsed: []*util.Vector{{Value: 5500000, Timestamp: 1686929350}},
  211. },
  212. expectedCPUAllocation: []*util.Vector{{Value: 1.0, Timestamp: 1686929350}},
  213. expectedRAMAllocation: []*util.Vector{{Value: 10000000, Timestamp: 1686929350}},
  214. },
  215. {
  216. name: "Requests less than usage",
  217. cd: CostData{
  218. CPUReq: []*util.Vector{{Value: 1.0, Timestamp: 1686929350}},
  219. CPUUsed: []*util.Vector{{Value: 2.2, Timestamp: 1686929350}},
  220. RAMReq: []*util.Vector{{Value: 10000000, Timestamp: 1686929350}},
  221. RAMUsed: []*util.Vector{{Value: 75000000, Timestamp: 1686929350}},
  222. },
  223. expectedCPUAllocation: []*util.Vector{{Value: 2.2, Timestamp: 1686929350}},
  224. expectedRAMAllocation: []*util.Vector{{Value: 75000000, Timestamp: 1686929350}},
  225. },
  226. {
  227. // Expected behavior for getContainerAllocation is to always use the
  228. // highest Timestamp value. The significance of 10 seconds comes
  229. // from the current default in ApplyVectorOp() in
  230. // pkg/util/vector.go.
  231. name: "Mismatched timestamps less than 10 seconds apart",
  232. cd: CostData{
  233. CPUReq: []*util.Vector{{Value: 1.0, Timestamp: 1686929354}},
  234. CPUUsed: []*util.Vector{{Value: .01, Timestamp: 1686929350}},
  235. RAMReq: []*util.Vector{{Value: 10000000, Timestamp: 1686929354}},
  236. RAMUsed: []*util.Vector{{Value: 5500000, Timestamp: 1686929350}},
  237. },
  238. expectedCPUAllocation: []*util.Vector{{Value: 1.0, Timestamp: 1686929354}},
  239. expectedRAMAllocation: []*util.Vector{{Value: 10000000, Timestamp: 1686929354}},
  240. },
  241. {
  242. // Expected behavior for getContainerAllocation is to always use the
  243. // hightest Timestamp value. The significance of 10 seconds comes
  244. // from the current default in ApplyVectorOp() in
  245. // pkg/util/vector.go.
  246. name: "Mismatched timestamps greater than 10 seconds apart",
  247. cd: CostData{
  248. CPUReq: []*util.Vector{{Value: 1.0, Timestamp: 1686929399}},
  249. CPUUsed: []*util.Vector{{Value: .01, Timestamp: 1686929350}},
  250. RAMReq: []*util.Vector{{Value: 10000000, Timestamp: 1686929399}},
  251. RAMUsed: []*util.Vector{{Value: 5500000, Timestamp: 1686929350}},
  252. },
  253. expectedCPUAllocation: []*util.Vector{{Value: 1.0, Timestamp: 1686929399}},
  254. expectedRAMAllocation: []*util.Vector{{Value: 10000000, Timestamp: 1686929399}},
  255. },
  256. {
  257. name: "Requests has no values",
  258. cd: CostData{
  259. CPUReq: []*util.Vector{{Value: 0, Timestamp: 0}},
  260. CPUUsed: []*util.Vector{{Value: .01, Timestamp: 1686929350}},
  261. RAMReq: []*util.Vector{{Value: 0, Timestamp: 0}},
  262. RAMUsed: []*util.Vector{{Value: 5500000, Timestamp: 1686929350}},
  263. },
  264. expectedCPUAllocation: []*util.Vector{{Value: .01, Timestamp: 1686929350}},
  265. expectedRAMAllocation: []*util.Vector{{Value: 5500000, Timestamp: 1686929350}},
  266. },
  267. {
  268. name: "Usage has no values",
  269. cd: CostData{
  270. CPUReq: []*util.Vector{{Value: 1.0, Timestamp: 1686929350}},
  271. CPUUsed: []*util.Vector{{Value: 0, Timestamp: 0}},
  272. RAMReq: []*util.Vector{{Value: 10000000, Timestamp: 1686929350}},
  273. RAMUsed: []*util.Vector{{Value: 0, Timestamp: 0}},
  274. },
  275. expectedCPUAllocation: []*util.Vector{{Value: 1.0, Timestamp: 1686929350}},
  276. expectedRAMAllocation: []*util.Vector{{Value: 10000000, Timestamp: 1686929350}},
  277. },
  278. {
  279. // WRN Log should be thrown
  280. name: "Both have no values",
  281. cd: CostData{
  282. CPUReq: []*util.Vector{{Value: 0, Timestamp: 0}},
  283. CPUUsed: []*util.Vector{{Value: 0, Timestamp: 0}},
  284. RAMReq: []*util.Vector{{Value: 0, Timestamp: 0}},
  285. RAMUsed: []*util.Vector{{Value: 0, Timestamp: 0}},
  286. },
  287. expectedCPUAllocation: []*util.Vector{{Value: 0, Timestamp: 0}},
  288. expectedRAMAllocation: []*util.Vector{{Value: 0, Timestamp: 0}},
  289. },
  290. {
  291. name: "Requests is Nil",
  292. cd: CostData{
  293. CPUReq: []*util.Vector{nil},
  294. CPUUsed: []*util.Vector{{Value: .01, Timestamp: 1686929350}},
  295. RAMReq: []*util.Vector{nil},
  296. RAMUsed: []*util.Vector{{Value: 5500000, Timestamp: 1686929350}},
  297. },
  298. expectedCPUAllocation: []*util.Vector{{Value: .01, Timestamp: 1686929350}},
  299. expectedRAMAllocation: []*util.Vector{{Value: 5500000, Timestamp: 1686929350}},
  300. },
  301. {
  302. name: "Usage is nil",
  303. cd: CostData{
  304. CPUReq: []*util.Vector{{Value: 1.0, Timestamp: 1686929350}},
  305. CPUUsed: []*util.Vector{nil},
  306. RAMReq: []*util.Vector{{Value: 10000000, Timestamp: 1686929350}},
  307. RAMUsed: []*util.Vector{nil},
  308. },
  309. expectedCPUAllocation: []*util.Vector{{Value: 1.0, Timestamp: 1686929350}},
  310. expectedRAMAllocation: []*util.Vector{{Value: 10000000, Timestamp: 1686929350}},
  311. },
  312. }
  313. for _, c := range cases {
  314. t.Run(c.name, func(t *testing.T) {
  315. cpuAllocation := getContainerAllocation(c.cd.CPUReq[0], c.cd.CPUUsed[0], "CPU")
  316. ramAllocation := getContainerAllocation(c.cd.RAMReq[0], c.cd.RAMUsed[0], "RAM")
  317. if cpuAllocation[0].Value != c.expectedCPUAllocation[0].Value {
  318. t.Errorf("CPU Allocation mismatch. Expected Value: %f. Got: %f", cpuAllocation[0].Value, c.expectedCPUAllocation[0].Value)
  319. }
  320. if cpuAllocation[0].Timestamp != c.expectedCPUAllocation[0].Timestamp {
  321. t.Errorf("CPU Allocation mismatch. Expected Timestamp: %f. Got: %f", cpuAllocation[0].Timestamp, c.expectedCPUAllocation[0].Timestamp)
  322. }
  323. if ramAllocation[0].Value != c.expectedRAMAllocation[0].Value {
  324. t.Errorf("RAM Allocation mismatch. Expected Value: %f. Got: %f", ramAllocation[0].Value, c.expectedRAMAllocation[0].Value)
  325. }
  326. if ramAllocation[0].Timestamp != c.expectedRAMAllocation[0].Timestamp {
  327. t.Errorf("RAM Allocation mismatch. Expected Timestamp: %f. Got: %f", ramAllocation[0].Timestamp, c.expectedRAMAllocation[0].Timestamp)
  328. }
  329. })
  330. }
  331. }