costmodel_test.go 7.3 KB

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