| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
- package gcp
- import (
- "encoding/json"
- "strings"
- "testing"
- "time"
- "github.com/opencost/opencost/core/pkg/model/pricingmodel"
- "github.com/opencost/opencost/core/pkg/model/shared"
- )
- func TestGCPNormalizeFamily(t *testing.T) {
- cases := []struct{ in, want string }{
- {"N1Standard", "n1"},
- {"N2Standard", "n2"},
- {"N2DStandard", "n2d"},
- {"E2", "e2"},
- {"A2", "a2"},
- {"A3", "a3"},
- {"G2", "g2"},
- {"C2Standard", "c2"},
- {"C2DStandard", "c2d"},
- {"C3Standard", "c3"},
- {"C3DStandard", "c3d"},
- {"M1", "m1"},
- {"M2", "m2"},
- {"M3", "m3"},
- {"T2DStandard", "t2d"},
- {"T2AStandard", "t2a"},
- {"N4Standard", "n4"},
- {"H3Standard", "h3"},
- }
- for _, tc := range cases {
- t.Run(tc.in, func(t *testing.T) {
- got := gcpNormalizeFamily(tc.in)
- if got != tc.want {
- t.Errorf("gcpNormalizeFamily(%q) = %q, want %q", tc.in, got, tc.want)
- }
- })
- }
- }
- func TestGCPUsageType(t *testing.T) {
- cases := []struct {
- in string
- want shared.UsageType
- }{
- {"OnDemand", shared.UsageTypeOnDemand},
- {"Preemptible", shared.UsageTypeSpot},
- {"Commit1Yr", shared.UsageTypeEmpty},
- {"Commit3Yr", shared.UsageTypeEmpty},
- {"", shared.UsageTypeEmpty},
- {"unknown", shared.UsageTypeEmpty},
- }
- for _, tc := range cases {
- t.Run(tc.in, func(t *testing.T) {
- got := gcpUsageType(tc.in)
- if got != tc.want {
- t.Errorf("gcpUsageType(%q) = %q, want %q", tc.in, got, tc.want)
- }
- })
- }
- }
- func TestGCPExtractHourlyRate(t *testing.T) {
- cases := []struct {
- name string
- sku *GCPPricing
- want float64
- wantOK bool
- }{
- {
- name: "nanos only",
- sku: skuWithRate("0", 48000000),
- want: 0.048,
- wantOK: true,
- },
- {
- name: "units and nanos",
- sku: skuWithRate("1", 500000000),
- want: 1.5,
- wantOK: true,
- },
- {
- name: "whole units no nanos",
- sku: skuWithRate("2", 0),
- want: 2.0,
- wantOK: true,
- },
- {
- name: "no pricing info",
- sku: &GCPPricing{PricingInfo: []*PricingInfo{}},
- want: 0,
- wantOK: false,
- },
- {
- name: "no tiered rates",
- sku: skuWithRates([]*TieredRates{}),
- want: 0,
- wantOK: false,
- },
- {
- name: "nil pricing info",
- sku: &GCPPricing{},
- want: 0,
- wantOK: false,
- },
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- got, ok := gcpExtractHourlyRate(tc.sku)
- if ok != tc.wantOK {
- t.Errorf("ok = %v, want %v", ok, tc.wantOK)
- }
- if ok && abs(got-tc.want) > 1e-9 {
- t.Errorf("rate = %v, want %v", got, tc.want)
- }
- })
- }
- }
- func TestGCPBillingPricingSource_ParsePage(t *testing.T) {
- page := gcpBillingPage{
- NextPageToken: "token-xyz",
- SKUs: []*GCPPricing{
- // N1 CPU OnDemand — us-central1
- {
- Category: &GCPResourceInfo{ResourceFamily: "Compute", ResourceGroup: "N1Standard", UsageType: "OnDemand"},
- ServiceRegions: []string{"us-central1"},
- Description: "N1 Predefined Instance Core running in Americas",
- PricingInfo: []*PricingInfo{pricingInfo("0", 31611000)},
- },
- // N1 RAM OnDemand — us-central1
- {
- Category: &GCPResourceInfo{ResourceFamily: "Compute", ResourceGroup: "N1Standard", UsageType: "OnDemand"},
- ServiceRegions: []string{"us-central1"},
- Description: "N1 Predefined Instance RAM running in Americas",
- PricingInfo: []*PricingInfo{pricingInfo("0", 4237000)},
- },
- // T4 GPU OnDemand — us-central1
- {
- Category: &GCPResourceInfo{ResourceFamily: "Compute", ResourceGroup: "GPU", UsageType: "OnDemand"},
- ServiceRegions: []string{"us-central1"},
- Description: "NVIDIA Tesla T4 running in Americas",
- PricingInfo: []*PricingInfo{pricingInfo("0", 400000000)},
- },
- // N1 CPU Commit1Yr — should be skipped
- {
- Category: &GCPResourceInfo{ResourceFamily: "Compute", ResourceGroup: "N1Standard", UsageType: "Commit1Yr"},
- ServiceRegions: []string{"us-central1"},
- Description: "Commitment v1: N1 in Americas for 1 year",
- PricingInfo: []*PricingInfo{pricingInfo("0", 20000000)},
- },
- // Non-Compute resourceFamily — should be skipped
- {
- Category: &GCPResourceInfo{ResourceFamily: "Storage", ResourceGroup: "SSD", UsageType: "OnDemand"},
- ServiceRegions: []string{"us-central1"},
- Description: "SSD backed Local Storage",
- PricingInfo: []*PricingInfo{pricingInfo("0", 17000000)},
- },
- // GPU SKU with zero rate — should be skipped (reserved)
- {
- Category: &GCPResourceInfo{ResourceFamily: "Compute", ResourceGroup: "GPU", UsageType: "OnDemand"},
- ServiceRegions: []string{"us-central1"},
- Description: "NVIDIA Tesla V100 running in Americas",
- PricingInfo: []*PricingInfo{pricingInfo("0", 0)},
- },
- },
- }
- body, _ := json.Marshal(page)
- src, err := NewGCPBillingPricingSource(GCPBillingPricingSourceConfig{APIKey: "test-key"})
- if err != nil {
- t.Fatalf("NewGCPBillingPricingSource error: %v", err)
- }
- pms := pricingmodel.NewPricingModelSet(time.Now().UTC(), src.PricingSourceType(), src.PricingSourceKey())
- nextToken, err := src.parsePage(strings.NewReader(string(body)), pms)
- if err != nil {
- t.Fatalf("parsePage error: %v", err)
- }
- if nextToken != "token-xyz" {
- t.Errorf("nextToken = %q, want %q", nextToken, "token-xyz")
- }
- if len(pms.NodePricing) != 3 {
- t.Errorf("expected 3 pricing entries (CPU, RAM, GPU), got %d", len(pms.NodePricing))
- }
- cpuKey := pricingmodel.NodeKey{
- Provider: shared.ProviderGCP,
- Region: "us-central1",
- Family: "n1",
- UsageType: shared.UsageTypeOnDemand,
- PricingType: pricingmodel.NodePricingTypeCPUCore,
- }
- if _, ok := pms.NodePricing[cpuKey]; !ok {
- t.Error("missing CPU entry for n1/us-central1")
- }
- ramKey := pricingmodel.NodeKey{
- Provider: shared.ProviderGCP,
- Region: "us-central1",
- Family: "n1",
- UsageType: shared.UsageTypeOnDemand,
- PricingType: pricingmodel.NodePricingTypeRamGB,
- }
- if _, ok := pms.NodePricing[ramKey]; !ok {
- t.Error("missing RAM entry for n1/us-central1")
- }
- gpuKey := pricingmodel.NodeKey{
- Provider: shared.ProviderGCP,
- Region: "us-central1",
- UsageType: shared.UsageTypeOnDemand,
- DeviceType: "nvidia-tesla-t4",
- PricingType: pricingmodel.NodePricingTypeDevice,
- }
- if entry, ok := pms.NodePricing[gpuKey]; !ok {
- t.Error("missing GPU entry for T4/us-central1")
- } else if abs(entry.HourlyRate-0.4) > 1e-9 {
- t.Errorf("GPU HourlyRate = %v, want 0.4", entry.HourlyRate)
- }
- }
- func TestGCPBillingPricingSource_ParsePage_Preemptible(t *testing.T) {
- page := gcpBillingPage{
- SKUs: []*GCPPricing{
- {
- Category: &GCPResourceInfo{ResourceFamily: "Compute", ResourceGroup: "N1Standard", UsageType: "Preemptible"},
- ServiceRegions: []string{"us-east1"},
- Description: "Preemptible N1 Predefined Instance Core",
- PricingInfo: []*PricingInfo{pricingInfo("0", 6655000)},
- },
- },
- }
- body, _ := json.Marshal(page)
- src, err := NewGCPBillingPricingSource(GCPBillingPricingSourceConfig{APIKey: "test-key"})
- if err != nil {
- t.Fatalf("NewGCPBillingPricingSource error: %v", err)
- }
- pms := pricingmodel.NewPricingModelSet(time.Now().UTC(), src.PricingSourceType(), src.PricingSourceKey())
- if _, err := src.parsePage(strings.NewReader(string(body)), pms); err != nil {
- t.Fatalf("parsePage error: %v", err)
- }
- key := pricingmodel.NodeKey{
- Provider: shared.ProviderGCP,
- Region: "us-east1",
- Family: "n1",
- UsageType: shared.UsageTypeSpot,
- PricingType: pricingmodel.NodePricingTypeCPUCore,
- }
- if _, ok := pms.NodePricing[key]; !ok {
- t.Error("missing Preemptible CPU entry")
- }
- }
- // --- helpers ---
- func skuWithRate(units string, nanos float64) *GCPPricing {
- return skuWithRates([]*TieredRates{
- {UnitPrice: &UnitPriceInfo{Units: units, Nanos: nanos}},
- })
- }
- func skuWithRates(rates []*TieredRates) *GCPPricing {
- return &GCPPricing{
- PricingInfo: []*PricingInfo{
- {PricingExpression: &PricingExpression{TieredRates: rates}},
- },
- }
- }
- func pricingInfo(units string, nanos float64) *PricingInfo {
- return &PricingInfo{
- PricingExpression: &PricingExpression{
- TieredRates: []*TieredRates{
- {UnitPrice: &UnitPriceInfo{Units: units, Nanos: nanos}},
- },
- },
- }
- }
- func abs(x float64) float64 {
- if x < 0 {
- return -x
- }
- return x
- }
|