| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- package pricing
- import (
- "testing"
- "time"
- "github.com/opencost/opencost/core/pkg/cloud"
- "github.com/opencost/opencost/core/pkg/unit"
- )
- func nodePricing(instanceType string, price float64) *NodePricing {
- return &NodePricing{
- Properties: NodePricingProperties{
- Provider: cloud.ProviderAWS,
- Region: "us-east-1",
- InstanceType: instanceType,
- },
- Prices: Prices{
- ResourceNode: {Unit: unit.Hour, Price: price},
- },
- }
- }
- func pvPricing(volumeType VolumeType, price float64) *PersistentVolumePricing {
- return &PersistentVolumePricing{
- Properties: PersistentVolumePricingProperties{
- Provider: cloud.ProviderAWS,
- Region: "us-east-1",
- VolumeType: volumeType,
- },
- Prices: Prices{
- ResourceStorage: {Unit: unit.GiBHour, Price: price},
- },
- }
- }
- // TestChecksumPriceSensitivity verifies that the checksum changes when only a
- // price value changes, even if all properties are identical.
- func TestChecksumPriceSensitivity(t *testing.T) {
- a := &PricingSet{NodePricing: []*NodePricing{nodePricing("m5.large", 0.096)}}
- b := &PricingSet{NodePricing: []*NodePricing{nodePricing("m5.large", 0.192)}}
- csA, err := a.Checksum()
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- csB, err := b.Checksum()
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if csA == csB {
- t.Errorf("expected differing checksums for differing prices, got %q for both", csA)
- }
- }
- // TestChecksumOrderStability verifies that the checksum is independent of the
- // ordering of pricing slices.
- func TestChecksumOrderStability(t *testing.T) {
- n1 := nodePricing("m5.large", 0.096)
- n2 := nodePricing("m5.xlarge", 0.192)
- n3 := nodePricing("m5.2xlarge", 0.384)
- forward := &PricingSet{NodePricing: []*NodePricing{n1, n2, n3}}
- reverse := &PricingSet{NodePricing: []*NodePricing{n3, n2, n1}}
- csForward, err := forward.Checksum()
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- csReverse, err := reverse.Checksum()
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if csForward != csReverse {
- t.Errorf("expected checksum to be order-independent, got %q vs %q", csForward, csReverse)
- }
- }
- // TestChecksumNilReceiver verifies that Checksum handles a nil receiver like
- // IsEmpty and Currencies do, rather than panicking.
- func TestChecksumNilReceiver(t *testing.T) {
- var ps *PricingSet
- if _, err := ps.Checksum(); err != nil {
- t.Errorf("unexpected error on nil receiver: %v", err)
- }
- }
- // TestIsEmptyAllKinds verifies that a set holding only Cluster/Network/Service
- // pricing is not reported empty.
- func TestIsEmptyAllKinds(t *testing.T) {
- if !(&PricingSet{}).IsEmpty() {
- t.Errorf("expected empty set to report empty")
- }
- cases := map[string]*PricingSet{
- "cluster": {ClusterPricing: []*ClusterPricing{{Properties: ClusterPricingProperties{Provider: cloud.ProviderAWS}}}},
- "network": {NetworkPricing: []*NetworkPricing{{Properties: NetworkPricingProperties{Provider: cloud.ProviderAWS}}}},
- "node": {NodePricing: []*NodePricing{nodePricing("m5.large", 0.096)}},
- "volume": {PersistentVolumePricing: []*PersistentVolumePricing{pvPricing(VolumeTypeGP3, 0.0001)}},
- "service": {ServicePricing: []*ServicePricing{{Properties: ServicePricingProperties{Provider: cloud.ProviderAWS}}}},
- }
- for name, ps := range cases {
- if ps.IsEmpty() {
- t.Errorf("set with only %s pricing should not be empty", name)
- }
- }
- }
- // fullPricingSet returns a PricingSet that exercises every kind plus every
- // reference-typed field (Labels maps, Prices maps, and *time.Time pointers) so
- // that Clone independence can be verified end to end.
- func fullPricingSet() *PricingSet {
- start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
- end := time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC)
- node := nodePricing("m5.large", 0.096)
- node.Properties.Labels = map[string]string{"team": "platform"}
- node.Properties.Start = &start
- node.Properties.End = &end
- pv := pvPricing(VolumeTypeGP3, 0.0001)
- pv.Properties.Labels = map[string]string{"env": "prod"}
- pv.Properties.Start = &start
- return &PricingSet{
- ClusterPricing: []*ClusterPricing{{
- Properties: ClusterPricingProperties{Provider: cloud.ProviderAWS, Start: &start},
- Prices: Prices{ResourceCluster: {Unit: unit.Hour, Price: 1.0}},
- }},
- NetworkPricing: []*NetworkPricing{{
- Properties: NetworkPricingProperties{Provider: cloud.ProviderAWS, End: &end},
- Prices: Prices{ResourceInternetEgress: {Unit: unit.GiB, Price: 0.09}},
- }},
- NodePricing: []*NodePricing{node},
- PersistentVolumePricing: []*PersistentVolumePricing{pv},
- ServicePricing: []*ServicePricing{{
- Properties: ServicePricingProperties{Provider: cloud.ProviderAWS, Region: "us-east-1", Start: &start},
- Prices: Prices{ResourceService: {Unit: unit.Hour, Price: 0.025}},
- }},
- }
- }
- // TestCloneEquality verifies that a clone is equal to the original by checksum.
- func TestCloneEquality(t *testing.T) {
- orig := fullPricingSet()
- clone := orig.Clone()
- csOrig, err := orig.Checksum()
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- csClone, err := clone.Checksum()
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if csOrig != csClone {
- t.Errorf("expected clone checksum %q to equal original %q", csClone, csOrig)
- }
- }
- // TestCloneIndependence verifies that mutating a clone's nested slices, maps,
- // and time pointers does not affect the original.
- func TestCloneIndependence(t *testing.T) {
- orig := fullPricingSet()
- csBefore, err := orig.Checksum()
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- clone := orig.Clone()
- // Mutate every reference-typed field reachable from the clone.
- clone.NodePricing[0].Properties.Labels["team"] = "mutated"
- clone.NodePricing[0].Prices[ResourceNode] = Price{Unit: unit.Hour, Price: 99}
- *clone.NodePricing[0].Properties.Start = time.Date(1999, 1, 1, 0, 0, 0, 0, time.UTC)
- clone.NodePricing[0].Properties.End = nil
- clone.PersistentVolumePricing[0].Properties.Labels["env"] = "mutated"
- clone.PersistentVolumePricing[0].Prices[ResourceStorage] = Price{Unit: unit.GiBHour, Price: 99}
- clone.ClusterPricing[0].Prices[ResourceCluster] = Price{Unit: unit.Hour, Price: 99}
- clone.NetworkPricing[0].Prices[ResourceInternetEgress] = Price{Unit: unit.GiB, Price: 99}
- clone.ServicePricing[0].Prices[ResourceService] = Price{Unit: unit.Hour, Price: 99}
- // Replace whole slices to confirm the slice headers are independent too.
- clone.NodePricing = append(clone.NodePricing, nodePricing("m5.xlarge", 0.192))
- csAfter, err := orig.Checksum()
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if csBefore != csAfter {
- t.Errorf("mutating clone changed original: checksum %q -> %q", csBefore, csAfter)
- }
- }
- // TestCloneNilReceiver verifies that Clone handles a nil receiver by returning
- // an empty, non-nil set rather than panicking.
- func TestCloneNilReceiver(t *testing.T) {
- var ps *PricingSet
- clone := ps.Clone()
- if clone == nil {
- t.Fatal("expected non-nil clone from nil receiver")
- }
- if !clone.IsEmpty() {
- t.Errorf("expected empty clone from nil receiver")
- }
- }
- // TestClonePreservesNilSlices verifies that Clone does not turn nil pricing
- // slices into empty ones, keeping serialization semantics stable.
- func TestClonePreservesNilSlices(t *testing.T) {
- clone := (&PricingSet{}).Clone()
- if clone.NodePricing != nil {
- t.Errorf("expected nil NodePricing slice, got %v", clone.NodePricing)
- }
- if clone.ClusterPricing != nil {
- t.Errorf("expected nil ClusterPricing slice, got %v", clone.ClusterPricing)
- }
- }
- // TestMockGetPricingSetAllKinds verifies that the mock's GetPricingSet exposes
- // the same kinds as its readers, not just node + persistent volume.
- func TestMockGetPricingSetAllKinds(t *testing.T) {
- mpm, err := NewMockPricingModule()
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- ps, err := mpm.GetPricingSet(t.Context())
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if len(ps.NodePricing) != len(mpm.NodePricing) {
- t.Errorf("expected %d node pricing, got %d", len(mpm.NodePricing), len(ps.NodePricing))
- }
- if len(ps.PersistentVolumePricing) != len(mpm.PersistentVolumePricing) {
- t.Errorf("expected %d volume pricing, got %d", len(mpm.PersistentVolumePricing), len(ps.PersistentVolumePricing))
- }
- if len(ps.ClusterPricing) != len(mpm.ClusterPricing) {
- t.Errorf("expected %d cluster pricing, got %d", len(mpm.ClusterPricing), len(ps.ClusterPricing))
- }
- if len(ps.NetworkPricing) != len(mpm.NetworkPricing) {
- t.Errorf("expected %d network pricing, got %d", len(mpm.NetworkPricing), len(ps.NetworkPricing))
- }
- if len(ps.ServicePricing) != len(mpm.ServicePricing) {
- t.Errorf("expected %d service pricing, got %d", len(mpm.ServicePricing), len(ps.ServicePricing))
- }
- }
|