Просмотр исходного кода

Merge pull request #1778 from saweber/saweber/asset-accumulate

change asset accumulate to accept AccumulationOption
Sean Holcomb 3 лет назад
Родитель
Сommit
a7263496eb

+ 2 - 3
pkg/kubecost/allocation_test.go

@@ -1893,7 +1893,7 @@ func TestAllocationSetRange_AccumulateBy_Hour(t *testing.T) {
 	currentHourAS.Set(NewMockUnitAllocation("", currentHour, time.Hour, nil))
 
 	asr := NewAllocationSetRange(ago4hAS, ago3hAS, ago2hAS, ago1hAS, currentHourAS)
-	asr, err := asr.Accumulate(AccumulateOptionNone)
+	asr, err := asr.Accumulate(AccumulateOptionHour)
 	if err != nil {
 		t.Fatalf("unexpected error calling accumulateBy: %s", err)
 	}
@@ -2048,8 +2048,7 @@ func TestAllocationSetRange_AccumulateBy_Month(t *testing.T) {
 
 	nextAS := NewAllocationSet(nextMonth1stDay, nextMonth2ndDay)
 	nextAS.Set(NewMockUnitAllocation("", nextMonth1stDay, day, nil))
-	// check there are two allocation sets
-	// check the windows are one month or less
+
 	asr := NewAllocationSetRange(prev1AS, prev2AS, prev3AS, nextAS)
 	asr, err := asr.Accumulate(AccumulateOptionMonth)
 	if err != nil {

+ 170 - 2
pkg/kubecost/asset.go

@@ -3256,9 +3256,166 @@ func NewAssetSetRange(assets ...*AssetSet) *AssetSetRange {
 	}
 }
 
+func (asr *AssetSetRange) Accumulate(accumulateBy AccumulateOption) (*AssetSetRange, error) {
+	switch accumulateBy {
+	case AccumulateOptionNone:
+		return asr.accumulateByNone()
+	case AccumulateOptionAll:
+		return asr.accumulateByAll()
+	case AccumulateOptionHour:
+		return asr.accumulateByHour()
+	case AccumulateOptionDay:
+		return asr.accumulateByDay()
+	case AccumulateOptionWeek:
+		return asr.accumulateByWeek()
+	case AccumulateOptionMonth:
+		return asr.accumulateByMonth()
+	default:
+		// ideally, this should never happen
+		return nil, fmt.Errorf("unexpected error, invalid accumulateByType: %s", accumulateBy)
+	}
+}
+
+func (asr *AssetSetRange) accumulateByNone() (*AssetSetRange, error) {
+	return asr.clone(), nil
+}
+
+func (asr *AssetSetRange) accumulateByAll() (*AssetSetRange, error) {
+	var err error
+	var as *AssetSet
+	as, err = asr.newAccumulation()
+	if err != nil {
+		return nil, fmt.Errorf("error accumulating all:%s", err)
+	}
+
+	accumulated := NewAssetSetRange(as)
+	return accumulated, nil
+}
+
+func (asr *AssetSetRange) accumulateByHour() (*AssetSetRange, error) {
+	// ensure that the asset sets have a 1-hour window, if a set exists
+	if len(asr.Assets) > 0 && asr.Assets[0].Window.Duration() != time.Hour {
+		return nil, fmt.Errorf("window duration must equal 1 hour; got:%s", asr.Assets[0].Window.Duration())
+	}
+
+	return asr.clone(), nil
+}
+
+func (asr *AssetSetRange) accumulateByDay() (*AssetSetRange, error) {
+	// if the asset set window is 1-day, just return the existing asset set range
+	if len(asr.Assets) > 0 && asr.Assets[0].Window.Duration() == time.Hour*24 {
+		return asr, nil
+	}
+
+	var toAccumulate *AssetSetRange
+	result := NewAssetSetRange()
+	for i, as := range asr.Assets {
+
+		if as.Window.Duration() != time.Hour {
+			return nil, fmt.Errorf("window duration must equal 1 hour; got:%s", as.Window.Duration())
+		}
+
+		hour := as.Window.Start().Hour()
+
+		if toAccumulate == nil {
+			toAccumulate = NewAssetSetRange()
+			as = as.Clone()
+		}
+
+		toAccumulate.Append(as)
+		asAccumulated, err := toAccumulate.accumulate()
+		if err != nil {
+			return nil, fmt.Errorf("error accumulating result: %s", err)
+		}
+		toAccumulate = NewAssetSetRange(asAccumulated)
+
+		if hour == 23 || i == len(asr.Assets)-1 {
+			if length := len(toAccumulate.Assets); length != 1 {
+				return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
+			}
+			result.Append(toAccumulate.Assets[0])
+			toAccumulate = nil
+		}
+	}
+	return result, nil
+}
+
+func (asr *AssetSetRange) accumulateByMonth() (*AssetSetRange, error) {
+	var toAccumulate *AssetSetRange
+	result := NewAssetSetRange()
+	for i, as := range asr.Assets {
+		if as.Window.Duration() != time.Hour*24 {
+			return nil, fmt.Errorf("window duration must equal 24 hours; got:%s", as.Window.Duration())
+		}
+
+		_, month, _ := as.Window.Start().Date()
+		_, nextDayMonth, _ := as.Window.Start().Add(time.Hour * 24).Date()
+
+		if toAccumulate == nil {
+			toAccumulate = NewAssetSetRange()
+			as = as.Clone()
+		}
+
+		toAccumulate.Append(as)
+		asAccumulated, err := toAccumulate.accumulate()
+		if err != nil {
+			return nil, fmt.Errorf("error accumulating result: %s", err)
+		}
+		toAccumulate = NewAssetSetRange(asAccumulated)
+
+		// either the month has ended, or there are no more asset sets
+		if month != nextDayMonth || i == len(asr.Assets)-1 {
+			if length := len(toAccumulate.Assets); length != 1 {
+				return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
+			}
+			result.Append(toAccumulate.Assets[0])
+			toAccumulate = nil
+		}
+	}
+	return result, nil
+}
+
+func (asr *AssetSetRange) accumulateByWeek() (*AssetSetRange, error) {
+	var toAccumulate *AssetSetRange
+	result := NewAssetSetRange()
+	for i, as := range asr.Assets {
+		if as.Window.Duration() != time.Hour*24 {
+			return nil, fmt.Errorf("window duration must equal 24 hours; got:%s", as.Window.Duration())
+		}
+
+		dayOfWeek := as.Window.Start().Weekday()
+
+		if toAccumulate == nil {
+			toAccumulate = NewAssetSetRange()
+			as = as.Clone()
+		}
+
+		toAccumulate.Append(as)
+		asAccumulated, err := toAccumulate.accumulate()
+		if err != nil {
+			return nil, fmt.Errorf("error accumulating result: %s", err)
+		}
+		toAccumulate = NewAssetSetRange(asAccumulated)
+
+		// current assumption is the week always ends on Saturday, or there are no more asset sets
+		if dayOfWeek == time.Saturday || i == len(asr.Assets)-1 {
+			if length := len(toAccumulate.Assets); length != 1 {
+				return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
+			}
+			result.Append(toAccumulate.Assets[0])
+			toAccumulate = nil
+		}
+	}
+	return result, nil
+}
+
+func (asr *AssetSetRange) AccumulateToAssetSet() (*AssetSet, error) {
+	return asr.accumulate()
+}
+
 // Accumulate sums each AssetSet in the given range, returning a single cumulative
 // AssetSet for the entire range.
-func (asr *AssetSetRange) Accumulate() (*AssetSet, error) {
+func (asr *AssetSetRange) accumulate() (*AssetSet, error) {
 	var assetSet *AssetSet
 	var err error
 
@@ -3274,7 +3431,7 @@ func (asr *AssetSetRange) Accumulate() (*AssetSet, error) {
 
 // NewAccumulation clones the first available AssetSet to use as the data structure to
 // accumulate the remaining data. This leaves the original AssetSetRange intact.
-func (asr *AssetSetRange) NewAccumulation() (*AssetSet, error) {
+func (asr *AssetSetRange) newAccumulation() (*AssetSet, error) {
 	var assetSet *AssetSet
 	var err error
 
@@ -3618,6 +3775,17 @@ func (asr *AssetSetRange) TotalCost() float64 {
 	return tc
 }
 
+func (asr *AssetSetRange) clone() *AssetSetRange {
+	asrClone := NewAssetSetRange()
+	asrClone.FromStore = asr.FromStore
+	for _, as := range asr.Assets {
+		asClone := as.Clone()
+		asrClone.Append(asClone)
+	}
+
+	return asrClone
+}
+
 // This is a helper type. The Asset API returns a json which cannot be natively
 // unmarshaled into any Asset struct. Therefore, this struct IN COMBINATION WITH
 // DESERIALIZATION LOGIC DEFINED IN asset_json.go can unmarshal a json directly

+ 237 - 31
pkg/kubecost/asset_test.go

@@ -673,7 +673,7 @@ func TestAssetSet_AggregateBy(t *testing.T) {
 	// 1  Single-aggregation
 
 	// 1a []AssetProperty=[Cluster]
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	err = as.AggregateBy([]string{string(AssetClusterProp)}, nil)
 	if err != nil {
 		t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
@@ -685,7 +685,7 @@ func TestAssetSet_AggregateBy(t *testing.T) {
 	}, nil)
 
 	// 1b []AssetProperty=[Type]
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	err = as.AggregateBy([]string{string(AssetTypeProp)}, nil)
 	if err != nil {
 		t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
@@ -697,7 +697,7 @@ func TestAssetSet_AggregateBy(t *testing.T) {
 	}, nil)
 
 	// 1c []AssetProperty=[Nil]
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	err = as.AggregateBy([]string{}, nil)
 	if err != nil {
 		t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
@@ -707,7 +707,7 @@ func TestAssetSet_AggregateBy(t *testing.T) {
 	}, nil)
 
 	// 1d []AssetProperty=nil
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	err = as.AggregateBy(nil, nil)
 	if err != nil {
 		t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
@@ -727,7 +727,7 @@ func TestAssetSet_AggregateBy(t *testing.T) {
 	}, nil)
 
 	// 1e aggregateBy []string=["label:test"]
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	err = as.AggregateBy([]string{"label:test"}, nil)
 	if err != nil {
 		t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
@@ -740,7 +740,7 @@ func TestAssetSet_AggregateBy(t *testing.T) {
 	// 2  Multi-aggregation
 
 	// 2a []AssetProperty=[Cluster,Type]
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	err = as.AggregateBy([]string{string(AssetClusterProp), string(AssetTypeProp)}, nil)
 	if err != nil {
 		t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
@@ -758,7 +758,7 @@ func TestAssetSet_AggregateBy(t *testing.T) {
 	// 3  Share resources
 
 	// 3a Shared hourly cost > 0.0
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	err = as.AggregateBy([]string{string(AssetTypeProp)}, &AssetAggregationOptions{
 		SharedHourlyCosts: map[string]float64{"shared1": 0.5},
 	})
@@ -784,7 +784,7 @@ func TestAssetSet_FindMatch(t *testing.T) {
 	var err error
 
 	// Assert success of a simple match of Type and ProviderID
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	query = NewNode("", "", "gcp-node3", s, e, w)
 	match, err = as.FindMatch(query, []string{string(AssetTypeProp), string(AssetProviderIDProp)}, nil)
 	if err != nil {
@@ -792,7 +792,7 @@ func TestAssetSet_FindMatch(t *testing.T) {
 	}
 
 	// Assert error of a simple non-match of Type and ProviderID
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	query = NewNode("", "", "aws-node3", s, e, w)
 	match, err = as.FindMatch(query, []string{string(AssetTypeProp), string(AssetProviderIDProp)}, nil)
 	if err == nil {
@@ -800,7 +800,7 @@ func TestAssetSet_FindMatch(t *testing.T) {
 	}
 
 	// Assert error of matching ProviderID, but not Type
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	query = NewCloud(ComputeCategory, "gcp-node3", s, e, w)
 	match, err = as.FindMatch(query, []string{string(AssetTypeProp), string(AssetProviderIDProp)}, nil)
 	if err == nil {
@@ -854,7 +854,7 @@ func TestAssetSet_ReconciliationMatchMap(t *testing.T) {
 	endYesterday := time.Now().UTC().Truncate(day)
 	startYesterday := endYesterday.Add(-day)
 
-	as := GenerateMockAssetSet(startYesterday)
+	as := GenerateMockAssetSet(startYesterday, day)
 	matchMap := as.ReconciliationMatchMap()
 
 	// Determine the number of assets by provider ID
@@ -876,7 +876,7 @@ func TestAssetSet_ReconciliationMatchMap(t *testing.T) {
 	}
 }
 
-func TestAssetSetRange_Accumulate(t *testing.T) {
+func TestAssetSetRange_AccumulateToAssetSet(t *testing.T) {
 	endYesterday := time.Now().UTC().Truncate(day)
 	startYesterday := endYesterday.Add(-day)
 
@@ -891,12 +891,12 @@ func TestAssetSetRange_Accumulate(t *testing.T) {
 	var err error
 
 	asr = NewAssetSetRange(
-		GenerateMockAssetSet(startD0),
-		GenerateMockAssetSet(startD1),
-		GenerateMockAssetSet(startD2),
+		GenerateMockAssetSet(startD0, day),
+		GenerateMockAssetSet(startD1, day),
+		GenerateMockAssetSet(startD2, day),
 	)
 	err = asr.AggregateBy(nil, nil)
-	as, err = asr.Accumulate()
+	as, err = asr.AccumulateToAssetSet()
 	if err != nil {
 		t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
 	}
@@ -915,12 +915,12 @@ func TestAssetSetRange_Accumulate(t *testing.T) {
 	}, nil)
 
 	asr = NewAssetSetRange(
-		GenerateMockAssetSet(startD0),
-		GenerateMockAssetSet(startD1),
-		GenerateMockAssetSet(startD2),
+		GenerateMockAssetSet(startD0, day),
+		GenerateMockAssetSet(startD1, day),
+		GenerateMockAssetSet(startD2, day),
 	)
 	err = asr.AggregateBy([]string{}, nil)
-	as, err = asr.Accumulate()
+	as, err = asr.AccumulateToAssetSet()
 	if err != nil {
 		t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
 	}
@@ -929,15 +929,15 @@ func TestAssetSetRange_Accumulate(t *testing.T) {
 	}, nil)
 
 	asr = NewAssetSetRange(
-		GenerateMockAssetSet(startD0),
-		GenerateMockAssetSet(startD1),
-		GenerateMockAssetSet(startD2),
+		GenerateMockAssetSet(startD0, day),
+		GenerateMockAssetSet(startD1, day),
+		GenerateMockAssetSet(startD2, day),
 	)
 	err = asr.AggregateBy([]string{string(AssetTypeProp)}, nil)
 	if err != nil {
 		t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
 	}
-	as, err = asr.Accumulate()
+	as, err = asr.AccumulateToAssetSet()
 	if err != nil {
 		t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
 	}
@@ -948,15 +948,15 @@ func TestAssetSetRange_Accumulate(t *testing.T) {
 	}, nil)
 
 	asr = NewAssetSetRange(
-		GenerateMockAssetSet(startD0),
-		GenerateMockAssetSet(startD1),
-		GenerateMockAssetSet(startD2),
+		GenerateMockAssetSet(startD0, day),
+		GenerateMockAssetSet(startD1, day),
+		GenerateMockAssetSet(startD2, day),
 	)
 	err = asr.AggregateBy([]string{string(AssetClusterProp)}, nil)
 	if err != nil {
 		t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
 	}
-	as, err = asr.Accumulate()
+	as, err = asr.AccumulateToAssetSet()
 	if err != nil {
 		t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
 	}
@@ -970,12 +970,12 @@ func TestAssetSetRange_Accumulate(t *testing.T) {
 	// is empty (this was previously an issue)
 	asr = NewAssetSetRange(
 		NewAssetSet(startD0, startD1),
-		GenerateMockAssetSet(startD1),
-		GenerateMockAssetSet(startD2),
+		GenerateMockAssetSet(startD1, day),
+		GenerateMockAssetSet(startD2, day),
 	)
 
 	err = asr.AggregateBy([]string{string(AssetTypeProp)}, nil)
-	as, err = asr.Accumulate()
+	as, err = asr.AccumulateToAssetSet()
 	if err != nil {
 		t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
 	}
@@ -1479,3 +1479,209 @@ func TestAssetSetRange_MarshalJSON(t *testing.T) {
 		// asset don't unmarshal back from json
 	}
 }
+
+func TestAssetSetRange_AccumulateBy_None(t *testing.T) {
+	ago4d := time.Now().UTC().Truncate(day).Add(-4 * day)
+	ago3d := time.Now().UTC().Truncate(day).Add(-3 * day)
+	ago2d := time.Now().UTC().Truncate(day).Add(-2 * day)
+	yesterday := time.Now().UTC().Truncate(day).Add(-day)
+	today := time.Now().UTC().Truncate(day)
+
+	ago4dAS := GenerateMockAssetSet(ago4d, day)
+	ago3dAS := GenerateMockAssetSet(ago3d, day)
+	ago2dAS := GenerateMockAssetSet(ago2d, day)
+	yesterdayAS := GenerateMockAssetSet(yesterday, day)
+	todayAS := GenerateMockAssetSet(today, day)
+
+	asr := NewAssetSetRange(ago4dAS, ago3dAS, ago2dAS, yesterdayAS, todayAS)
+	asr, err := asr.Accumulate(AccumulateOptionNone)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.Assets) != 5 {
+		t.Fatalf("expected 5 asset sets, got:%d", len(asr.Assets))
+	}
+}
+
+func TestAssetSetRange_AccumulateBy_All(t *testing.T) {
+	ago4d := time.Now().UTC().Truncate(day).Add(-4 * day)
+	ago3d := time.Now().UTC().Truncate(day).Add(-3 * day)
+	ago2d := time.Now().UTC().Truncate(day).Add(-2 * day)
+	yesterday := time.Now().UTC().Truncate(day).Add(-day)
+	today := time.Now().UTC().Truncate(day)
+
+	ago4dAS := GenerateMockAssetSet(ago4d, day)
+	ago3dAS := GenerateMockAssetSet(ago3d, day)
+	ago2dAS := GenerateMockAssetSet(ago2d, day)
+	yesterdayAS := GenerateMockAssetSet(yesterday, day)
+	todayAS := GenerateMockAssetSet(today, day)
+
+	asr := NewAssetSetRange(ago4dAS, ago3dAS, ago2dAS, yesterdayAS, todayAS)
+	asr, err := asr.Accumulate(AccumulateOptionAll)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.Assets) != 1 {
+		t.Fatalf("expected 1 asset set, got:%d", len(asr.Assets))
+	}
+}
+
+func TestAssetSetRange_AccumulateBy_Hour(t *testing.T) {
+	ago4h := time.Now().UTC().Truncate(time.Hour).Add(-4 * time.Hour)
+	ago3h := time.Now().UTC().Truncate(time.Hour).Add(-3 * time.Hour)
+	ago2h := time.Now().UTC().Truncate(time.Hour).Add(-2 * time.Hour)
+	ago1h := time.Now().UTC().Truncate(time.Hour).Add(-time.Hour)
+	currentHour := time.Now().UTC().Truncate(time.Hour)
+
+	ago4hAS := GenerateMockAssetSet(ago4h, time.Hour)
+	ago3hAS := GenerateMockAssetSet(ago3h, time.Hour)
+	ago2hAS := GenerateMockAssetSet(ago2h, time.Hour)
+	ago1hAS := GenerateMockAssetSet(ago1h, time.Hour)
+	currentHourAS := GenerateMockAssetSet(currentHour, time.Hour)
+
+	asr := NewAssetSetRange(ago4hAS, ago3hAS, ago2hAS, ago1hAS, currentHourAS)
+	asr, err := asr.Accumulate(AccumulateOptionHour)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.Assets) != 5 {
+		t.Fatalf("expected 5 asset sets, got:%d", len(asr.Assets))
+	}
+
+	allocMap := asr.Assets[0].Assets
+	alloc := allocMap["__undefined__/__undefined__/__undefined__/Storage/cluster2/Disk/Kubernetes/gcp-disk4/disk4"]
+	if alloc.Minutes() != 60.0 {
+		t.Errorf("accumulating asset set range: expected %f minutes; actual %f", 60.0, alloc.Minutes())
+	}
+}
+
+func TestAssetSetRange_AccumulateBy_Day_From_Day(t *testing.T) {
+	ago4d := time.Now().UTC().Truncate(day).Add(-4 * day)
+	ago3d := time.Now().UTC().Truncate(day).Add(-3 * day)
+	ago2d := time.Now().UTC().Truncate(day).Add(-2 * day)
+	yesterday := time.Now().UTC().Truncate(day).Add(-day)
+	today := time.Now().UTC().Truncate(day)
+
+	ago4dAS := GenerateMockAssetSet(ago4d, day)
+	ago3dAS := GenerateMockAssetSet(ago3d, day)
+	ago2dAS := GenerateMockAssetSet(ago2d, day)
+	yesterdayAS := GenerateMockAssetSet(yesterday, day)
+	todayAS := GenerateMockAssetSet(today, day)
+
+	asr := NewAssetSetRange(ago4dAS, ago3dAS, ago2dAS, yesterdayAS, todayAS)
+	asr, err := asr.Accumulate(AccumulateOptionDay)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.Assets) != 5 {
+		t.Fatalf("expected 5 asset sets, got:%d", len(asr.Assets))
+	}
+
+	allocMap := asr.Assets[0].Assets
+	alloc := allocMap["__undefined__/__undefined__/__undefined__/Storage/cluster2/Disk/Kubernetes/gcp-disk4/disk4"]
+	if alloc.Minutes() != 1440.0 {
+		t.Errorf("accumulating asset set range: expected %f minutes; actual %f", 1440.0, alloc.Minutes())
+	}
+}
+
+func TestAssetSetRange_AccumulateBy_Day_From_Hours(t *testing.T) {
+	ago4h := time.Now().UTC().Truncate(time.Hour).Add(-4 * time.Hour)
+	ago3h := time.Now().UTC().Truncate(time.Hour).Add(-3 * time.Hour)
+	ago2h := time.Now().UTC().Truncate(time.Hour).Add(-2 * time.Hour)
+	ago1h := time.Now().UTC().Truncate(time.Hour).Add(-time.Hour)
+	currentHour := time.Now().UTC().Truncate(time.Hour)
+
+	ago4hAS := GenerateMockAssetSet(ago4h, time.Hour)
+	ago3hAS := GenerateMockAssetSet(ago3h, time.Hour)
+	ago2hAS := GenerateMockAssetSet(ago2h, time.Hour)
+	ago1hAS := GenerateMockAssetSet(ago1h, time.Hour)
+	currentHourAS := GenerateMockAssetSet(currentHour, time.Hour)
+
+	asr := NewAssetSetRange(ago4hAS, ago3hAS, ago2hAS, ago1hAS, currentHourAS)
+	asr, err := asr.Accumulate(AccumulateOptionDay)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.Assets) != 1 && len(asr.Assets) != 2 {
+		t.Fatalf("expected 1 allocation set, got:%d", len(asr.Assets))
+	}
+
+	allocMap := asr.Assets[0].Assets
+	alloc := allocMap["__undefined__/__undefined__/__undefined__/Storage/cluster2/Disk/Kubernetes/gcp-disk4/disk4"]
+	if alloc.Minutes() > 300.0 {
+		t.Errorf("accumulating AllocationSetRange: expected %f or less minutes; actual %f", 300.0, alloc.Minutes())
+	}
+}
+
+func TestAssetSetRange_AccumulateBy_Week(t *testing.T) {
+	ago9d := time.Now().UTC().Truncate(day).Add(-9 * day)
+	ago8d := time.Now().UTC().Truncate(day).Add(-8 * day)
+	ago7d := time.Now().UTC().Truncate(day).Add(-7 * day)
+	ago6d := time.Now().UTC().Truncate(day).Add(-6 * day)
+	ago5d := time.Now().UTC().Truncate(day).Add(-5 * day)
+	ago4d := time.Now().UTC().Truncate(day).Add(-4 * day)
+	ago3d := time.Now().UTC().Truncate(day).Add(-3 * day)
+	ago2d := time.Now().UTC().Truncate(day).Add(-2 * day)
+	yesterday := time.Now().UTC().Truncate(day).Add(-day)
+	today := time.Now().UTC().Truncate(day)
+
+	ago9dAS := GenerateMockAssetSet(ago9d, day)
+	ago8dAS := GenerateMockAssetSet(ago8d, day)
+	ago7dAS := GenerateMockAssetSet(ago7d, day)
+	ago6dAS := GenerateMockAssetSet(ago6d, day)
+	ago5dAS := GenerateMockAssetSet(ago5d, day)
+	ago4dAS := GenerateMockAssetSet(ago4d, day)
+	ago3dAS := GenerateMockAssetSet(ago3d, day)
+	ago2dAS := GenerateMockAssetSet(ago2d, day)
+	yesterdayAS := GenerateMockAssetSet(yesterday, day)
+	todayAS := GenerateMockAssetSet(today, day)
+
+	asr := NewAssetSetRange(ago9dAS, ago8dAS, ago7dAS, ago6dAS, ago5dAS, ago4dAS, ago3dAS, ago2dAS, yesterdayAS, todayAS)
+	asr, err := asr.Accumulate(AccumulateOptionWeek)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.Assets) != 2 && len(asr.Assets) != 3 {
+		t.Fatalf("expected 2 or 3 asset sets, got:%d", len(asr.Assets))
+	}
+
+	for _, as := range asr.Assets {
+		if as.Window.Duration() < time.Hour*24 || as.Window.Duration() > time.Hour*24*7 {
+			t.Fatalf("expected window duration to be between 1 and 7 days, got:%s", as.Window.Duration().String())
+		}
+	}
+}
+
+func TestAssetSetRange_AccumulateBy_Month(t *testing.T) {
+	prevMonth1stDay := time.Date(2020, 01, 29, 0, 0, 0, 0, time.UTC)
+	prevMonth2ndDay := time.Date(2020, 01, 30, 0, 0, 0, 0, time.UTC)
+	prevMonth3ndDay := time.Date(2020, 01, 31, 0, 0, 0, 0, time.UTC)
+	nextMonth1stDay := time.Date(2020, 02, 01, 0, 0, 0, 0, time.UTC)
+
+	prev1AS := GenerateMockAssetSet(prevMonth1stDay, day)
+	prev2AS := GenerateMockAssetSet(prevMonth2ndDay, day)
+	prev3AS := GenerateMockAssetSet(prevMonth3ndDay, day)
+	nextAS := GenerateMockAssetSet(nextMonth1stDay, day)
+
+	asr := NewAssetSetRange(prev1AS, prev2AS, prev3AS, nextAS)
+	asr, err := asr.Accumulate(AccumulateOptionMonth)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.Assets) != 2 {
+		t.Fatalf("expected 2 assets sets, got:%d", len(asr.Assets))
+	}
+
+	for _, as := range asr.Assets {
+		if as.Window.Duration() < time.Hour*24 || as.Window.Duration() > time.Hour*24*31 {
+			t.Fatalf("expected window duration to be between 1 and 31 days, got:%s", as.Window.Duration().String())
+		}
+	}
+}

+ 3 - 3
pkg/kubecost/kubecost_codecs_test.go

@@ -212,9 +212,9 @@ func TestAssetSetRange_BinaryEncoding(t *testing.T) {
 	var err error
 
 	asr0 = NewAssetSetRange(
-		GenerateMockAssetSet(startD0),
-		GenerateMockAssetSet(startD1),
-		GenerateMockAssetSet(startD2),
+		GenerateMockAssetSet(startD0, day),
+		GenerateMockAssetSet(startD1, day),
+		GenerateMockAssetSet(startD2, day),
 	)
 
 	bs, err = asr0.MarshalBinary()

+ 2 - 2
pkg/kubecost/mock.go

@@ -608,8 +608,8 @@ func GenerateMockAssetSets(start, end time.Time) []*AssetSet {
 //	total                          57.00   3.00
 //
 // +------------------------------+------+------+
-func GenerateMockAssetSet(start time.Time) *AssetSet {
-	end := start.Add(day)
+func GenerateMockAssetSet(start time.Time, duration time.Duration) *AssetSet {
+	end := start.Add(duration)
 	window := NewWindow(&start, &end)
 
 	hours := window.Duration().Hours()