window_test.go 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204
  1. package kubecost
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "strings"
  6. "testing"
  7. "time"
  8. "github.com/google/go-cmp/cmp"
  9. "github.com/opencost/opencost/pkg/util/timeutil"
  10. "github.com/opencost/opencost/pkg/env"
  11. )
  12. func TestRoundBack(t *testing.T) {
  13. boulder := time.FixedZone("Boulder", -7*60*60)
  14. beijing := time.FixedZone("Beijing", 8*60*60)
  15. to := time.Date(2020, time.January, 1, 0, 0, 0, 0, boulder)
  16. tb := RoundBack(to, 24*time.Hour)
  17. if !tb.Equal(time.Date(2020, time.January, 1, 0, 0, 0, 0, boulder)) {
  18. t.Fatalf("RoundBack: expected 2020-01-01T00:00:00-07:00; actual %s", tb)
  19. }
  20. to = time.Date(2020, time.January, 1, 0, 0, 1, 0, boulder)
  21. tb = RoundBack(to, 24*time.Hour)
  22. if !tb.Equal(time.Date(2020, time.January, 1, 0, 0, 0, 0, boulder)) {
  23. t.Fatalf("RoundBack: expected 2020-01-01T00:00:00-07:00; actual %s", tb)
  24. }
  25. to = time.Date(2020, time.January, 1, 12, 37, 48, 0, boulder)
  26. tb = RoundBack(to, 24*time.Hour)
  27. if !tb.Equal(time.Date(2020, time.January, 1, 0, 0, 0, 0, boulder)) {
  28. t.Fatalf("RoundBack: expected 2020-01-01T00:00:00-07:00; actual %s", tb)
  29. }
  30. to = time.Date(2020, time.January, 1, 23, 37, 48, 0, boulder)
  31. tb = RoundBack(to, 24*time.Hour)
  32. if !tb.Equal(time.Date(2020, time.January, 1, 0, 0, 0, 0, boulder)) {
  33. t.Fatalf("RoundBack: expected 2020-01-01T00:00:00-07:00; actual %s", tb)
  34. }
  35. to = time.Date(2020, time.January, 1, 0, 0, 0, 0, beijing)
  36. tb = RoundBack(to, 24*time.Hour)
  37. if !tb.Equal(time.Date(2020, time.January, 1, 0, 0, 0, 0, beijing)) {
  38. t.Fatalf("RoundBack: expected 2020-01-01T00:00:00+08:00; actual %s", tb)
  39. }
  40. to = time.Date(2020, time.January, 1, 0, 0, 1, 0, beijing)
  41. tb = RoundBack(to, 24*time.Hour)
  42. if !tb.Equal(time.Date(2020, time.January, 1, 0, 0, 0, 0, beijing)) {
  43. t.Fatalf("RoundBack: expected 2020-01-01T00:00:00+08:00; actual %s", tb)
  44. }
  45. to = time.Date(2020, time.January, 1, 12, 37, 48, 0, beijing)
  46. tb = RoundBack(to, 24*time.Hour)
  47. if !tb.Equal(time.Date(2020, time.January, 1, 0, 0, 0, 0, beijing)) {
  48. t.Fatalf("RoundBack: expected 2020-01-01T00:00:00+08:00; actual %s", tb)
  49. }
  50. to = time.Date(2020, time.January, 1, 23, 59, 59, 0, beijing)
  51. tb = RoundBack(to, 24*time.Hour)
  52. if !tb.Equal(time.Date(2020, time.January, 1, 0, 0, 0, 0, beijing)) {
  53. t.Fatalf("RoundBack: expected 2020-01-01T00:00:00+08:00; actual %s", tb)
  54. }
  55. to = time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)
  56. tb = RoundBack(to, 24*time.Hour)
  57. if !tb.Equal(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) {
  58. t.Fatalf("RoundBack: expected 2020-01-01T00:00:00Z; actual %s", tb)
  59. }
  60. to = time.Date(2020, time.January, 1, 0, 0, 1, 0, time.UTC)
  61. tb = RoundBack(to, 24*time.Hour)
  62. if !tb.Equal(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) {
  63. t.Fatalf("RoundBack: expected 2020-01-01T00:00:00Z; actual %s", tb)
  64. }
  65. to = time.Date(2020, time.January, 1, 12, 37, 48, 0, time.UTC)
  66. tb = RoundBack(to, 24*time.Hour)
  67. if !tb.Equal(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) {
  68. t.Fatalf("RoundBack: expected 2020-01-01T00:00:00Z; actual %s", tb)
  69. }
  70. to = time.Date(2020, time.January, 1, 23, 59, 0, 0, time.UTC)
  71. tb = RoundBack(to, 24*time.Hour)
  72. if !tb.Equal(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) {
  73. t.Fatalf("RoundBack: expected 2020-01-01T00:00:00Z; actual %s", tb)
  74. }
  75. to = time.Date(2020, time.January, 1, 23, 59, 0, 0, time.UTC)
  76. tb = RoundBack(to, timeutil.Week)
  77. if !tb.Equal(time.Date(2019, time.December, 29, 0, 0, 0, 0, time.UTC)) {
  78. t.Fatalf("RoundForward: expected 2019-12-29T00:00:00Z; actual %s", tb)
  79. }
  80. }
  81. func TestRoundForward(t *testing.T) {
  82. boulder := time.FixedZone("Boulder", -7*60*60)
  83. beijing := time.FixedZone("Beijing", 8*60*60)
  84. to := time.Date(2020, time.January, 1, 0, 0, 0, 0, boulder)
  85. tb := RoundForward(to, 24*time.Hour)
  86. if !tb.Equal(time.Date(2020, time.January, 1, 0, 0, 0, 0, boulder)) {
  87. t.Fatalf("RoundForward: expected 2020-01-01T00:00:00-07:00; actual %s", tb)
  88. }
  89. to = time.Date(2020, time.January, 1, 0, 0, 1, 0, boulder)
  90. tb = RoundForward(to, 24*time.Hour)
  91. if !tb.Equal(time.Date(2020, time.January, 2, 0, 0, 0, 0, boulder)) {
  92. t.Fatalf("RoundForward: expected 2020-01-02T00:00:00-07:00; actual %s", tb)
  93. }
  94. to = time.Date(2020, time.January, 1, 12, 37, 48, 0, boulder)
  95. tb = RoundForward(to, 24*time.Hour)
  96. if !tb.Equal(time.Date(2020, time.January, 2, 0, 0, 0, 0, boulder)) {
  97. t.Fatalf("RoundForward: expected 2020-01-02T00:00:00-07:00; actual %s", tb)
  98. }
  99. to = time.Date(2020, time.January, 1, 23, 37, 48, 0, boulder)
  100. tb = RoundForward(to, 24*time.Hour)
  101. if !tb.Equal(time.Date(2020, time.January, 2, 0, 0, 0, 0, boulder)) {
  102. t.Fatalf("RoundForward: expected 2020-01-02T00:00:00-07:00; actual %s", tb)
  103. }
  104. to = time.Date(2020, time.January, 1, 0, 0, 0, 0, beijing)
  105. tb = RoundForward(to, 24*time.Hour)
  106. if !tb.Equal(time.Date(2020, time.January, 1, 0, 0, 0, 0, beijing)) {
  107. t.Fatalf("RoundForward: expected 2020-01-01T00:00:00+08:00; actual %s", tb)
  108. }
  109. to = time.Date(2020, time.January, 1, 0, 0, 1, 0, beijing)
  110. tb = RoundForward(to, 24*time.Hour)
  111. if !tb.Equal(time.Date(2020, time.January, 2, 0, 0, 0, 0, beijing)) {
  112. t.Fatalf("RoundForward: expected 2020-01-02T00:00:00+08:00; actual %s", tb)
  113. }
  114. to = time.Date(2020, time.January, 1, 12, 37, 48, 0, beijing)
  115. tb = RoundForward(to, 24*time.Hour)
  116. if !tb.Equal(time.Date(2020, time.January, 2, 0, 0, 0, 0, beijing)) {
  117. t.Fatalf("RoundForward: expected 2020-01-02T00:00:00+08:00; actual %s", tb)
  118. }
  119. to = time.Date(2020, time.January, 1, 23, 59, 59, 0, beijing)
  120. tb = RoundForward(to, 24*time.Hour)
  121. if !tb.Equal(time.Date(2020, time.January, 2, 0, 0, 0, 0, beijing)) {
  122. t.Fatalf("RoundForward: expected 2020-01-02T00:00:00+08:00; actual %s", tb)
  123. }
  124. to = time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)
  125. tb = RoundForward(to, 24*time.Hour)
  126. if !tb.Equal(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) {
  127. t.Fatalf("RoundForward: expected 2020-01-01T00:00:00Z; actual %s", tb)
  128. }
  129. to = time.Date(2020, time.January, 1, 0, 0, 1, 0, time.UTC)
  130. tb = RoundForward(to, 24*time.Hour)
  131. if !tb.Equal(time.Date(2020, time.January, 2, 0, 0, 0, 0, time.UTC)) {
  132. t.Fatalf("RoundForward: expected 2020-01-02T00:00:00Z; actual %s", tb)
  133. }
  134. to = time.Date(2020, time.January, 1, 12, 37, 48, 0, time.UTC)
  135. tb = RoundForward(to, 24*time.Hour)
  136. if !tb.Equal(time.Date(2020, time.January, 2, 0, 0, 0, 0, time.UTC)) {
  137. t.Fatalf("RoundForward: expected 2020-01-02T00:00:00Z; actual %s", tb)
  138. }
  139. to = time.Date(2020, time.January, 1, 23, 59, 0, 0, time.UTC)
  140. tb = RoundForward(to, 24*time.Hour)
  141. if !tb.Equal(time.Date(2020, time.January, 2, 0, 0, 0, 0, time.UTC)) {
  142. t.Fatalf("RoundForward: expected 2020-01-02T00:00:00Z; actual %s", tb)
  143. }
  144. to = time.Date(2020, time.January, 1, 23, 59, 0, 0, time.UTC)
  145. tb = RoundForward(to, timeutil.Week)
  146. if !tb.Equal(time.Date(2020, time.January, 5, 0, 0, 0, 0, time.UTC)) {
  147. t.Fatalf("RoundForward: expected 2020-01-05T00:00:00Z; actual %s", tb)
  148. }
  149. }
  150. func TestParseWindowUTC(t *testing.T) {
  151. now := time.Now().UTC()
  152. // "today" should span Now() and not produce an error
  153. today, err := ParseWindowUTC("today")
  154. if err != nil {
  155. t.Fatalf(`unexpected error parsing "today": %s`, err)
  156. }
  157. if today.Duration().Hours() != 24 {
  158. t.Fatalf(`expect: window "today" to have duration 24 hour; actual: %f hours`, today.Duration().Hours())
  159. }
  160. if !today.Contains(time.Now().UTC()) {
  161. t.Fatalf(`expect: window "today" to contain now; actual: %s`, today)
  162. }
  163. // "yesterday" should span Now() and not produce an error
  164. yesterday, err := ParseWindowUTC("yesterday")
  165. if err != nil {
  166. t.Fatalf(`unexpected error parsing "yesterday": %s`, err)
  167. }
  168. if yesterday.Duration().Hours() != 24 {
  169. t.Fatalf(`expect: window "yesterday" to have duration 24 hour; actual: %f hours`, yesterday.Duration().Hours())
  170. }
  171. if !yesterday.End().Before(time.Now().UTC()) {
  172. t.Fatalf(`expect: window "yesterday" to end before now; actual: %s ends after %s`, yesterday, time.Now().UTC())
  173. }
  174. week, err := ParseWindowUTC("week")
  175. hoursThisWeek := float64(time.Now().UTC().Weekday()) * 24.0
  176. if err != nil {
  177. t.Fatalf(`unexpected error parsing "week": %s`, err)
  178. }
  179. if week.Duration().Hours() < hoursThisWeek {
  180. t.Fatalf(`expect: window "week" to have at least %f hours; actual: %f hours`, hoursThisWeek, week.Duration().Hours())
  181. }
  182. if week.End().After(time.Now().UTC()) {
  183. t.Fatalf(`expect: window "week" to end before now; actual: %s ends after %s`, week, time.Now().UTC())
  184. }
  185. month, err := ParseWindowUTC("month")
  186. hoursThisMonth := float64(time.Now().UTC().Day()) * 24.0
  187. if err != nil {
  188. t.Fatalf(`unexpected error parsing "month": %s`, err)
  189. }
  190. if month.Duration().Hours() > hoursThisMonth || month.Duration().Hours() < (hoursThisMonth-24.0) {
  191. t.Fatalf(`expect: window "month" to have approximately %f hours; actual: %f hours`, hoursThisMonth, month.Duration().Hours())
  192. }
  193. if !month.End().Before(time.Now().UTC()) {
  194. t.Fatalf(`expect: window "month" to end before now; actual: %s ends after %s`, month, time.Now().UTC())
  195. }
  196. // TODO lastweek
  197. lastmonth, err := ParseWindowUTC("lastmonth")
  198. monthMinHours := float64(24 * 28)
  199. monthMaxHours := float64(24 * 31)
  200. firstOfMonth := now.Truncate(time.Hour * 24).Add(-24 * time.Hour * time.Duration(now.Day()-1))
  201. if err != nil {
  202. t.Fatalf(`unexpected error parsing "lastmonth": %s`, err)
  203. }
  204. if lastmonth.Duration().Hours() > monthMaxHours || lastmonth.Duration().Hours() < monthMinHours {
  205. t.Fatalf(`expect: window "lastmonth" to have approximately %f hours; actual: %f hours`, hoursThisMonth, lastmonth.Duration().Hours())
  206. }
  207. if !lastmonth.End().Equal(firstOfMonth) {
  208. t.Fatalf(`expect: window "lastmonth" to end on the first of the current month; actual: %s doesn't end on %s`, lastmonth, firstOfMonth)
  209. }
  210. ago12h := time.Now().UTC().Add(-12 * time.Hour)
  211. ago36h := time.Now().UTC().Add(-36 * time.Hour)
  212. ago60h := time.Now().UTC().Add(-60 * time.Hour)
  213. // "24h" should have 24 hour duration and not produce an error
  214. dur24h, err := ParseWindowUTC("24h")
  215. if err != nil {
  216. t.Fatalf(`unexpected error parsing "24h": %s`, err)
  217. }
  218. if dur24h.Duration().Hours() != 24 {
  219. t.Fatalf(`expect: window "24h" to have duration 24 hour; actual: %f hours`, dur24h.Duration().Hours())
  220. }
  221. if !dur24h.Contains(ago12h) {
  222. t.Fatalf(`expect: window "24h" to contain 12 hours ago; actual: %s doesn't contain %s`, dur24h, ago12h)
  223. }
  224. if dur24h.Contains(ago36h) {
  225. t.Fatalf(`expect: window "24h" to not contain 36 hours ago; actual: %s contains %s`, dur24h, ago36h)
  226. }
  227. // "2d" should have 2 day duration and not produce an error
  228. dur2d, err := ParseWindowUTC("2d")
  229. if err != nil {
  230. t.Fatalf(`unexpected error parsing "2d": %s`, err)
  231. }
  232. if dur2d.Duration().Hours() != 48 {
  233. t.Fatalf(`expect: window "2d" to have duration 48 hour; actual: %f hours`, dur2d.Duration().Hours())
  234. }
  235. if !dur2d.Contains(ago36h) {
  236. t.Fatalf(`expect: window "2d" to contain 36 hours ago; actual: %s doesn't contain %s`, dur2d, ago36h)
  237. }
  238. if dur2d.Contains(ago60h) {
  239. t.Fatalf(`expect: window "2d" to not contain 60 hours ago; actual: %s contains %s`, dur2d, ago60h)
  240. }
  241. // "24h offset 14h" should have 24 hour duration and not produce an error
  242. dur24hOff14h, err := ParseWindowUTC("24h offset 14h")
  243. if err != nil {
  244. t.Fatalf(`unexpected error parsing "24h offset 14h": %s`, err)
  245. }
  246. if dur24hOff14h.Duration().Hours() != 24 {
  247. t.Fatalf(`expect: window "24h offset 14h" to have duration 24 hour; actual: %f hours`, dur24hOff14h.Duration().Hours())
  248. }
  249. if dur24hOff14h.Contains(ago12h) {
  250. t.Fatalf(`expect: window "24h offset 14h" not to contain 12 hours ago; actual: %s contains %s`, dur24hOff14h, ago12h)
  251. }
  252. if !dur24hOff14h.Contains(ago36h) {
  253. t.Fatalf(`expect: window "24h offset 14h" to contain 36 hours ago; actual: %s does not contain %s`, dur24hOff14h, ago36h)
  254. }
  255. april152020, _ := time.Parse(time.RFC3339, "2020-04-15T00:00:00Z")
  256. april102020, _ := time.Parse(time.RFC3339, "2020-04-10T00:00:00Z")
  257. april052020, _ := time.Parse(time.RFC3339, "2020-04-05T00:00:00Z")
  258. // "2020-04-08T00:00:00Z,2020-04-12T00:00:00Z" should have 96 hour duration and not produce an error
  259. april8to12, err := ParseWindowUTC("2020-04-08T00:00:00Z,2020-04-12T00:00:00Z")
  260. if err != nil {
  261. t.Fatalf(`unexpected error parsing "2020-04-08T00:00:00Z,2020-04-12T00:00:00Z": %s`, err)
  262. }
  263. if april8to12.Duration().Hours() != 96 {
  264. t.Fatalf(`expect: window %s to have duration 96 hour; actual: %f hours`, april8to12, april8to12.Duration().Hours())
  265. }
  266. if !april8to12.Contains(april102020) {
  267. t.Fatalf(`expect: window April 8-12 to contain April 10; actual: %s doesn't contain %s`, april8to12, april102020)
  268. }
  269. if april8to12.Contains(april052020) {
  270. t.Fatalf(`expect: window April 8-12 to not contain April 5; actual: %s contains %s`, april8to12, april052020)
  271. }
  272. if april8to12.Contains(april152020) {
  273. t.Fatalf(`expect: window April 8-12 to not contain April 15; actual: %s contains %s`, april8to12, april152020)
  274. }
  275. march152020, _ := time.Parse(time.RFC3339, "2020-03-15T00:00:00Z")
  276. march102020, _ := time.Parse(time.RFC3339, "2020-03-10T00:00:00Z")
  277. march052020, _ := time.Parse(time.RFC3339, "2020-03-05T00:00:00Z")
  278. // "1583712000,1583884800" should have 48 hour duration and not produce an error
  279. march9to11, err := ParseWindowUTC("1583712000,1583884800")
  280. if err != nil {
  281. t.Fatalf(`unexpected error parsing "2020-04-08T00:00:00Z,2020-04-12T00:00:00Z": %s`, err)
  282. }
  283. if march9to11.Duration().Hours() != 48 {
  284. t.Fatalf(`expect: window %s to have duration 48 hour; actual: %f hours`, march9to11, march9to11.Duration().Hours())
  285. }
  286. if !march9to11.Contains(march102020) {
  287. t.Fatalf(`expect: window March 9-11 to contain March 10; actual: %s doesn't contain %s`, march9to11, march102020)
  288. }
  289. if march9to11.Contains(march052020) {
  290. t.Fatalf(`expect: window March 9-11 to not contain March 5; actual: %s contains %s`, march9to11, march052020)
  291. }
  292. if march9to11.Contains(march152020) {
  293. t.Fatalf(`expect: window March 9-11 to not contain March 15; actual: %s contains %s`, march9to11, march152020)
  294. }
  295. }
  296. func BenchmarkParseWindowUTC(b *testing.B) {
  297. for n := 0; n < b.N; n++ {
  298. _, err := ParseWindowUTC("2020-04-08T00:00:00Z,2020-04-12T00:00:00Z")
  299. if err != nil {
  300. b.Fatalf("error running benchmark: %s", err.Error())
  301. }
  302. }
  303. }
  304. func TestParseWindowWithOffsetString(t *testing.T) {
  305. // ParseWindowWithOffsetString should equal ParseWindowUTC when location == "UTC"
  306. // for all window string formats
  307. todayUTC, err := ParseWindowUTC("today")
  308. if err != nil {
  309. t.Fatalf(`unexpected error parsing "today": %s`, err)
  310. }
  311. todayTZ, err := ParseWindowWithOffsetString("today", "UTC")
  312. if err != nil {
  313. t.Fatalf(`unexpected error parsing "today": %s`, err)
  314. }
  315. if !todayUTC.ApproximatelyEqual(todayTZ, time.Millisecond) {
  316. t.Fatalf(`expect: window "today" UTC to equal "today" with timezone "UTC"; actual: %s not equal %s`, todayUTC, todayTZ)
  317. }
  318. yesterdayUTC, err := ParseWindowUTC("yesterday")
  319. if err != nil {
  320. t.Fatalf(`unexpected error parsing "yesterday": %s`, err)
  321. }
  322. yesterdayTZ, err := ParseWindowWithOffsetString("yesterday", "UTC")
  323. if err != nil {
  324. t.Fatalf(`unexpected error parsing "yesterday": %s`, err)
  325. }
  326. if !yesterdayUTC.ApproximatelyEqual(yesterdayTZ, time.Millisecond) {
  327. t.Fatalf(`expect: window "yesterday" UTC to equal "yesterday" with timezone "UTC"; actual: %s not equal %s`, yesterdayUTC, yesterdayTZ)
  328. }
  329. weekUTC, err := ParseWindowUTC("week")
  330. if err != nil {
  331. t.Fatalf(`unexpected error parsing "week": %s`, err)
  332. }
  333. weekTZ, err := ParseWindowWithOffsetString("week", "UTC")
  334. if err != nil {
  335. t.Fatalf(`unexpected error parsing "week": %s`, err)
  336. }
  337. if !weekUTC.ApproximatelyEqual(weekTZ, time.Millisecond) {
  338. t.Fatalf(`expect: window "week" UTC to equal "week" with timezone "UTC"; actual: %s not equal %s`, weekUTC, weekTZ)
  339. }
  340. monthUTC, err := ParseWindowUTC("month")
  341. if err != nil {
  342. t.Fatalf(`unexpected error parsing "month": %s`, err)
  343. }
  344. monthTZ, err := ParseWindowWithOffsetString("month", "UTC")
  345. if err != nil {
  346. t.Fatalf(`unexpected error parsing "month": %s`, err)
  347. }
  348. if !monthUTC.ApproximatelyEqual(monthTZ, time.Millisecond) {
  349. t.Fatalf(`expect: window "month" UTC to equal "month" with timezone "UTC"; actual: %s not equal %s`, monthUTC, monthTZ)
  350. }
  351. lastweekUTC, err := ParseWindowUTC("lastweek")
  352. if err != nil {
  353. t.Fatalf(`unexpected error parsing "lastweek": %s`, err)
  354. }
  355. lastweekTZ, err := ParseWindowWithOffsetString("lastweek", "UTC")
  356. if err != nil {
  357. t.Fatalf(`unexpected error parsing "lastweek": %s`, err)
  358. }
  359. if !lastweekUTC.ApproximatelyEqual(lastweekTZ, time.Millisecond) {
  360. t.Fatalf(`expect: window "lastweek" UTC to equal "lastweek" with timezone "UTC"; actual: %s not equal %s`, lastweekUTC, lastweekTZ)
  361. }
  362. lastmonthUTC, err := ParseWindowUTC("lastmonth")
  363. if err != nil {
  364. t.Fatalf(`unexpected error parsing "lastmonth": %s`, err)
  365. }
  366. lastmonthTZ, err := ParseWindowWithOffsetString("lastmonth", "UTC")
  367. if err != nil {
  368. t.Fatalf(`unexpected error parsing "lastmonth": %s`, err)
  369. }
  370. if !lastmonthUTC.ApproximatelyEqual(lastmonthTZ, time.Millisecond) {
  371. t.Fatalf(`expect: window "lastmonth" UTC to equal "lastmonth" with timezone "UTC"; actual: %s not equal %s`, lastmonthUTC, lastmonthTZ)
  372. }
  373. dur10mUTC, err := ParseWindowUTC("10m")
  374. if err != nil {
  375. t.Fatalf(`unexpected error parsing "10m": %s`, err)
  376. }
  377. dur10mTZ, err := ParseWindowWithOffsetString("10m", "UTC")
  378. if err != nil {
  379. t.Fatalf(`unexpected error parsing "10m": %s`, err)
  380. }
  381. if !dur10mUTC.ApproximatelyEqual(dur10mTZ, time.Millisecond) {
  382. t.Fatalf(`expect: window "10m" UTC to equal "10m" with timezone "UTC"; actual: %s not equal %s`, dur10mUTC, dur10mTZ)
  383. }
  384. dur24hUTC, err := ParseWindowUTC("24h")
  385. if err != nil {
  386. t.Fatalf(`unexpected error parsing "24h": %s`, err)
  387. }
  388. dur24hTZ, err := ParseWindowWithOffsetString("24h", "UTC")
  389. if err != nil {
  390. t.Fatalf(`unexpected error parsing "24h": %s`, err)
  391. }
  392. if !dur24hUTC.ApproximatelyEqual(dur24hTZ, time.Millisecond) {
  393. t.Fatalf(`expect: window "24h" UTC to equal "24h" with timezone "UTC"; actual: %s not equal %s`, dur24hUTC, dur24hTZ)
  394. }
  395. dur37dUTC, err := ParseWindowUTC("37d")
  396. if err != nil {
  397. t.Fatalf(`unexpected error parsing "37d": %s`, err)
  398. }
  399. dur37dTZ, err := ParseWindowWithOffsetString("37d", "UTC")
  400. if err != nil {
  401. t.Fatalf(`unexpected error parsing "37d": %s`, err)
  402. }
  403. if !dur37dUTC.ApproximatelyEqual(dur37dTZ, time.Millisecond) {
  404. t.Fatalf(`expect: window "37d" UTC to equal "37d" with timezone "UTC"; actual: %s not equal %s`, dur37dUTC, dur37dTZ)
  405. }
  406. // ParseWindowWithOffsetString should be the correct relative to ParseWindowUTC; i.e.
  407. // - for durations, the times should match, but the representations should differ
  408. // by the number of hours offset
  409. // - for words like "today" and "yesterday", the times may not match, in which
  410. // case, for example, "today" UTC-08:00 might equal "yesterday" UTC
  411. // fmtWindow only compares date and time to the minute, not second or
  412. // timezone. Helper for comparing timezone shifted windows.
  413. fmtWindow := func(w Window) string {
  414. s := "nil"
  415. if w.start != nil {
  416. s = w.start.Format("2006-01-02T15:04")
  417. }
  418. e := "nil"
  419. if w.end != nil {
  420. e = w.end.Format("2006-01-02T15:04")
  421. }
  422. return fmt.Sprintf("[%s, %s]", s, e)
  423. }
  424. // Test UTC-08:00 (California), UTC+03:00 (Moscow), UTC+12:00 (New Zealand), and UTC itself
  425. for _, offsetHrs := range []int{-8, 3, 12, 0} {
  426. offStr := fmt.Sprintf("+%02d:00", offsetHrs)
  427. if offsetHrs < 0 {
  428. offStr = fmt.Sprintf("-%02d:00", -offsetHrs)
  429. }
  430. off := time.Duration(offsetHrs) * time.Hour
  431. dur10mTZ, err = ParseWindowWithOffsetString("10m", offStr)
  432. if err != nil {
  433. t.Fatalf(`unexpected error parsing "10m": %s`, err)
  434. }
  435. if !dur10mTZ.ApproximatelyEqual(dur10mUTC, time.Second) {
  436. t.Fatalf(`expect: window "10m" UTC to equal "10m" with timezone "%s"; actual: %s not equal %s`, offStr, dur10mUTC, dur10mTZ)
  437. }
  438. if fmtWindow(dur10mTZ.Shift(-off)) != fmtWindow(dur10mUTC) {
  439. t.Fatalf(`expect: date, hour, and minute of window "10m" UTC to equal that of "10m" %s shifted by %s; actual: %s not equal %s`, offStr, off, fmtWindow(dur10mUTC), fmtWindow(dur10mTZ.Shift(-off)))
  440. }
  441. dur24hTZ, err = ParseWindowWithOffsetString("24h", offStr)
  442. if err != nil {
  443. t.Fatalf(`unexpected error parsing "24h": %s`, err)
  444. }
  445. if !dur24hTZ.ApproximatelyEqual(dur24hUTC, time.Second) {
  446. t.Fatalf(`expect: window "24h" UTC to equal "24h" with timezone "%s"; actual: %s not equal %s`, offStr, dur24hUTC, dur24hTZ)
  447. }
  448. if fmtWindow(dur24hTZ.Shift(-off)) != fmtWindow(dur24hUTC) {
  449. t.Fatalf(`expect: date, hour, and minute of window "24h" UTC to equal that of "24h" %s shifted by %s; actual: %s not equal %s`, offStr, off, fmtWindow(dur24hUTC), fmtWindow(dur24hTZ.Shift(-off)))
  450. }
  451. dur37dTZ, err = ParseWindowWithOffsetString("37d", offStr)
  452. if err != nil {
  453. t.Fatalf(`unexpected error parsing "37d": %s`, err)
  454. }
  455. if !dur37dTZ.ApproximatelyEqual(dur37dUTC, time.Second) {
  456. t.Fatalf(`expect: window "37d" UTC to equal "37d" with timezone "%s"; actual: %s not equal %s`, offStr, dur37dUTC, dur37dTZ)
  457. }
  458. if fmtWindow(dur37dTZ.Shift(-off)) != fmtWindow(dur37dUTC) {
  459. t.Fatalf(`expect: date, hour, and minute of window "37d" UTC to equal that of "37d" %s shifted by %s; actual: %s not equal %s`, offStr, off, fmtWindow(dur37dUTC), fmtWindow(dur37dTZ.Shift(-off)))
  460. }
  461. // "today" and "yesterday" should comply with the current day in each
  462. // respective timezone, depending on if it is ahead of, equal to, or
  463. // behind UTC at the given moment.
  464. todayTZ, err = ParseWindowWithOffsetString("today", offStr)
  465. if err != nil {
  466. t.Fatalf(`unexpected error parsing "today": %s`, err)
  467. }
  468. yesterdayTZ, err = ParseWindowWithOffsetString("yesterday", offStr)
  469. if err != nil {
  470. t.Fatalf(`unexpected error parsing "yesterday": %s`, err)
  471. }
  472. hoursSinceYesterdayUTC := time.Now().UTC().Sub(time.Now().UTC().Truncate(24.0 * time.Hour)).Hours()
  473. hoursUntilTomorrowUTC := 24.0 - hoursSinceYesterdayUTC
  474. aheadOfUTC := float64(offsetHrs)-hoursUntilTomorrowUTC > 0
  475. behindUTC := float64(offsetHrs)+hoursSinceYesterdayUTC < 0
  476. // yesterday in this timezone should equal today UTC
  477. if aheadOfUTC {
  478. if fmtWindow(yesterdayTZ) != fmtWindow(todayUTC) {
  479. t.Fatalf(`expect: window "today" UTC to equal "yesterday" with timezone "%s"; actual: %s not equal %s`, offStr, yesterdayTZ, todayUTC)
  480. }
  481. }
  482. // today in this timezone should equal yesterday UTC
  483. if behindUTC {
  484. if fmtWindow(todayTZ) != fmtWindow(yesterdayUTC) {
  485. t.Fatalf(`expect: window "today" UTC to equal "yesterday" with timezone "%s"; actual: %s not equal %s`, offStr, todayTZ, yesterdayUTC)
  486. }
  487. }
  488. // today in this timezone should equal today UTC, likewise for yesterday
  489. if !aheadOfUTC && !behindUTC {
  490. if fmtWindow(todayTZ) != fmtWindow(todayUTC) {
  491. t.Fatalf(`expect: window "today" UTC to equal "today" with timezone "%s"; actual: %s not equal %s`, offStr, todayTZ, todayUTC)
  492. }
  493. // yesterday in this timezone should equal yesterday UTC
  494. if fmtWindow(yesterdayTZ) != fmtWindow(yesterdayUTC) {
  495. t.Fatalf(`expect: window "yesterday" UTC to equal "yesterday" with timezone "%s"; actual: %s not equal %s`, offStr, yesterdayTZ, yesterdayUTC)
  496. }
  497. }
  498. }
  499. }
  500. func TestWindow_DurationOffsetStrings(t *testing.T) {
  501. w, err := ParseWindowUTC("1d")
  502. if err != nil {
  503. t.Fatalf(`unexpected error parsing "1d": %s`, err)
  504. }
  505. dur, off := w.DurationOffsetStrings()
  506. if dur != "1d" {
  507. t.Fatalf(`expect: window to be "1d"; actual: "%s"`, dur)
  508. }
  509. if off != "" {
  510. t.Fatalf(`expect: offset to be ""; actual: "%s"`, off)
  511. }
  512. w, err = ParseWindowUTC("3h")
  513. if err != nil {
  514. t.Fatalf(`unexpected error parsing "1d": %s`, err)
  515. }
  516. dur, off = w.DurationOffsetStrings()
  517. if dur != "3h" {
  518. t.Fatalf(`expect: window to be "3h"; actual: "%s"`, dur)
  519. }
  520. if off != "" {
  521. t.Fatalf(`expect: offset to be ""; actual: "%s"`, off)
  522. }
  523. w, err = ParseWindowUTC("10m")
  524. if err != nil {
  525. t.Fatalf(`unexpected error parsing "1d": %s`, err)
  526. }
  527. dur, off = w.DurationOffsetStrings()
  528. if dur != "10m" {
  529. t.Fatalf(`expect: window to be "10m"; actual: "%s"`, dur)
  530. }
  531. if off != "" {
  532. t.Fatalf(`expect: offset to be ""; actual: "%s"`, off)
  533. }
  534. w, err = ParseWindowUTC("1589448338,1589534798")
  535. if err != nil {
  536. t.Fatalf(`unexpected error parsing "1589448338,1589534798": %s`, err)
  537. }
  538. dur, off = w.DurationOffsetStrings()
  539. if dur != "1441m" {
  540. t.Fatalf(`expect: window to be "1441m"; actual: "%s"`, dur)
  541. }
  542. if off == "" {
  543. t.Fatalf(`expect: offset to not be ""; actual: "%s"`, off)
  544. }
  545. w, err = ParseWindowUTC("yesterday")
  546. if err != nil {
  547. t.Fatalf(`unexpected error parsing "1589448338,1589534798": %s`, err)
  548. }
  549. dur, _ = w.DurationOffsetStrings()
  550. if dur != "1d" {
  551. t.Fatalf(`expect: window to be "1d"; actual: "%s"`, dur)
  552. }
  553. }
  554. func TestWindow_DurationOffsetForPrometheus(t *testing.T) {
  555. // Set-up and tear-down
  556. thanosEnabled := env.GetBool(env.ThanosEnabledEnvVar, false)
  557. defer env.SetBool(env.ThanosEnabledEnvVar, thanosEnabled)
  558. // Test for Prometheus (env.IsThanosEnabled() == false)
  559. env.SetBool(env.ThanosEnabledEnvVar, false)
  560. if env.IsThanosEnabled() {
  561. t.Fatalf("expected env.IsThanosEnabled() == false")
  562. }
  563. w, err := ParseWindowUTC("1d")
  564. if err != nil {
  565. t.Fatalf(`unexpected error parsing "1d": %s`, err)
  566. }
  567. dur, off, err := w.DurationOffsetForPrometheus()
  568. if err != nil {
  569. t.Fatalf("unexpected error: %s", err)
  570. }
  571. if dur != "1d" {
  572. t.Fatalf(`expect: window to be "1d"; actual: "%s"`, dur)
  573. }
  574. if off != "" {
  575. t.Fatalf(`expect: offset to be ""; actual: "%s"`, off)
  576. }
  577. w, err = ParseWindowUTC("2h")
  578. if err != nil {
  579. t.Fatalf(`unexpected error parsing "2h": %s`, err)
  580. }
  581. dur, off, err = w.DurationOffsetForPrometheus()
  582. if err != nil {
  583. t.Fatalf("unexpected error: %s", err)
  584. }
  585. if dur != "2h" {
  586. t.Fatalf(`expect: window to be "2h"; actual: "%s"`, dur)
  587. }
  588. if off != "" {
  589. t.Fatalf(`expect: offset to be ""; actual: "%s"`, off)
  590. }
  591. w, err = ParseWindowUTC("10m")
  592. if err != nil {
  593. t.Fatalf(`unexpected error parsing "10m": %s`, err)
  594. }
  595. dur, off, err = w.DurationOffsetForPrometheus()
  596. if err != nil {
  597. t.Fatalf("unexpected error: %s", err)
  598. }
  599. if dur != "10m" {
  600. t.Fatalf(`expect: window to be "10m"; actual: "%s"`, dur)
  601. }
  602. if off != "" {
  603. t.Fatalf(`expect: offset to be ""; actual: "%s"`, off)
  604. }
  605. w, err = ParseWindowUTC("1589448338,1589534798")
  606. if err != nil {
  607. t.Fatalf(`unexpected error parsing "1589448338,1589534798": %s`, err)
  608. }
  609. dur, off, err = w.DurationOffsetForPrometheus()
  610. if err != nil {
  611. t.Fatalf("unexpected error: %s", err)
  612. }
  613. if dur != "1441m" {
  614. t.Fatalf(`expect: window to be "1441m"; actual: "%s"`, dur)
  615. }
  616. if !strings.HasPrefix(off, " offset ") {
  617. t.Fatalf(`expect: offset to start with " offset "; actual: "%s"`, off)
  618. }
  619. w, err = ParseWindowUTC("yesterday")
  620. if err != nil {
  621. t.Fatalf(`unexpected error parsing "yesterday": %s`, err)
  622. }
  623. dur, off, err = w.DurationOffsetForPrometheus()
  624. if err != nil {
  625. t.Fatalf("unexpected error: %s", err)
  626. }
  627. if dur != "1d" {
  628. t.Fatalf(`expect: window to be "1d"; actual: "%s"`, dur)
  629. }
  630. if !strings.HasPrefix(off, " offset ") {
  631. t.Fatalf(`expect: offset to start with " offset "; actual: "%s"`, off)
  632. }
  633. // Test for Thanos (env.IsThanosEnabled() == true)
  634. env.SetBool(env.ThanosEnabledEnvVar, true)
  635. if !env.IsThanosEnabled() {
  636. t.Fatalf("expected env.IsThanosEnabled() == true")
  637. }
  638. w, err = ParseWindowUTC("1d")
  639. if err != nil {
  640. t.Fatalf(`unexpected error parsing "1d": %s`, err)
  641. }
  642. dur, off, err = w.DurationOffsetForPrometheus()
  643. if err != nil {
  644. t.Fatalf("unexpected error: %s", err)
  645. }
  646. if dur != "21h" {
  647. t.Fatalf(`expect: window to be "21d"; actual: "%s"`, dur)
  648. }
  649. if off != " offset 3h" {
  650. t.Fatalf(`expect: offset to be " offset 3h"; actual: "%s"`, off)
  651. }
  652. w, err = ParseWindowUTC("2h")
  653. if err != nil {
  654. t.Fatalf(`unexpected error parsing "2h": %s`, err)
  655. }
  656. dur, off, err = w.DurationOffsetForPrometheus()
  657. if err == nil {
  658. t.Fatalf(`expected error (negative duration); got ("%s", "%s")`, dur, off)
  659. }
  660. w, err = ParseWindowUTC("10m")
  661. if err != nil {
  662. t.Fatalf(`unexpected error parsing "1d": %s`, err)
  663. }
  664. dur, off, err = w.DurationOffsetForPrometheus()
  665. if err == nil {
  666. t.Fatalf(`expected error (negative duration); got ("%s", "%s")`, dur, off)
  667. }
  668. w, err = ParseWindowUTC("1589448338,1589534798")
  669. if err != nil {
  670. t.Fatalf(`unexpected error parsing "1589448338,1589534798": %s`, err)
  671. }
  672. dur, off, err = w.DurationOffsetForPrometheus()
  673. if err != nil {
  674. t.Fatalf("unexpected error: %s", err)
  675. }
  676. if dur != "1441m" {
  677. t.Fatalf(`expect: window to be "1441m"; actual: "%s"`, dur)
  678. }
  679. if !strings.HasPrefix(off, " offset ") {
  680. t.Fatalf(`expect: offset to start with " offset "; actual: "%s"`, off)
  681. }
  682. }
  683. // TODO
  684. // func TestWindow_Overlaps(t *testing.T) {}
  685. // TODO
  686. // func TestWindow_Contains(t *testing.T) {}
  687. // TODO
  688. // func TestWindow_Duration(t *testing.T) {}
  689. // TODO
  690. // func TestWindow_End(t *testing.T) {}
  691. // TODO
  692. // func TestWindow_Equal(t *testing.T) {}
  693. // TODO
  694. // func TestWindow_ExpandStart(t *testing.T) {}
  695. // TODO
  696. // func TestWindow_ExpandEnd(t *testing.T) {}
  697. func TestWindow_Expand(t *testing.T) {
  698. t1 := time.Now().Round(time.Hour)
  699. t2 := t1.Add(34 * time.Minute)
  700. t3 := t1.Add(50 * time.Minute)
  701. t4 := t1.Add(84 * time.Minute)
  702. cases := []struct {
  703. windowToExpand Window
  704. windowArgument Window
  705. expected Window
  706. }{
  707. {
  708. windowToExpand: NewClosedWindow(t1, t2),
  709. windowArgument: NewClosedWindow(t3, t4),
  710. expected: NewClosedWindow(t1, t4),
  711. },
  712. {
  713. windowToExpand: NewClosedWindow(t3, t4),
  714. windowArgument: NewClosedWindow(t1, t2),
  715. expected: NewClosedWindow(t1, t4),
  716. },
  717. {
  718. windowToExpand: NewClosedWindow(t1, t3),
  719. windowArgument: NewClosedWindow(t2, t4),
  720. expected: NewClosedWindow(t1, t4),
  721. },
  722. {
  723. windowToExpand: NewClosedWindow(t2, t4),
  724. windowArgument: NewClosedWindow(t1, t3),
  725. expected: NewClosedWindow(t1, t4),
  726. },
  727. {
  728. windowToExpand: Window{},
  729. windowArgument: NewClosedWindow(t1, t2),
  730. expected: NewClosedWindow(t1, t2),
  731. },
  732. {
  733. windowToExpand: NewWindow(nil, &t2),
  734. windowArgument: NewWindow(nil, &t3),
  735. expected: NewWindow(nil, &t3),
  736. },
  737. {
  738. windowToExpand: NewWindow(&t2, nil),
  739. windowArgument: NewWindow(&t1, nil),
  740. expected: NewWindow(&t1, nil),
  741. },
  742. }
  743. for _, c := range cases {
  744. result := c.windowToExpand.Expand(c.windowArgument)
  745. if !result.Equal(c.expected) {
  746. t.Errorf("Expand %s with %s, expected %s but got %s", c.windowToExpand, c.windowArgument, c.expected, result)
  747. }
  748. }
  749. }
  750. // TODO
  751. // func TestWindow_Start(t *testing.T) {}
  752. // TODO
  753. // func TestWindow_String(t *testing.T) {}
  754. func TestWindow_GetPercentInWindow(t *testing.T) {
  755. dayStart := time.Date(2022, 12, 6, 0, 0, 0, 0, time.UTC)
  756. dayEnd := dayStart.Add(timeutil.Day)
  757. window := NewClosedWindow(dayStart, dayEnd)
  758. testcases := map[string]struct {
  759. window Window
  760. itemStart time.Time
  761. itemEnd time.Time
  762. expected float64
  763. }{
  764. "matching start/matching end": {
  765. window: window,
  766. itemStart: dayStart,
  767. itemEnd: dayEnd,
  768. expected: 1.0,
  769. },
  770. "matching start/contained end": {
  771. window: window,
  772. itemStart: dayStart,
  773. itemEnd: dayEnd.Add(-time.Hour * 6),
  774. expected: 1.0,
  775. },
  776. "contained start/matching end": {
  777. window: window,
  778. itemStart: dayStart.Add(time.Hour * 6),
  779. itemEnd: dayEnd,
  780. expected: 1.0,
  781. },
  782. "contained start/contained end": {
  783. window: window,
  784. itemStart: dayStart.Add(time.Hour * 6),
  785. itemEnd: dayEnd.Add(-time.Hour * 6),
  786. expected: 1.0,
  787. },
  788. "before start/contained end": {
  789. window: window,
  790. itemStart: dayStart.Add(-time.Hour * 12),
  791. itemEnd: dayEnd.Add(-time.Hour * 12),
  792. expected: 0.5,
  793. },
  794. "before start/before end": {
  795. window: window,
  796. itemStart: dayStart.Add(-time.Hour * 24),
  797. itemEnd: dayEnd.Add(-time.Hour * 24),
  798. expected: 0.0,
  799. },
  800. "contained start/after end": {
  801. window: window,
  802. itemStart: dayStart.Add(time.Hour * 12),
  803. itemEnd: dayEnd.Add(time.Hour * 12),
  804. expected: 0.5,
  805. },
  806. "after start/after end": {
  807. window: window,
  808. itemStart: dayStart.Add(time.Hour * 24),
  809. itemEnd: dayEnd.Add(time.Hour * 24),
  810. expected: 0.0,
  811. },
  812. "before start/after end": {
  813. window: window,
  814. itemStart: dayStart.Add(-time.Hour * 12),
  815. itemEnd: dayEnd.Add(time.Hour * 12),
  816. expected: 0.5,
  817. },
  818. }
  819. for name, tc := range testcases {
  820. t.Run(name, func(t *testing.T) {
  821. thatWindow := NewWindow(&tc.itemStart, &tc.itemEnd)
  822. if actual := tc.window.GetPercentInWindow(thatWindow); actual != tc.expected {
  823. t.Errorf("GetPercentInWindow() = %v, want %v", actual, tc.expected)
  824. }
  825. })
  826. }
  827. }
  828. func TestWindow_GetWindows(t *testing.T) {
  829. dayStart := time.Date(2022, 12, 6, 0, 0, 0, 0, time.UTC)
  830. dayEnd := dayStart.Add(timeutil.Day)
  831. loc, _ := time.LoadLocation("America/Vancouver")
  832. testCases := map[string]struct {
  833. start time.Time
  834. end time.Time
  835. windowSize time.Duration
  836. expected []Window
  837. expectedErr bool
  838. }{
  839. "mismatching tz": {
  840. start: dayStart,
  841. end: dayEnd.In(loc),
  842. windowSize: time.Hour,
  843. expected: nil,
  844. expectedErr: true,
  845. },
  846. "hour windows over 1 hours": {
  847. start: dayStart,
  848. end: dayStart.Add(time.Hour),
  849. windowSize: time.Hour,
  850. expected: []Window{
  851. NewClosedWindow(dayStart, dayStart.Add(time.Hour)),
  852. },
  853. expectedErr: false,
  854. },
  855. "hour windows over 3 hours": {
  856. start: dayStart,
  857. end: dayStart.Add(time.Hour * 3),
  858. windowSize: time.Hour,
  859. expected: []Window{
  860. NewClosedWindow(dayStart, dayStart.Add(time.Hour)),
  861. NewClosedWindow(dayStart.Add(time.Hour), dayStart.Add(time.Hour*2)),
  862. NewClosedWindow(dayStart.Add(time.Hour*2), dayStart.Add(time.Hour*3)),
  863. },
  864. expectedErr: false,
  865. },
  866. "hour windows off hour grid": {
  867. start: dayStart.Add(time.Minute),
  868. end: dayEnd.Add(time.Minute),
  869. windowSize: time.Hour,
  870. expected: nil,
  871. expectedErr: true,
  872. },
  873. "hour windows range not divisible by hour": {
  874. start: dayStart,
  875. end: dayStart.Add(time.Minute * 90),
  876. windowSize: time.Hour,
  877. expected: nil,
  878. expectedErr: true,
  879. },
  880. "day windows over 1 day": {
  881. start: dayStart,
  882. end: dayEnd,
  883. windowSize: timeutil.Day,
  884. expected: []Window{
  885. NewClosedWindow(dayStart, dayEnd),
  886. },
  887. expectedErr: false,
  888. },
  889. "day windows over 3 days": {
  890. start: dayStart,
  891. end: dayStart.Add(timeutil.Day * 3),
  892. windowSize: timeutil.Day,
  893. expected: []Window{
  894. NewClosedWindow(dayStart, dayStart.Add(timeutil.Day)),
  895. NewClosedWindow(dayStart.Add(timeutil.Day), dayStart.Add(timeutil.Day*2)),
  896. NewClosedWindow(dayStart.Add(timeutil.Day*2), dayStart.Add(timeutil.Day*3)),
  897. },
  898. expectedErr: false,
  899. },
  900. "day windows off day grid": {
  901. start: dayStart.Add(time.Hour),
  902. end: dayEnd.Add(time.Hour),
  903. windowSize: timeutil.Day,
  904. expected: nil,
  905. expectedErr: true,
  906. },
  907. "day windows range not divisible by day": {
  908. start: dayStart,
  909. end: dayEnd.Add(time.Hour),
  910. windowSize: timeutil.Day,
  911. expected: nil,
  912. expectedErr: true,
  913. },
  914. }
  915. for name, tc := range testCases {
  916. t.Run(name, func(t *testing.T) {
  917. actual, err := GetWindows(tc.start, tc.end, tc.windowSize)
  918. if (err != nil) != tc.expectedErr {
  919. t.Errorf("GetWindows() error = %v, expectedErr %v", err, tc.expectedErr)
  920. return
  921. }
  922. if len(tc.expected) != len(actual) {
  923. t.Errorf("GetWindows() []window has incorrect length expected: %d, actual: %d", len(tc.expected), len(actual))
  924. }
  925. for i, actualWindow := range actual {
  926. expectedWindow := tc.expected[i]
  927. if !actualWindow.Equal(expectedWindow) {
  928. t.Errorf("GetWindow() window at index %d were not equal expected: %s, actual %s", i, expectedWindow.String(), actualWindow)
  929. }
  930. }
  931. })
  932. }
  933. }
  934. func TestWindow_GetWindowsForQueryWindow(t *testing.T) {
  935. dayStart := time.Date(2022, 12, 6, 0, 0, 0, 0, time.UTC)
  936. dayEnd := dayStart.Add(timeutil.Day)
  937. loc, _ := time.LoadLocation("America/Vancouver")
  938. testCases := map[string]struct {
  939. start time.Time
  940. end time.Time
  941. windowSize time.Duration
  942. expected []Window
  943. expectedErr bool
  944. }{
  945. "mismatching tz": {
  946. start: dayStart,
  947. end: dayEnd.In(loc),
  948. windowSize: time.Hour,
  949. expected: nil,
  950. expectedErr: true,
  951. },
  952. "hour windows over 1 hours": {
  953. start: dayStart,
  954. end: dayStart.Add(time.Hour),
  955. windowSize: time.Hour,
  956. expected: []Window{
  957. NewClosedWindow(dayStart, dayStart.Add(time.Hour)),
  958. },
  959. expectedErr: false,
  960. },
  961. "hour windows over 3 hours": {
  962. start: dayStart,
  963. end: dayStart.Add(time.Hour * 3),
  964. windowSize: time.Hour,
  965. expected: []Window{
  966. NewClosedWindow(dayStart, dayStart.Add(time.Hour)),
  967. NewClosedWindow(dayStart.Add(time.Hour), dayStart.Add(time.Hour*2)),
  968. NewClosedWindow(dayStart.Add(time.Hour*2), dayStart.Add(time.Hour*3)),
  969. },
  970. expectedErr: false,
  971. },
  972. "hour windows off hour grid": {
  973. start: dayStart.Add(time.Minute),
  974. end: dayStart.Add(time.Minute * 61),
  975. windowSize: time.Hour,
  976. expected: []Window{
  977. NewClosedWindow(dayStart.Add(time.Minute), dayStart.Add(time.Minute*61)),
  978. },
  979. expectedErr: false,
  980. },
  981. "hour windows range not divisible by hour": {
  982. start: dayStart,
  983. end: dayStart.Add(time.Minute * 90),
  984. windowSize: time.Hour,
  985. expected: []Window{
  986. NewClosedWindow(dayStart, dayStart.Add(time.Hour)),
  987. NewClosedWindow(dayStart.Add(time.Hour), dayStart.Add(time.Minute*90)),
  988. },
  989. expectedErr: false,
  990. },
  991. "day windows over 1 day": {
  992. start: dayStart,
  993. end: dayEnd,
  994. windowSize: timeutil.Day,
  995. expected: []Window{
  996. NewClosedWindow(dayStart, dayEnd),
  997. },
  998. expectedErr: false,
  999. },
  1000. "day windows over 3 days": {
  1001. start: dayStart,
  1002. end: dayStart.Add(timeutil.Day * 3),
  1003. windowSize: timeutil.Day,
  1004. expected: []Window{
  1005. NewClosedWindow(dayStart, dayStart.Add(timeutil.Day)),
  1006. NewClosedWindow(dayStart.Add(timeutil.Day), dayStart.Add(timeutil.Day*2)),
  1007. NewClosedWindow(dayStart.Add(timeutil.Day*2), dayStart.Add(timeutil.Day*3)),
  1008. },
  1009. expectedErr: false,
  1010. },
  1011. "day windows off day grid": {
  1012. start: dayStart.Add(time.Hour),
  1013. end: dayEnd.Add(time.Hour),
  1014. windowSize: timeutil.Day,
  1015. expected: []Window{
  1016. NewClosedWindow(dayStart.Add(time.Hour), dayEnd.Add(time.Hour)),
  1017. },
  1018. expectedErr: false,
  1019. },
  1020. "day windows range not divisible by day": {
  1021. start: dayStart,
  1022. end: dayEnd.Add(time.Hour),
  1023. windowSize: timeutil.Day,
  1024. expected: []Window{
  1025. NewClosedWindow(dayStart, dayEnd),
  1026. NewClosedWindow(dayEnd, dayEnd.Add(time.Hour)),
  1027. },
  1028. expectedErr: false,
  1029. },
  1030. }
  1031. for name, tc := range testCases {
  1032. t.Run(name, func(t *testing.T) {
  1033. actual, err := GetWindowsForQueryWindow(tc.start, tc.end, tc.windowSize)
  1034. if (err != nil) != tc.expectedErr {
  1035. t.Errorf("GetWindowsForQueryWindow() error = %v, expectedErr %v", err, tc.expectedErr)
  1036. return
  1037. }
  1038. if len(tc.expected) != len(actual) {
  1039. t.Errorf("GetWindowsForQueryWindow() []window has incorrect length expected: %d, actual: %d", len(tc.expected), len(actual))
  1040. }
  1041. for i, actualWindow := range actual {
  1042. expectedWindow := tc.expected[i]
  1043. if !actualWindow.Equal(expectedWindow) {
  1044. t.Errorf("GetWindowsForQueryWindow() window at index %d were not equal expected: %s, actual %s", i, expectedWindow.String(), actualWindow)
  1045. }
  1046. }
  1047. })
  1048. }
  1049. }
  1050. func TestMarshalUnmarshal(t *testing.T) {
  1051. t1 := time.Date(2023, 03, 11, 01, 29, 15, 0, time.UTC)
  1052. t2 := t1.Add(8 * time.Minute)
  1053. cases := []struct {
  1054. w Window
  1055. }{
  1056. {
  1057. w: NewClosedWindow(t1, t2),
  1058. },
  1059. {
  1060. w: NewWindow(&t1, nil),
  1061. },
  1062. {
  1063. w: NewWindow(nil, &t2),
  1064. },
  1065. {
  1066. w: NewWindow(nil, nil),
  1067. },
  1068. }
  1069. for _, c := range cases {
  1070. name := c.w.String()
  1071. t.Run(name, func(t *testing.T) {
  1072. marshaled, err := json.Marshal(c.w)
  1073. if err != nil {
  1074. t.Fatalf("marshaling: %s", err)
  1075. }
  1076. var unmarshaledW Window
  1077. err = json.Unmarshal(marshaled, &unmarshaledW)
  1078. if err != nil {
  1079. t.Fatalf("unmarshaling: %s", err)
  1080. }
  1081. if diff := cmp.Diff(c.w, unmarshaledW); len(diff) > 0 {
  1082. t.Errorf(diff)
  1083. }
  1084. })
  1085. }
  1086. }