| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- package carbon
- import (
- "math"
- "testing"
- "time"
- "github.com/opencost/opencost/core/pkg/opencost"
- v1 "k8s.io/api/core/v1"
- )
- const (
- // Known-good row from carbonlookupdata.csv:
- // AWS,us-east-1,t4g.nano,Node,0.012788433076234564,4.84769853777516e-06
- awsT4gNanoUSEast1Coeff = 4.84769853777516e-06
- // AWS,average-region,,Node,0.186739186034359,7.278989705005508e-05
- awsAvgRegionNodeCoeff = 7.278989705005508e-05
- // AWS,us-east-1,,Network,0.001135,4.30243315e-7
- awsUSEast1NetworkCoeff = 4.30243315e-7
- )
- // floatEqual compares floats at a tolerance appropriate for the lookup table
- // values, which are stored with full float64 precision in the CSV.
- func floatEqual(a, b float64) bool {
- if a == b {
- return true
- }
- return math.Abs(a-b) <= 1e-18+1e-12*math.Max(math.Abs(a), math.Abs(b))
- }
- func nodeWithLabels(provider, providerID, region, instanceType string, minutes float64) *opencost.Node {
- start := time.Date(2026, time.April, 1, 0, 0, 0, 0, time.UTC)
- end := start.Add(time.Duration(minutes) * time.Minute)
- window := opencost.NewWindow(&start, &end)
- n := opencost.NewNode("node", "cluster", providerID, start, end, window)
- n.Properties.Provider = provider
- labels := opencost.AssetLabels{}
- if region != "" {
- labels[v1.LabelTopologyRegion] = region
- }
- if instanceType != "" {
- labels[v1.LabelInstanceTypeStable] = instanceType
- }
- n.Labels = labels
- return n
- }
- func diskWithLabels(provider, providerID, region string, minutes float64) *opencost.Disk {
- start := time.Date(2026, time.April, 1, 0, 0, 0, 0, time.UTC)
- end := start.Add(time.Duration(minutes) * time.Minute)
- window := opencost.NewWindow(&start, &end)
- d := opencost.NewDisk("disk", "cluster", providerID, start, end, window)
- d.Properties.Provider = provider
- if region != "" {
- d.Labels = opencost.AssetLabels{v1.LabelTopologyRegion: region}
- }
- return d
- }
- func networkWithLabels(provider, providerID, region string, minutes float64) *opencost.Network {
- start := time.Date(2026, time.April, 1, 0, 0, 0, 0, time.UTC)
- end := start.Add(time.Duration(minutes) * time.Minute)
- window := opencost.NewWindow(&start, &end)
- nw := opencost.NewNetwork("network", "cluster", providerID, start, end, window)
- nw.Properties.Provider = provider
- if region != "" {
- nw.Labels = opencost.AssetLabels{v1.LabelTopologyRegion: region}
- }
- return nw
- }
- func TestInferProviderFromProviderID(t *testing.T) {
- cases := []struct {
- name string
- id string
- want string
- }{
- {"empty", "", ""},
- {"aws standard", "aws:///us-east-1a/i-0abc123", opencost.AWSProvider},
- {"aws raw instance", "i-0abc123", opencost.AWSProvider},
- {"gce standard", "gce://my-project/us-central1-a/gke-node-1", opencost.GCPProvider},
- {"legacy gke prefix", "gke-node-1", opencost.GCPProvider},
- {"azure standard", "azure:///subscriptions/x/resourceGroups/y/providers/Microsoft.Compute/virtualMachines/z", opencost.AzureProvider},
- {"unknown prefix", "something-else", ""},
- {"whitespace and case", " AWS:///eu-west-1a/i-xyz ", opencost.AWSProvider},
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- if got := inferProviderFromProviderID(tc.id); got != tc.want {
- t.Fatalf("inferProviderFromProviderID(%q) = %q, want %q", tc.id, got, tc.want)
- }
- })
- }
- }
- func TestResolveProvider_PrefersCanonicalProperty(t *testing.T) {
- // ProviderID is a GCP-shaped string but the canonical property says AWS.
- // Canonical property wins.
- n := nodeWithLabels(opencost.AWSProvider, "gce://foo/bar/baz", "us-east-1", "t4g.nano", 60)
- if got := resolveProvider(n); got != opencost.AWSProvider {
- t.Fatalf("resolveProvider = %q, want %q", got, opencost.AWSProvider)
- }
- }
- func TestResolveProvider_FallsBackToProviderID(t *testing.T) {
- // No canonical Provider property — must fall back to parsing ProviderID.
- n := nodeWithLabels("", "gce://my-project/us-central1-a/gke-node-1", "us-central1", "e2-standard-2", 60)
- if got := resolveProvider(n); got != opencost.GCPProvider {
- t.Fatalf("resolveProvider = %q, want %q", got, opencost.GCPProvider)
- }
- }
- func TestLookupCarbonCoeff_Node_ExactMatch(t *testing.T) {
- n := nodeWithLabels(opencost.AWSProvider, "aws:///us-east-1a/i-1", "us-east-1", "t4g.nano", 60)
- if got := lookupCarbonCoeff(n); !floatEqual(got, awsT4gNanoUSEast1Coeff) {
- t.Fatalf("lookupCarbonCoeff = %g, want %g", got, awsT4gNanoUSEast1Coeff)
- }
- }
- func TestLookupCarbonCoeff_Node_FallsBackWhenRegionUnknown(t *testing.T) {
- // Region is garbage; instance type is fine. Should fall back to
- // (AWS, average-region, "") instead of returning zero.
- n := nodeWithLabels(opencost.AWSProvider, "aws:///xx/i-1", "not-a-real-region", "t4g.nano", 60)
- if got := lookupCarbonCoeff(n); !floatEqual(got, awsAvgRegionNodeCoeff) {
- t.Fatalf("lookupCarbonCoeff = %g, want %g (average-region fallback)", got, awsAvgRegionNodeCoeff)
- }
- }
- func TestLookupCarbonCoeff_Node_FallsBackWhenInstanceTypeUnknown(t *testing.T) {
- // Region is real; instance type is unknown. Previously returned 0 because
- // only the region was reset. Must now fall back to average-region.
- n := nodeWithLabels(opencost.AWSProvider, "aws:///us-east-1a/i-1", "us-east-1", "future-xxlarge", 60)
- if got := lookupCarbonCoeff(n); !floatEqual(got, awsAvgRegionNodeCoeff) {
- t.Fatalf("lookupCarbonCoeff = %g, want %g (average-region fallback)", got, awsAvgRegionNodeCoeff)
- }
- }
- func TestLookupCarbonCoeff_Node_FallsBackWhenBothUnknown(t *testing.T) {
- n := nodeWithLabels(opencost.AWSProvider, "aws:///xx/i-1", "not-a-real-region", "future-xxlarge", 60)
- if got := lookupCarbonCoeff(n); !floatEqual(got, awsAvgRegionNodeCoeff) {
- t.Fatalf("lookupCarbonCoeff = %g, want %g (average-region fallback)", got, awsAvgRegionNodeCoeff)
- }
- }
- func TestLookupCarbonCoeff_Node_ZeroForUnknownProvider(t *testing.T) {
- n := nodeWithLabels("", "some-unknown-id", "us-east-1", "t4g.nano", 60)
- if got := lookupCarbonCoeff(n); got != 0 {
- t.Fatalf("lookupCarbonCoeff = %g, want 0", got)
- }
- }
- func TestLookupCarbonCoeff_Disk_ExactMatch(t *testing.T) {
- // The CSV contains several disk rows per (provider, region), one per
- // disk type. They collide under a key of (provider, region), so we
- // check the lookup against whatever value the table actually holds.
- want, ok := carbonLookupDisk[carbonLookupKeyRegion{opencost.AWSProvider, "us-east-1"}]
- if !ok || want == 0 {
- t.Fatalf("expected AWS/us-east-1 disk coefficient to be loaded")
- }
- d := diskWithLabels(opencost.AWSProvider, "aws:///us-east-1a/vol-1", "us-east-1", 60)
- if got := lookupCarbonCoeff(d); !floatEqual(got, want) {
- t.Fatalf("lookupCarbonCoeff disk = %g, want %g", got, want)
- }
- }
- func TestLookupCarbonCoeff_Disk_FallsBackWhenRegionUnknown(t *testing.T) {
- d := diskWithLabels(opencost.AWSProvider, "aws:///xx/vol-1", "not-a-real-region", 60)
- want, ok := carbonLookupDisk[carbonLookupKeyRegion{opencost.AWSProvider, averageRegionKey}]
- if !ok {
- t.Fatalf("expected AWS average-region disk coefficient to be loaded")
- }
- if got := lookupCarbonCoeff(d); !floatEqual(got, want) {
- t.Fatalf("lookupCarbonCoeff disk fallback = %g, want %g", got, want)
- }
- }
- func TestLookupCarbonCoeff_Network_Populated(t *testing.T) {
- // Regression: Network rows were loaded but never consulted, so every
- // Network asset produced 0 emissions.
- nw := networkWithLabels(opencost.AWSProvider, "aws:///us-east-1a/net-1", "us-east-1", 60)
- if got := lookupCarbonCoeff(nw); !floatEqual(got, awsUSEast1NetworkCoeff) {
- t.Fatalf("lookupCarbonCoeff network = %g, want %g", got, awsUSEast1NetworkCoeff)
- }
- }
- func TestRelateCarbonAssets_MinutesToHours(t *testing.T) {
- // Coefficient is tonnes CO2e per hour, so 120 minutes should yield exactly
- // twice the coefficient.
- n := nodeWithLabels(opencost.AWSProvider, "aws:///us-east-1a/i-1", "us-east-1", "t4g.nano", 120)
- as := opencost.NewAssetSet(*n.Window.Start(), *n.Window.End(), n)
- rows, err := RelateCarbonAssets(as)
- if err != nil {
- t.Fatalf("RelateCarbonAssets: %v", err)
- }
- if len(rows) != 1 {
- t.Fatalf("got %d rows, want 1", len(rows))
- }
- var row CarbonRow
- for _, r := range rows {
- row = r
- }
- want := awsT4gNanoUSEast1Coeff * 2
- if !floatEqual(row.Co2e, want) {
- t.Fatalf("Co2e = %g, want %g", row.Co2e, want)
- }
- }
- func TestRelateCarbonAssets_ZeroForUnknownProvider(t *testing.T) {
- n := nodeWithLabels("", "totally-unknown", "us-east-1", "t4g.nano", 60)
- as := opencost.NewAssetSet(*n.Window.Start(), *n.Window.End(), n)
- rows, err := RelateCarbonAssets(as)
- if err != nil {
- t.Fatalf("RelateCarbonAssets: %v", err)
- }
- for _, r := range rows {
- if r.Co2e != 0 {
- t.Fatalf("Co2e = %g, want 0 for unknown provider", r.Co2e)
- }
- }
- }
- func TestLookupCarbonCoeff_NoPanicOnNilProperties(t *testing.T) {
- // A bare Node with nil Properties must not panic — older code would
- // dereference props.ProviderID in the log line after resolveProvider
- // returned "" for nil properties.
- start := time.Date(2026, time.April, 1, 0, 0, 0, 0, time.UTC)
- end := start.Add(60 * time.Minute)
- window := opencost.NewWindow(&start, &end)
- n := opencost.NewNode("node", "cluster", "", start, end, window)
- n.Properties = nil
- if got := lookupCarbonCoeff(n); got != 0 {
- t.Fatalf("lookupCarbonCoeff with nil properties = %g, want 0", got)
- }
- }
- func TestLookupTables_LoadedAtInit(t *testing.T) {
- if len(carbonLookupNode) == 0 {
- t.Error("carbonLookupNode is empty — init did not populate node lookups")
- }
- if len(carbonLookupDisk) == 0 {
- t.Error("carbonLookupDisk is empty — init did not populate disk lookups")
- }
- if len(carbonLookupNetwork) == 0 {
- t.Error("carbonLookupNetwork is empty — init did not populate network lookups")
- }
- }
|