retailpricingsource_test.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. package azure
  2. import (
  3. "encoding/json"
  4. "strings"
  5. "testing"
  6. "time"
  7. "github.com/opencost/opencost/core/pkg/model/pricingmodel"
  8. "github.com/opencost/opencost/core/pkg/model/shared"
  9. )
  10. func TestAzureRetailPricingSource_BuildInitialURL(t *testing.T) {
  11. cases := []struct {
  12. name string
  13. currencyCode string
  14. }{
  15. {"no currency", ""},
  16. {"USD", "USD"},
  17. {"EUR", "EUR"},
  18. }
  19. for _, tc := range cases {
  20. t.Run(tc.name, func(t *testing.T) {
  21. src := NewAzureRetailPricingSource(AzureRetailPricingSourceConfig{CurrencyCode: tc.currencyCode})
  22. u := src.buildInitialURL()
  23. if strings.Contains(u, " ") {
  24. t.Errorf("URL contains unencoded space: %s", u)
  25. }
  26. if strings.Contains(u, "'") {
  27. t.Errorf("URL contains unencoded single quote: %s", u)
  28. }
  29. if !strings.Contains(u, "$filter=") {
  30. t.Errorf("URL missing $filter param: %s", u)
  31. }
  32. if tc.currencyCode != "" {
  33. if !strings.Contains(u, "currencyCode="+tc.currencyCode) {
  34. t.Errorf("URL missing or malformed currencyCode param: %s", u)
  35. }
  36. // ensure no single quotes wrapped around the currency value
  37. if strings.Contains(u, "'"+tc.currencyCode+"'") {
  38. t.Errorf("currency code is wrapped in single quotes: %s", u)
  39. }
  40. }
  41. })
  42. }
  43. }
  44. func TestAzureRetailPricingSource_IncludeItem(t *testing.T) {
  45. src := NewAzureRetailPricingSource(AzureRetailPricingSourceConfig{})
  46. cases := []struct {
  47. name string
  48. item AzureRetailPricingAttributes
  49. want bool
  50. }{
  51. {
  52. name: "valid linux VM",
  53. item: AzureRetailPricingAttributes{ArmSkuName: "Standard_D4s_v3", ArmRegionName: "eastus", SkuName: "D4s v3", ProductName: "Virtual Machines D Series"},
  54. want: true,
  55. },
  56. {
  57. name: "spot SKU is included",
  58. item: AzureRetailPricingAttributes{ArmSkuName: "Standard_D4s_v3", ArmRegionName: "eastus", SkuName: "D4s v3 Spot", ProductName: "Virtual Machines D Series"},
  59. want: true,
  60. },
  61. {
  62. name: "missing ArmSkuName",
  63. item: AzureRetailPricingAttributes{ArmRegionName: "eastus", SkuName: "D4s v3", ProductName: "Virtual Machines D Series"},
  64. want: false,
  65. },
  66. {
  67. name: "missing ArmRegionName",
  68. item: AzureRetailPricingAttributes{ArmSkuName: "Standard_D4s_v3", SkuName: "D4s v3", ProductName: "Virtual Machines D Series"},
  69. want: false,
  70. },
  71. {
  72. name: "Windows product excluded",
  73. item: AzureRetailPricingAttributes{ArmSkuName: "Standard_D4s_v3", ArmRegionName: "eastus", SkuName: "D4s v3", ProductName: "Windows Virtual Machines"},
  74. want: false,
  75. },
  76. {
  77. name: "low priority excluded",
  78. item: AzureRetailPricingAttributes{ArmSkuName: "Standard_D4s_v3", ArmRegionName: "eastus", SkuName: "D4s v3 Low Priority", ProductName: "Virtual Machines D Series"},
  79. want: false,
  80. },
  81. {
  82. name: "cloud services excluded",
  83. item: AzureRetailPricingAttributes{ArmSkuName: "Standard_D4s_v3", ArmRegionName: "eastus", SkuName: "D4s v3", ProductName: "Cloud Services General"},
  84. want: false,
  85. },
  86. {
  87. name: "cloudservices excluded",
  88. item: AzureRetailPricingAttributes{ArmSkuName: "Standard_D4s_v3", ArmRegionName: "eastus", SkuName: "D4s v3", ProductName: "CloudServices General"},
  89. want: false,
  90. },
  91. }
  92. for _, tc := range cases {
  93. t.Run(tc.name, func(t *testing.T) {
  94. got := src.includeItem(tc.item)
  95. if got != tc.want {
  96. t.Errorf("includeItem() = %v, want %v", got, tc.want)
  97. }
  98. })
  99. }
  100. }
  101. func TestUsageTypeFromSku(t *testing.T) {
  102. cases := []struct {
  103. skuName string
  104. want shared.UsageType
  105. }{
  106. {"D4s v3", shared.UsageTypeOnDemand},
  107. {"D4s v3 Spot", shared.UsageTypeSpot},
  108. {"D4s v3 spot", shared.UsageTypeSpot},
  109. {"D4s V3 SPOT", shared.UsageTypeSpot},
  110. {"E8s v5 Spot Extra", shared.UsageTypeSpot},
  111. {"", shared.UsageTypeOnDemand},
  112. }
  113. for _, tc := range cases {
  114. t.Run(tc.skuName, func(t *testing.T) {
  115. got := usageTypeFromSku(tc.skuName)
  116. if got != tc.want {
  117. t.Errorf("usageTypeFromSku(%q) = %q, want %q", tc.skuName, got, tc.want)
  118. }
  119. })
  120. }
  121. }
  122. func absf(x float64) float64 {
  123. if x < 0 {
  124. return -x
  125. }
  126. return x
  127. }
  128. func TestAzureRetailPricingSource_ParsePage(t *testing.T) {
  129. items := []AzureRetailPricingAttributes{
  130. // included: standard on-demand VM
  131. {ArmSkuName: "Standard_D4s_v3", ArmRegionName: "eastus", SkuName: "D4s v3", ProductName: "Virtual Machines D Series", RetailPrice: 0.192},
  132. // included: spot VM
  133. {ArmSkuName: "Standard_D4s_v3", ArmRegionName: "eastus", SkuName: "D4s v3 Spot", ProductName: "Virtual Machines D Series", RetailPrice: 0.05},
  134. // excluded: Windows
  135. {ArmSkuName: "Standard_D4s_v3", ArmRegionName: "eastus", SkuName: "D4s v3", ProductName: "Windows Virtual Machines", RetailPrice: 0.3},
  136. // excluded: missing region
  137. {ArmSkuName: "Standard_D4s_v3", SkuName: "D4s v3", ProductName: "Virtual Machines D Series", RetailPrice: 0.192},
  138. }
  139. page := AzureRetailPricing{Items: items}
  140. body, _ := json.Marshal(page)
  141. src := NewAzureRetailPricingSource(AzureRetailPricingSourceConfig{})
  142. pms := pricingmodel.NewPricingModelSet(time.Now().UTC(), src.PricingSourceType(), src.PricingSourceKey())
  143. _, err := src.parsePage(strings.NewReader(string(body)), pms)
  144. if err != nil {
  145. t.Fatalf("parsePage error: %v", err)
  146. }
  147. if len(pms.NodePricing) != 2 {
  148. t.Errorf("expected 2 pricing entries, got %d", len(pms.NodePricing))
  149. }
  150. onDemandKey := pricingmodel.NodeKey{
  151. Provider: shared.ProviderAzure,
  152. Region: "eastus",
  153. NodeType: "Standard_D4s_v3",
  154. UsageType: shared.UsageTypeOnDemand,
  155. PricingType: pricingmodel.NodePricingTypeTotal,
  156. }
  157. if entry, ok := pms.NodePricing[onDemandKey]; !ok {
  158. t.Error("missing OnDemand entry")
  159. } else if absf(entry.HourlyRate-0.192) > 1e-5 {
  160. t.Errorf("OnDemand HourlyRate = %v, want ~0.192", entry.HourlyRate)
  161. }
  162. spotKey := pricingmodel.NodeKey{
  163. Provider: shared.ProviderAzure,
  164. Region: "eastus",
  165. NodeType: "Standard_D4s_v3",
  166. UsageType: shared.UsageTypeSpot,
  167. PricingType: pricingmodel.NodePricingTypeTotal,
  168. }
  169. if _, ok := pms.NodePricing[spotKey]; !ok {
  170. t.Error("missing Spot entry")
  171. }
  172. }