intervals_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. package costmodel
  2. import (
  3. "reflect"
  4. "testing"
  5. "time"
  6. "github.com/opencost/opencost/pkg/kubecost"
  7. )
  8. func TestGetIntervalPointsFromWindows(t *testing.T) {
  9. cases := []struct {
  10. name string
  11. pvcIntervalMap map[podKey]kubecost.Window
  12. expected []IntervalPoint
  13. }{
  14. {
  15. name: "four pods w/ various overlaps",
  16. pvcIntervalMap: map[podKey]kubecost.Window{
  17. // Pod running from 8 am to 9 am
  18. podKey{
  19. Pod: "Pod1",
  20. }: kubecost.Window(kubecost.NewClosedWindow(
  21. time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC),
  22. time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC),
  23. )),
  24. // Pod running from 8:30 am to 9 am
  25. podKey{
  26. Pod: "Pod2",
  27. }: kubecost.Window(kubecost.NewClosedWindow(
  28. time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC),
  29. time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC),
  30. )),
  31. // Pod running from 8:45 am to 9 am
  32. podKey{
  33. Pod: "Pod3",
  34. }: kubecost.Window(kubecost.NewClosedWindow(
  35. time.Date(2021, 2, 19, 8, 45, 0, 0, time.UTC),
  36. time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC),
  37. )),
  38. // Pod running from 8 am to 8:15 am
  39. podKey{
  40. Pod: "Pod4",
  41. }: kubecost.Window(kubecost.NewClosedWindow(
  42. time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC),
  43. time.Date(2021, 2, 19, 8, 15, 0, 0, time.UTC),
  44. )),
  45. },
  46. expected: []IntervalPoint{
  47. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", podKey{Pod: "Pod1"}),
  48. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", podKey{Pod: "Pod4"}),
  49. NewIntervalPoint(time.Date(2021, 2, 19, 8, 15, 0, 0, time.UTC), "end", podKey{Pod: "Pod4"}),
  50. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "start", podKey{Pod: "Pod2"}),
  51. NewIntervalPoint(time.Date(2021, 2, 19, 8, 45, 0, 0, time.UTC), "start", podKey{Pod: "Pod3"}),
  52. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", podKey{Pod: "Pod2"}),
  53. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", podKey{Pod: "Pod3"}),
  54. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", podKey{Pod: "Pod1"}),
  55. },
  56. },
  57. {
  58. name: "two pods no overlap",
  59. pvcIntervalMap: map[podKey]kubecost.Window{
  60. // Pod running from 8 am to 8:30 am
  61. podKey{
  62. Pod: "Pod1",
  63. }: kubecost.Window(kubecost.NewClosedWindow(
  64. time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC),
  65. time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC),
  66. )),
  67. // Pod running from 8:30 am to 9 am
  68. podKey{
  69. Pod: "Pod2",
  70. }: kubecost.Window(kubecost.NewClosedWindow(
  71. time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC),
  72. time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC),
  73. )),
  74. },
  75. expected: []IntervalPoint{
  76. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", podKey{Pod: "Pod1"}),
  77. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "start", podKey{Pod: "Pod2"}),
  78. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "end", podKey{Pod: "Pod1"}),
  79. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", podKey{Pod: "Pod2"}),
  80. },
  81. },
  82. {
  83. name: "two pods total overlap",
  84. pvcIntervalMap: map[podKey]kubecost.Window{
  85. // Pod running from 8:30 am to 9 am
  86. podKey{
  87. Pod: "Pod1",
  88. }: kubecost.Window(kubecost.NewClosedWindow(
  89. time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC),
  90. time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC),
  91. )),
  92. // Pod running from 8:30 am to 9 am
  93. podKey{
  94. Pod: "Pod2",
  95. }: kubecost.Window(kubecost.NewClosedWindow(
  96. time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC),
  97. time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC),
  98. )),
  99. },
  100. expected: []IntervalPoint{
  101. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "start", podKey{Pod: "Pod1"}),
  102. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "start", podKey{Pod: "Pod2"}),
  103. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", podKey{Pod: "Pod1"}),
  104. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", podKey{Pod: "Pod2"}),
  105. },
  106. },
  107. {
  108. name: "one pod",
  109. pvcIntervalMap: map[podKey]kubecost.Window{
  110. // Pod running from 8 am to 9 am
  111. podKey{
  112. Pod: "Pod1",
  113. }: kubecost.Window(kubecost.NewClosedWindow(
  114. time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC),
  115. time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC),
  116. )),
  117. },
  118. expected: []IntervalPoint{
  119. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", podKey{Pod: "Pod1"}),
  120. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", podKey{Pod: "Pod1"}),
  121. },
  122. },
  123. }
  124. for _, testCase := range cases {
  125. t.Run(testCase.name, func(t *testing.T) {
  126. result := getIntervalPointsFromWindows(testCase.pvcIntervalMap)
  127. if len(result) != len(testCase.expected) {
  128. t.Errorf("getIntervalPointsFromWindows test failed: %s: Got %+v but expected %+v", testCase.name, result, testCase.expected)
  129. }
  130. for i := range testCase.expected {
  131. // For correctness in terms of individual position of IntervalPoints, we only need to check the time/type.
  132. // Key is used in other associated calculations, so it must exist, but order does not matter if other sorting
  133. // logic is obeyed.
  134. if !testCase.expected[i].Time.Equal(result[i].Time) || testCase.expected[i].PointType != result[i].PointType {
  135. t.Errorf("getIntervalPointsFromWindows test failed: %s: Got point %s:%s but expected %s:%s", testCase.name, testCase.expected[i].PointType, testCase.expected[i].Time, result[i].PointType, result[i].Time)
  136. }
  137. }
  138. })
  139. }
  140. }
  141. func TestGetPVCCostCoefficients(t *testing.T) {
  142. pvc1 := &pvc{
  143. Bytes: 0,
  144. Name: "pvc1",
  145. Cluster: "cluster1",
  146. Namespace: "namespace1",
  147. Start: time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC),
  148. End: time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC),
  149. }
  150. pod1Key := newPodKey("cluster1", "namespace1", "pod1")
  151. pod2Key := newPodKey("cluster1", "namespace1", "pod2")
  152. pod3Key := newPodKey("cluster1", "namespace1", "pod3")
  153. pod4Key := newPodKey("cluster1", "namespace1", "pod4")
  154. ummountedPodKey := newPodKey("cluster1", kubecost.UnmountedSuffix, kubecost.UnmountedSuffix)
  155. cases := []struct {
  156. name string
  157. pvc *pvc
  158. pvcIntervalMap map[podKey]kubecost.Window
  159. intervals []IntervalPoint
  160. expected map[podKey][]CoefficientComponent
  161. }{
  162. {
  163. name: "four pods w/ various overlaps",
  164. pvc: pvc1,
  165. intervals: []IntervalPoint{
  166. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", pod1Key),
  167. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", pod4Key),
  168. NewIntervalPoint(time.Date(2021, 2, 19, 8, 15, 0, 0, time.UTC), "end", pod4Key),
  169. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "start", pod2Key),
  170. NewIntervalPoint(time.Date(2021, 2, 19, 8, 45, 0, 0, time.UTC), "start", pod3Key),
  171. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", pod2Key),
  172. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", pod3Key),
  173. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", pod1Key),
  174. },
  175. expected: map[podKey][]CoefficientComponent{
  176. pod1Key: []CoefficientComponent{
  177. CoefficientComponent{0.5, 0.25},
  178. CoefficientComponent{1, 0.25},
  179. CoefficientComponent{0.5, 0.25},
  180. CoefficientComponent{1.0 / 3.0, 0.25},
  181. },
  182. pod2Key: []CoefficientComponent{
  183. CoefficientComponent{0.5, 0.25},
  184. CoefficientComponent{1.0 / 3.0, 0.25},
  185. },
  186. pod3Key: []CoefficientComponent{
  187. CoefficientComponent{1.0 / 3.0, 0.25},
  188. },
  189. pod4Key: []CoefficientComponent{
  190. CoefficientComponent{0.5, 0.25},
  191. },
  192. },
  193. },
  194. {
  195. name: "two pods no overlap",
  196. pvc: pvc1,
  197. intervals: []IntervalPoint{
  198. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", pod1Key),
  199. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "start", pod2Key),
  200. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "end", pod1Key),
  201. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", pod2Key),
  202. },
  203. expected: map[podKey][]CoefficientComponent{
  204. pod1Key: []CoefficientComponent{
  205. CoefficientComponent{1.0, 0.5},
  206. },
  207. pod2Key: []CoefficientComponent{
  208. CoefficientComponent{1.0, 0.5},
  209. },
  210. },
  211. },
  212. {
  213. name: "two pods total overlap",
  214. pvc: pvc1,
  215. intervals: []IntervalPoint{
  216. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "start", pod1Key),
  217. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "start", pod2Key),
  218. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", pod1Key),
  219. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", pod2Key),
  220. },
  221. expected: map[podKey][]CoefficientComponent{
  222. pod1Key: []CoefficientComponent{
  223. CoefficientComponent{0.5, 0.5},
  224. },
  225. pod2Key: []CoefficientComponent{
  226. CoefficientComponent{0.5, 0.5},
  227. },
  228. ummountedPodKey: []CoefficientComponent{
  229. CoefficientComponent{1.0, 0.5},
  230. },
  231. },
  232. },
  233. {
  234. name: "one pod",
  235. pvc: pvc1,
  236. intervals: []IntervalPoint{
  237. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", pod1Key),
  238. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", pod1Key),
  239. },
  240. expected: map[podKey][]CoefficientComponent{
  241. pod1Key: []CoefficientComponent{
  242. CoefficientComponent{1.0, 1.0},
  243. },
  244. },
  245. },
  246. {
  247. name: "two pods with gap",
  248. pvc: pvc1,
  249. intervals: []IntervalPoint{
  250. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", pod1Key),
  251. NewIntervalPoint(time.Date(2021, 2, 19, 8, 15, 0, 0, time.UTC), "end", pod1Key),
  252. NewIntervalPoint(time.Date(2021, 2, 19, 8, 45, 0, 0, time.UTC), "start", pod2Key),
  253. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", pod2Key),
  254. },
  255. expected: map[podKey][]CoefficientComponent{
  256. pod1Key: []CoefficientComponent{
  257. CoefficientComponent{1.0, 0.25},
  258. },
  259. pod2Key: []CoefficientComponent{
  260. CoefficientComponent{1.0, 0.25},
  261. },
  262. ummountedPodKey: []CoefficientComponent{
  263. CoefficientComponent{1.0, 0.5},
  264. },
  265. },
  266. },
  267. {
  268. name: "one pods start and end in window",
  269. pvc: pvc1,
  270. intervals: []IntervalPoint{
  271. NewIntervalPoint(time.Date(2021, 2, 19, 8, 15, 0, 0, time.UTC), "start", pod1Key),
  272. NewIntervalPoint(time.Date(2021, 2, 19, 8, 45, 0, 0, time.UTC), "end", pod1Key),
  273. },
  274. expected: map[podKey][]CoefficientComponent{
  275. pod1Key: []CoefficientComponent{
  276. CoefficientComponent{1.0, 0.5},
  277. },
  278. ummountedPodKey: []CoefficientComponent{
  279. CoefficientComponent{1.0, 0.25},
  280. CoefficientComponent{1.0, 0.25},
  281. },
  282. },
  283. },
  284. }
  285. for _, testCase := range cases {
  286. t.Run(testCase.name, func(t *testing.T) {
  287. result := getPVCCostCoefficients(testCase.intervals, testCase.pvc)
  288. if !reflect.DeepEqual(result, testCase.expected) {
  289. t.Errorf("getPVCCostCoefficients test failed: %s: Got %+v but expected %+v", testCase.name, result, testCase.expected)
  290. }
  291. // check that coefficients sum to 1, to ensure that 100% of PVC cost is being distributed
  292. sum := 0.0
  293. for _, coefs := range result {
  294. sum += getCoefficientFromComponents(coefs)
  295. }
  296. if sum != 1.0 {
  297. t.Errorf("getPVCCostCoefficients test failed: coefficient totals did not sum to 1.0: %f", sum)
  298. }
  299. })
  300. }
  301. }