queryservice_helper_test.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. package inferencecost
  2. import (
  3. "testing"
  4. "time"
  5. "github.com/opencost/opencost/core/pkg/opencost"
  6. "github.com/opencost/opencost/core/pkg/util/httputil"
  7. )
  8. // makeQP is a small helper that converts a plain map into an httputil.QueryParams
  9. // so tests can call ParseInferenceCostRequest without spinning up HTTP.
  10. func makeQP(pairs map[string]string) httputil.QueryParams {
  11. raw := make(map[string][]string, len(pairs))
  12. for k, v := range pairs {
  13. raw[k] = []string{v}
  14. }
  15. return httputil.NewQueryParams(raw)
  16. }
  17. // validWindowStr returns a well-formed RFC3339 window string covering [yesterday, now].
  18. func validWindowStr() string {
  19. now := time.Now().UTC().Truncate(time.Hour)
  20. start := now.Add(-24 * time.Hour)
  21. return start.Format(time.RFC3339) + "," + now.Format(time.RFC3339)
  22. }
  23. // --- ParseInferenceCostRequest ---
  24. func TestParseInferenceCostRequest_MissingWindow(t *testing.T) {
  25. _, err := ParseInferenceCostRequest(makeQP(map[string]string{}))
  26. if err == nil {
  27. t.Fatal("expected error for missing window, got nil")
  28. }
  29. }
  30. func TestParseInferenceCostRequest_InvalidWindow(t *testing.T) {
  31. _, err := ParseInferenceCostRequest(makeQP(map[string]string{
  32. "window": "not-a-window",
  33. }))
  34. if err == nil {
  35. t.Fatal("expected error for invalid window, got nil")
  36. }
  37. }
  38. func TestParseInferenceCostRequest_DefaultBasis(t *testing.T) {
  39. req, err := ParseInferenceCostRequest(makeQP(map[string]string{
  40. "window": validWindowStr(),
  41. }))
  42. if err != nil {
  43. t.Fatalf("unexpected error: %v", err)
  44. }
  45. if req.CostBasis != CostBasisAllocation {
  46. t.Errorf("default CostBasis = %q, want %q", req.CostBasis, CostBasisAllocation)
  47. }
  48. }
  49. func TestParseInferenceCostRequest_UsageBasis(t *testing.T) {
  50. req, err := ParseInferenceCostRequest(makeQP(map[string]string{
  51. "window": validWindowStr(),
  52. "costBasis": "usage",
  53. }))
  54. if err != nil {
  55. t.Fatalf("unexpected error: %v", err)
  56. }
  57. if req.CostBasis != CostBasisUsage {
  58. t.Errorf("CostBasis = %q, want %q", req.CostBasis, CostBasisUsage)
  59. }
  60. }
  61. func TestParseInferenceCostRequest_InvalidCostBasis(t *testing.T) {
  62. _, err := ParseInferenceCostRequest(makeQP(map[string]string{
  63. "window": validWindowStr(),
  64. "costBasis": "bogus",
  65. }))
  66. if err == nil {
  67. t.Fatal("expected error for invalid costBasis, got nil")
  68. }
  69. }
  70. func TestParseInferenceCostRequest_ValidAggregation(t *testing.T) {
  71. req, err := ParseInferenceCostRequest(makeQP(map[string]string{
  72. "window": validWindowStr(),
  73. "aggregate": "model_name,namespace",
  74. }))
  75. if err != nil {
  76. t.Fatalf("unexpected error: %v", err)
  77. }
  78. if len(req.AggregateBy) != 2 {
  79. t.Errorf("AggregateBy len = %d, want 2", len(req.AggregateBy))
  80. }
  81. }
  82. func TestParseInferenceCostRequest_UnsupportedAggregateDimension(t *testing.T) {
  83. _, err := ParseInferenceCostRequest(makeQP(map[string]string{
  84. "window": validWindowStr(),
  85. "aggregate": "product",
  86. }))
  87. if err == nil {
  88. t.Fatal("expected error for unsupported aggregate dimension, got nil")
  89. }
  90. }
  91. func TestParseInferenceCostRequest_ValidFilter(t *testing.T) {
  92. req, err := ParseInferenceCostRequest(makeQP(map[string]string{
  93. "window": validWindowStr(),
  94. "filter": `namespace:"llm-prod"`,
  95. }))
  96. if err != nil {
  97. t.Fatalf("unexpected error: %v", err)
  98. }
  99. if len(req.Filter) != 1 {
  100. t.Errorf("Filter len = %d, want 1", len(req.Filter))
  101. }
  102. if req.Filter[0].property != "namespace" || req.Filter[0].value != "llm-prod" {
  103. t.Errorf("Filter[0] = %+v, want {namespace, llm-prod}", req.Filter[0])
  104. }
  105. }
  106. func TestParseInferenceCostRequest_InvalidFilterProperty(t *testing.T) {
  107. _, err := ParseInferenceCostRequest(makeQP(map[string]string{
  108. "window": validWindowStr(),
  109. "filter": `product:"team-a"`,
  110. }))
  111. if err == nil {
  112. t.Fatal("expected error for unsupported filter property, got nil")
  113. }
  114. }
  115. func TestParseInferenceCostRequest_AccumulateDay(t *testing.T) {
  116. req, err := ParseInferenceCostRequest(makeQP(map[string]string{
  117. "window": validWindowStr(),
  118. "accumulate": "day",
  119. }))
  120. if err != nil {
  121. t.Fatalf("unexpected error: %v", err)
  122. }
  123. if req.Accumulate != opencost.AccumulateOptionDay {
  124. t.Errorf("Accumulate = %q, want %q", req.Accumulate, opencost.AccumulateOptionDay)
  125. }
  126. if req.Step != 24*time.Hour {
  127. t.Errorf("Step = %s, want 24h", req.Step)
  128. }
  129. }
  130. // --- ParseInferenceCostTimeseriesRequest ---
  131. func TestParseInferenceCostTimeseriesRequest_MissingAccumulate(t *testing.T) {
  132. _, err := ParseInferenceCostTimeseriesRequest(makeQP(map[string]string{
  133. "window": validWindowStr(),
  134. }))
  135. if err == nil {
  136. t.Fatal("expected error when accumulate is missing for timeseries, got nil")
  137. }
  138. }
  139. func TestParseInferenceCostTimeseriesRequest_Valid(t *testing.T) {
  140. req, err := ParseInferenceCostTimeseriesRequest(makeQP(map[string]string{
  141. "window": validWindowStr(),
  142. "accumulate": "hour",
  143. }))
  144. if err != nil {
  145. t.Fatalf("unexpected error: %v", err)
  146. }
  147. if req.Accumulate != opencost.AccumulateOptionHour {
  148. t.Errorf("Accumulate = %q, want %q", req.Accumulate, opencost.AccumulateOptionHour)
  149. }
  150. if req.Step != time.Hour {
  151. t.Errorf("Step = %s, want 1h", req.Step)
  152. }
  153. }