costmodel_test.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. package costmodel
  2. import (
  3. "math"
  4. "math/rand"
  5. "testing"
  6. "time"
  7. "github.com/google/go-cmp/cmp"
  8. "github.com/opencost/opencost/core/pkg/clustercache"
  9. "github.com/opencost/opencost/core/pkg/util"
  10. "github.com/stretchr/testify/assert"
  11. v1 "k8s.io/api/core/v1"
  12. "k8s.io/apimachinery/pkg/api/resource"
  13. )
  14. func TestIsValidNodeName(t *testing.T) {
  15. tests := []string{
  16. "ip-10-1-2-3.ec2.internal",
  17. "node-1",
  18. "another.test.node",
  19. "10-55.23-10",
  20. "s21",
  21. "s1",
  22. "s",
  23. }
  24. for _, test := range tests {
  25. if !isValidNodeName(test) {
  26. t.Errorf("Expected %s to be a valid node name", test)
  27. }
  28. }
  29. chars := "abcdefghijklmnopqrstuvwxyz"
  30. longName := ""
  31. r := rand.New(rand.NewSource(time.Now().UnixNano()))
  32. for i := 0; i < 255; i++ {
  33. longName += string(chars[r.Intn(len(chars))])
  34. }
  35. fails := []string{
  36. longName,
  37. "192.168.1.1:80",
  38. "10.0.0.1:443",
  39. "127.0.0.1:8080",
  40. "172.16.254.1:22",
  41. "0.0.0.0:5000",
  42. "::1:80",
  43. "2001:db8::1:443",
  44. "2001:0db8:85a3:0000:0000:8a2e:0370:7334:8080",
  45. "fe80::1:22",
  46. "10.1.2.3:10240",
  47. ":::80",
  48. "node$-15",
  49. "not:valid",
  50. ".hello-world",
  51. "hello-world.",
  52. "i--",
  53. }
  54. for _, fail := range fails {
  55. if isValidNodeName(fail) {
  56. t.Errorf("Expected %s to be an invalid node name", fail)
  57. }
  58. }
  59. }
  60. func TestGetGPUCount(t *testing.T) {
  61. tests := []struct {
  62. name string
  63. node *clustercache.Node
  64. expectedGPU float64
  65. expectedVGPU float64
  66. expectedError bool
  67. }{
  68. {
  69. name: "Standard NVIDIA GPU",
  70. node: &clustercache.Node{
  71. Status: v1.NodeStatus{
  72. Capacity: v1.ResourceList{
  73. "nvidia.com/gpu": resource.MustParse("2"),
  74. },
  75. },
  76. },
  77. expectedGPU: 2.0,
  78. expectedVGPU: 2.0,
  79. },
  80. {
  81. name: "NVIDIA GPU with GFD - renameByDefault=true",
  82. node: &clustercache.Node{
  83. Labels: map[string]string{
  84. "nvidia.com/gpu.replicas": "4",
  85. "nvidia.com/gpu.count": "1",
  86. },
  87. Status: v1.NodeStatus{
  88. Capacity: v1.ResourceList{
  89. "nvidia.com/gpu.shared": resource.MustParse("4"),
  90. },
  91. },
  92. },
  93. expectedGPU: 1.0,
  94. expectedVGPU: 4.0,
  95. },
  96. {
  97. name: "NVIDIA GPU with GFD - renameByDefault=false",
  98. node: &clustercache.Node{
  99. Labels: map[string]string{
  100. "nvidia.com/gpu.replicas": "4",
  101. "nvidia.com/gpu.count": "1",
  102. },
  103. Status: v1.NodeStatus{
  104. Capacity: v1.ResourceList{
  105. "nvidia.com/gpu": resource.MustParse("4"),
  106. },
  107. },
  108. },
  109. expectedGPU: 1.0,
  110. expectedVGPU: 4.0,
  111. },
  112. {
  113. name: "No GPU",
  114. node: &clustercache.Node{
  115. Status: v1.NodeStatus{
  116. Capacity: v1.ResourceList{},
  117. },
  118. },
  119. expectedGPU: -1.0,
  120. expectedVGPU: -1.0,
  121. },
  122. }
  123. for _, tt := range tests {
  124. t.Run(tt.name, func(t *testing.T) {
  125. gpu, vgpu, err := getGPUCount(nil, tt.node)
  126. if tt.expectedError {
  127. assert.Error(t, err)
  128. } else {
  129. assert.NoError(t, err)
  130. assert.Equal(t, tt.expectedGPU, gpu)
  131. assert.Equal(t, tt.expectedVGPU, vgpu)
  132. }
  133. })
  134. }
  135. }
  136. func Test_CostData_GetController_CronJob(t *testing.T) {
  137. cases := []struct {
  138. name string
  139. cd CostData
  140. expectedName string
  141. expectedKind string
  142. expectedHasController bool
  143. }{
  144. {
  145. name: "batch/v1beta1 CronJob Job name",
  146. cd: CostData{
  147. // batch/v1beta1 CronJobs create Jobs with a 10 character
  148. // timestamp appended to the end of the name.
  149. //
  150. // It looks like this:
  151. // CronJob: cronjob-1
  152. // Job: cronjob-1-1651057200
  153. // Pod: cronjob-1-1651057200-mf5c9
  154. Jobs: []string{"cronjob-1-1651057200"},
  155. },
  156. expectedName: "cronjob-1",
  157. expectedKind: "job",
  158. expectedHasController: true,
  159. },
  160. {
  161. name: "batch/v1 CronJob Job name",
  162. cd: CostData{
  163. // batch/v1CronJobs create Jobs with an 8 character timestamp
  164. // appended to the end of the name.
  165. //
  166. // It looks like this:
  167. // CronJob: cj-v1
  168. // Job: cj-v1-27517770
  169. // Pod: cj-v1-27517770-xkrgn
  170. Jobs: []string{"cj-v1-27517770"},
  171. },
  172. expectedName: "cj-v1",
  173. expectedKind: "job",
  174. expectedHasController: true,
  175. },
  176. }
  177. for _, c := range cases {
  178. t.Run(c.name, func(t *testing.T) {
  179. name, kind, hasController := c.cd.GetController()
  180. if name != c.expectedName {
  181. t.Errorf("Name mismatch. Expected: %s. Got: %s", c.expectedName, name)
  182. }
  183. if kind != c.expectedKind {
  184. t.Errorf("Kind mismatch. Expected: %s. Got: %s", c.expectedKind, kind)
  185. }
  186. if hasController != c.expectedHasController {
  187. t.Errorf("HasController mismatch. Expected: %t. Got: %t", c.expectedHasController, hasController)
  188. }
  189. })
  190. }
  191. }
  192. func TestGetContainerAllocation(t *testing.T) {
  193. cases := []struct {
  194. name string
  195. req *util.Vector
  196. used *util.Vector
  197. allocationType string
  198. expected []*util.Vector
  199. }{
  200. {
  201. name: "request > usage",
  202. req: &util.Vector{
  203. Value: 100,
  204. Timestamp: 1672531200,
  205. },
  206. used: &util.Vector{
  207. Value: 50,
  208. Timestamp: 1672531200,
  209. },
  210. allocationType: "RAM",
  211. expected: []*util.Vector{
  212. {
  213. Value: 100,
  214. Timestamp: 1672531200,
  215. },
  216. },
  217. },
  218. {
  219. name: "usage > request",
  220. req: &util.Vector{
  221. Value: 50,
  222. Timestamp: 1672531200,
  223. },
  224. used: &util.Vector{
  225. Value: 100,
  226. Timestamp: 1672531200,
  227. },
  228. allocationType: "RAM",
  229. expected: []*util.Vector{
  230. {
  231. Value: 100,
  232. Timestamp: 1672531200,
  233. },
  234. },
  235. },
  236. {
  237. name: "only request is non-nil",
  238. req: &util.Vector{
  239. Value: 100,
  240. Timestamp: 1672531200,
  241. },
  242. used: nil,
  243. allocationType: "CPU",
  244. expected: []*util.Vector{
  245. {
  246. Value: 100,
  247. Timestamp: 1672531200,
  248. },
  249. },
  250. },
  251. {
  252. name: "only used is non-nil",
  253. req: nil,
  254. used: &util.Vector{
  255. Value: 100,
  256. Timestamp: 1672531200,
  257. },
  258. allocationType: "CPU",
  259. expected: []*util.Vector{
  260. {
  261. Value: 100,
  262. Timestamp: 1672531200,
  263. },
  264. },
  265. },
  266. {
  267. name: "both req and used are nil",
  268. req: nil,
  269. used: nil,
  270. allocationType: "GPU",
  271. expected: []*util.Vector{
  272. {
  273. Value: 0,
  274. Timestamp: float64(time.Now().UTC().Unix()),
  275. },
  276. },
  277. },
  278. {
  279. name: "NaN in request value",
  280. req: &util.Vector{
  281. Value: math.NaN(),
  282. Timestamp: 1672531200,
  283. },
  284. used: &util.Vector{
  285. Value: 50,
  286. Timestamp: 1672531200,
  287. },
  288. allocationType: "RAM",
  289. expected: []*util.Vector{
  290. {
  291. Value: 50,
  292. Timestamp: 1672531200,
  293. },
  294. },
  295. },
  296. {
  297. name: "NaN in used value",
  298. req: &util.Vector{
  299. Value: 100,
  300. Timestamp: 1672531200,
  301. },
  302. used: &util.Vector{
  303. Value: math.NaN(),
  304. Timestamp: 1672531200,
  305. },
  306. allocationType: "CPU",
  307. expected: []*util.Vector{
  308. {
  309. Value: 100,
  310. Timestamp: 1672531200,
  311. },
  312. },
  313. },
  314. }
  315. for _, tc := range cases {
  316. t.Run(tc.name, func(t *testing.T) {
  317. // For the nil case, the timestamp is dynamic, so we need to handle it separately
  318. if tc.name == "both req and used are nil" {
  319. result := getContainerAllocation(tc.req, tc.used, tc.allocationType)
  320. if result[0].Value != 0 {
  321. t.Errorf("Expected value to be 0, but got %f", result[0].Value)
  322. }
  323. if time.Now().UTC().Unix()-int64(result[0].Timestamp) > 5 {
  324. t.Errorf("Expected timestamp to be recent, but it was not")
  325. }
  326. return
  327. }
  328. result := getContainerAllocation(tc.req, tc.used, tc.allocationType)
  329. if diff := cmp.Diff(tc.expected, result); diff != "" {
  330. t.Errorf("getContainerAllocation() mismatch (-want +got):\n%s", diff)
  331. }
  332. })
  333. }
  334. }