intervals_test.go 12 KB

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