Przeglądaj źródła

Merge pull request #1716 from saweber/saweber/CORE-49/accumulate-by-v2

allocation - change accumulate to work by month, week, day, hour, true, or false
Niko Kovacevic 3 lat temu
rodzic
commit
75f1532eac

+ 16 - 18
pkg/costmodel/aggregation.go

@@ -1076,7 +1076,7 @@ func (a *Accesses) ComputeAggregateCostModel(promClient prometheusClient.Client,
 		if durMins%60 != 0 || durMins < 3*60 { // not divisible by 1h or less than 3h
 			resolution = time.Minute
 		}
-	} else { // greater than 1d
+	} else {                    // greater than 1d
 		if durMins >= 7*24*60 { // greater than (or equal to) 7 days
 			resolution = 24.0 * time.Hour
 		} else if durMins >= 2*24*60 { // greater than (or equal to) 2 days
@@ -2191,12 +2191,11 @@ func (a *Accesses) ComputeAllocationHandlerSummary(w http.ResponseWriter, r *htt
 
 	// Accumulate, if requested
 	if accumulate {
-		as, err := asr.Accumulate()
+		asr, err = asr.Accumulate(kubecost.AccumulateOptionAll)
 		if err != nil {
 			WriteError(w, InternalServerError(err.Error()))
 			return
 		}
-		asr = kubecost.NewAllocationSetRange(as)
 	}
 
 	sasl := []*kubecost.SummaryAllocationSet{}
@@ -2244,10 +2243,15 @@ func (a *Accesses) ComputeAllocationHandler(w http.ResponseWriter, r *http.Reque
 	// sums each Set in the Range, producing one Set.
 	accumulate := qp.GetBool("accumulate", false)
 
-	// AccumulateBy is an optional parameter that accumulates an AllocationSetRange
+	// Accumulate is an optional parameter that accumulates an AllocationSetRange
 	// by the resolution of the given time duration.
 	// Defaults to 0. If a value is not passed then the parameter is not used.
-	accumulateBy := qp.GetDuration("accumulateBy", 0)
+	accumulateBy := kubecost.AccumulateOption(qp.Get("accumulateBy", ""))
+
+	// if accumulateBy is not explicitly set, and accumulate is true, ensure result is accumulated
+	if accumulateBy == kubecost.AccumulateOptionNone && accumulate {
+		accumulateBy = kubecost.AccumulateOptionAll
+	}
 
 	// Query for AllocationSets in increments of the given step duration,
 	// appending each to the AllocationSetRange.
@@ -2277,19 +2281,13 @@ func (a *Accesses) ComputeAllocationHandler(w http.ResponseWriter, r *http.Reque
 	}
 
 	// Accumulate, if requested
-	if accumulateBy != 0 {
-		asr, err = asr.AccumulateBy(accumulateBy)
-		if err != nil {
-			WriteError(w, InternalServerError(err.Error()))
-			return
-		}
-	} else if accumulate {
-		as, err := asr.Accumulate()
-		if err != nil {
-			WriteError(w, InternalServerError(err.Error()))
-			return
-		}
-		asr = kubecost.NewAllocationSetRange(as)
+	if accumulateBy != kubecost.AccumulateOptionNone {
+		asr, err = asr.Accumulate(accumulateBy)
+	}
+
+	if err != nil {
+		WriteError(w, InternalServerError(err.Error()))
+		return
 	}
 
 	w.Write(WrapData(asr, nil))

+ 8 - 1
pkg/costmodel/allocation.go

@@ -174,10 +174,17 @@ func (cm *CostModel) ComputeAllocation(start, end time.Time, resolution time.Dur
 	// Accumulate to yield the result AllocationSet. After this step, we will
 	// be nearly complete, but without the raw allocation data, which must be
 	// recomputed.
-	result, err := asr.Accumulate()
+	resultASR, err := asr.Accumulate(kubecost.AccumulateOptionAll)
 	if err != nil {
 		return kubecost.NewAllocationSet(start, end), fmt.Errorf("error accumulating data for %s: %s", kubecost.NewClosedWindow(s, e), err)
 	}
+	if resultASR != nil && len(resultASR.Allocations) == 0 {
+		return kubecost.NewAllocationSet(start, end), nil
+	}
+	if length := len(resultASR.Allocations); length != 1 {
+		return kubecost.NewAllocationSet(start, end), fmt.Errorf("expected 1 accumulated allocation set, found %d sets", length)
+	}
+	result := resultASR.Allocations[0]
 
 	// Apply the annotations, labels, and services to the post-accumulation
 	// results. (See above for why this is necessary.)

+ 159 - 21
pkg/kubecost/allocation.go

@@ -2079,7 +2079,7 @@ func (asr *AllocationSetRange) Get(i int) (*AllocationSet, error) {
 
 // Accumulate sums each AllocationSet in the given range, returning a single cumulative
 // AllocationSet for the entire range.
-func (asr *AllocationSetRange) Accumulate() (*AllocationSet, error) {
+func (asr *AllocationSetRange) accumulate() (*AllocationSet, error) {
 	var allocSet *AllocationSet
 	var err error
 
@@ -2093,12 +2093,12 @@ func (asr *AllocationSetRange) Accumulate() (*AllocationSet, error) {
 	return allocSet, nil
 }
 
-// NewAccumulation clones the first available AllocationSet to use as the data structure to
+// newAccumulation clones the first available AllocationSet to use as the data structure to
 // Accumulate the remaining data. This leaves the original AllocationSetRange intact.
-func (asr *AllocationSetRange) NewAccumulation() (*AllocationSet, error) {
+func (asr *AllocationSetRange) newAccumulation() (*AllocationSet, error) {
 	// NOTE: Adding this API for consistency across SummaryAllocation and Assets, but this
 	// NOTE: implementation is almost identical to regular Accumulate(). The Accumulate() method
-	// NOTE: for Allocation returns Clone() of the input, which is required for AccumulateBy
+	// NOTE: for Allocation returns Clone() of the input, which is required for Accumulate
 	// NOTE: support (unit tests are great for verifying this information).
 	var allocSet *AllocationSet
 	var err error
@@ -2131,34 +2131,159 @@ func (asr *AllocationSetRange) NewAccumulation() (*AllocationSet, error) {
 	return allocSet, nil
 }
 
-// AccumulateBy sums AllocationSets based on the resolution given. The resolution given is subject to the scale used for the AllocationSets.
-// Resolutions not evenly divisible by the AllocationSetRange window durations Accumulate sets until a sum greater than or equal to the resolution is met,
-// at which point AccumulateBy will start summing from 0 until the requested resolution is met again.
-// If the requested resolution is smaller than the window of an AllocationSet then the resolution will default to the duration of a set.
-// Resolutions larger than the duration of the entire AllocationSetRange will default to the duration of the range.
-func (asr *AllocationSetRange) AccumulateBy(resolution time.Duration) (*AllocationSetRange, error) {
-	allocSetRange := NewAllocationSetRange()
-	var allocSet *AllocationSet
+// Accumulate sums AllocationSets based on the AccumulateOption (calendar week or calendar month).
+// The accumulated set is determined by the start of the window of the allocation set.
+func (asr *AllocationSetRange) Accumulate(accumulateBy AccumulateOption) (*AllocationSetRange, 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 *AllocationSetRange) accumulateByAll() (*AllocationSetRange, error) {
 	var err error
+	var as *AllocationSet
+	as, err = asr.newAccumulation()
+	if err != nil {
+		return nil, fmt.Errorf("error accumulating all:%s", err)
+	}
+
+	accumulated := NewAllocationSetRange(as)
+	return accumulated, nil
+}
+
+func (asr *AllocationSetRange) accumulateByNone() (*AllocationSetRange, error) {
+	return asr.Clone(), nil
+}
+func (asr *AllocationSetRange) accumulateByHour() (*AllocationSetRange, error) {
+	// ensure that the summary allocation sets have a 1-hour window, if a set exists
+	if len(asr.Allocations) > 0 && asr.Allocations[0].Window.Duration() != time.Hour {
+		return nil, fmt.Errorf("window duration must equal 1 hour; got:%s", asr.Allocations[0].Window.Duration())
+	}
+
+	return asr.Clone(), nil
+}
 
+func (asr *AllocationSetRange) accumulateByDay() (*AllocationSetRange, error) {
+	// if the allocation set window is 1-day, just return the existing allocation set range
+	if len(asr.Allocations) > 0 && asr.Allocations[0].Window.Duration() == time.Hour*24 {
+		return asr, nil
+	}
+
+	var toAccumulate *AllocationSetRange
+	result := NewAllocationSetRange()
 	for i, as := range asr.Allocations {
-		allocSet, err = allocSet.Accumulate(as)
+
+		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 = NewAllocationSetRange()
+			as = as.Clone()
+		}
+
+		toAccumulate.Append(as)
+		asAccumulated, err := toAccumulate.accumulate()
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("error accumulating result: %s", err)
 		}
+		toAccumulate = NewAllocationSetRange(asAccumulated)
 
-		if allocSet != nil {
+		if hour == 23 || i == len(asr.Allocations)-1 {
+			if length := len(toAccumulate.Allocations); length != 1 {
+				return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
+			}
+			result.Append(toAccumulate.Allocations[0])
+			toAccumulate = nil
+		}
+	}
+	return result, nil
+}
 
-			// check if end of asr to sum the final set
-			// If total asr accumulated sum <= resolution return 1 accumulated set
-			if allocSet.Window.Duration() >= resolution || i == len(asr.Allocations)-1 {
-				allocSetRange.Allocations = append(allocSetRange.Allocations, allocSet)
-				allocSet = NewAllocationSet(time.Time{}, time.Time{})
+func (asr *AllocationSetRange) accumulateByMonth() (*AllocationSetRange, error) {
+	var toAccumulate *AllocationSetRange
+	result := NewAllocationSetRange()
+	for i, as := range asr.Allocations {
+		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 = NewAllocationSetRange()
+			as = as.Clone()
+		}
+
+		toAccumulate.Append(as)
+		asAccumulated, err := toAccumulate.accumulate()
+		if err != nil {
+			return nil, fmt.Errorf("error accumulating result: %s", err)
+		}
+		toAccumulate = NewAllocationSetRange(asAccumulated)
+
+		// either the month has ended, or there are no more allocation sets
+		if month != nextDayMonth || i == len(asr.Allocations)-1 {
+			if length := len(toAccumulate.Allocations); length != 1 {
+				return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
 			}
+			result.Append(toAccumulate.Allocations[0])
+			toAccumulate = nil
 		}
 	}
+	return result, nil
+}
+
+func (asr *AllocationSetRange) accumulateByWeek() (*AllocationSetRange, error) {
+	var toAccumulate *AllocationSetRange
+	result := NewAllocationSetRange()
+	for i, as := range asr.Allocations {
+		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()
 
-	return allocSetRange, nil
+		if toAccumulate == nil {
+			toAccumulate = NewAllocationSetRange()
+			as = as.Clone()
+		}
+
+		toAccumulate.Append(as)
+		asAccumulated, err := toAccumulate.accumulate()
+		if err != nil {
+			return nil, fmt.Errorf("error accumulating result: %s", err)
+		}
+		toAccumulate = NewAllocationSetRange(asAccumulated)
+
+		// current assumption is the week always ends on Saturday, or there are no more allocation sets
+		if dayOfWeek == time.Saturday || i == len(asr.Allocations)-1 {
+			if length := len(toAccumulate.Allocations); length != 1 {
+				return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
+			}
+			result.Append(toAccumulate.Allocations[0])
+			toAccumulate = nil
+		}
+	}
+	return result, nil
 }
 
 // AggregateBy aggregates each AllocationSet in the range by the given
@@ -2435,3 +2560,16 @@ func (asr *AllocationSetRange) TotalCost() float64 {
 	}
 	return tc
 }
+
+// Clone returns a new AllocationSetRange cloned from the existing ASR
+func (asr *AllocationSetRange) Clone() *AllocationSetRange {
+	sasrClone := NewAllocationSetRange()
+	sasrClone.FromStore = asr.FromStore
+
+	for _, as := range asr.Allocations {
+		asClone := as.Clone()
+		sasrClone.Append(asClone)
+	}
+
+	return sasrClone
+}

+ 204 - 290
pkg/kubecost/allocation_test.go

@@ -1631,7 +1631,7 @@ func TestAllocationSetRange_AccumulateRepeat(t *testing.T) {
 	totalCost := asr.TotalCost()
 
 	// NewAccumulation does not mutate
-	result, err := asr.NewAccumulation()
+	result, err := asr.newAccumulation()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -1643,7 +1643,7 @@ func TestAllocationSetRange_AccumulateRepeat(t *testing.T) {
 	}
 
 	// Next NewAccumulation() call should prove that there is no mutation of inner data
-	result, err = asr.NewAccumulation()
+	result, err = asr.newAccumulation()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -1663,7 +1663,7 @@ func TestAllocationSetRange_Accumulate(t *testing.T) {
 	tomorrow := time.Now().UTC().Truncate(day).Add(day)
 
 	// Accumulating any combination of nil and/or empty set should result in empty set
-	result, err := NewAllocationSetRange(nil).Accumulate()
+	result, err := NewAllocationSetRange(nil).accumulate()
 	if err != nil {
 		t.Fatalf("unexpected error accumulating nil AllocationSetRange: %s", err)
 	}
@@ -1671,7 +1671,7 @@ func TestAllocationSetRange_Accumulate(t *testing.T) {
 		t.Fatalf("accumulating nil AllocationSetRange: expected empty; actual %s", result)
 	}
 
-	result, err = NewAllocationSetRange(nil, nil).Accumulate()
+	result, err = NewAllocationSetRange(nil, nil).accumulate()
 	if err != nil {
 		t.Fatalf("unexpected error accumulating nil AllocationSetRange: %s", err)
 	}
@@ -1679,7 +1679,7 @@ func TestAllocationSetRange_Accumulate(t *testing.T) {
 		t.Fatalf("accumulating nil AllocationSetRange: expected empty; actual %s", result)
 	}
 
-	result, err = NewAllocationSetRange(NewAllocationSet(yesterday, today)).Accumulate()
+	result, err = NewAllocationSetRange(NewAllocationSet(yesterday, today)).accumulate()
 	if err != nil {
 		t.Fatalf("unexpected error accumulating nil AllocationSetRange: %s", err)
 	}
@@ -1687,7 +1687,7 @@ func TestAllocationSetRange_Accumulate(t *testing.T) {
 		t.Fatalf("accumulating nil AllocationSetRange: expected empty; actual %s", result)
 	}
 
-	result, err = NewAllocationSetRange(nil, NewAllocationSet(ago2d, yesterday), nil, NewAllocationSet(today, tomorrow), nil).Accumulate()
+	result, err = NewAllocationSetRange(nil, NewAllocationSet(ago2d, yesterday), nil, NewAllocationSet(today, tomorrow), nil).accumulate()
 	if err != nil {
 		t.Fatalf("unexpected error accumulating nil AllocationSetRange: %s", err)
 	}
@@ -1702,7 +1702,7 @@ func TestAllocationSetRange_Accumulate(t *testing.T) {
 	yesterdayAS.Set(NewMockUnitAllocation("", yesterday, day, nil))
 
 	// Accumulate non-nil with nil should result in copy of non-nil, regardless of order
-	result, err = NewAllocationSetRange(nil, todayAS).Accumulate()
+	result, err = NewAllocationSetRange(nil, todayAS).accumulate()
 	if err != nil {
 		t.Fatalf("unexpected error accumulating AllocationSetRange of length 1: %s", err)
 	}
@@ -1713,7 +1713,7 @@ func TestAllocationSetRange_Accumulate(t *testing.T) {
 		t.Fatalf("accumulating AllocationSetRange: expected total cost 6.0; actual %f", result.TotalCost())
 	}
 
-	result, err = NewAllocationSetRange(todayAS, nil).Accumulate()
+	result, err = NewAllocationSetRange(todayAS, nil).accumulate()
 	if err != nil {
 		t.Fatalf("unexpected error accumulating AllocationSetRange of length 1: %s", err)
 	}
@@ -1724,7 +1724,7 @@ func TestAllocationSetRange_Accumulate(t *testing.T) {
 		t.Fatalf("accumulating AllocationSetRange: expected total cost 6.0; actual %f", result.TotalCost())
 	}
 
-	result, err = NewAllocationSetRange(nil, todayAS, nil).Accumulate()
+	result, err = NewAllocationSetRange(nil, todayAS, nil).accumulate()
 	if err != nil {
 		t.Fatalf("unexpected error accumulating AllocationSetRange of length 1: %s", err)
 	}
@@ -1736,7 +1736,7 @@ func TestAllocationSetRange_Accumulate(t *testing.T) {
 	}
 
 	// Accumulate two non-nil should result in sum of both with appropriate start, end
-	result, err = NewAllocationSetRange(yesterdayAS, todayAS).Accumulate()
+	result, err = NewAllocationSetRange(yesterdayAS, todayAS).accumulate()
 	if err != nil {
 		t.Fatalf("unexpected error accumulating AllocationSetRange of length 1: %s", err)
 	}
@@ -1806,115 +1806,110 @@ func TestAllocationSetRange_Accumulate(t *testing.T) {
 		t.Fatalf("accumulating AllocationSetRange: expected %f minutes; actual %f", 2880.0, alloc.Minutes())
 	}
 }
-func TestAllocationSetRange_AccumulateBy_Nils(t *testing.T) {
-	var err error
-	var result *AllocationSetRange
 
+func TestAllocationSetRange_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)
 	tomorrow := time.Now().UTC().Truncate(day).Add(day)
 
-	// Test nil & empty sets
-	nilEmptycases := []struct {
-		asr        *AllocationSetRange
-		resolution time.Duration
-
-		testId string
-	}{
-		{
-			asr:        NewAllocationSetRange(nil),
-			resolution: time.Hour * 24 * 2,
-
-			testId: "AccumulateBy_Nils Empty Test 1",
-		},
-		{
-			asr:        NewAllocationSetRange(nil, nil),
-			resolution: time.Hour * 1,
-
-			testId: "AccumulateBy_Nils Empty Test 2",
-		},
-		{
-			asr:        NewAllocationSetRange(nil, NewAllocationSet(ago2d, yesterday), nil, NewAllocationSet(today, tomorrow)),
-			resolution: time.Hour * 24 * 7,
+	ago4dAS := NewAllocationSet(ago4d, ago3d)
+	ago4dAS.Set(NewMockUnitAllocation("4", ago4d, day, nil))
+	ago3dAS := NewAllocationSet(ago3d, ago2d)
+	ago3dAS.Set(NewMockUnitAllocation("a", ago3d, day, nil))
+	ago2dAS := NewAllocationSet(ago2d, yesterday)
+	ago2dAS.Set(NewMockUnitAllocation("", ago2d, day, nil))
+	yesterdayAS := NewAllocationSet(yesterday, today)
+	yesterdayAS.Set(NewMockUnitAllocation("", yesterday, day, nil))
+	todayAS := NewAllocationSet(today, tomorrow)
+	todayAS.Set(NewMockUnitAllocation("", today, day, nil))
 
-			testId: "AccumulateBy_Nils Empty Test 3",
-		},
+	asr := NewAllocationSetRange(ago4dAS, ago3dAS, ago2dAS, yesterdayAS, todayAS)
+	asr, err := asr.Accumulate(AccumulateOptionNone)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
 	}
 
-	for _, c := range nilEmptycases {
-		result, err = c.asr.AccumulateBy(c.resolution)
-		for _, as := range result.Allocations {
-			if !as.IsEmpty() {
-				t.Errorf("accumulating nil AllocationSetRange: expected empty; actual %s; TestId: %s", result, c.testId)
-			}
-		}
-	}
-	if err != nil {
-		t.Errorf("unexpected error accumulating nil AllocationSetRange: %s", err)
+	if len(asr.Allocations) != 5 {
+		t.Fatalf("expected 5 allocation sets, got:%d", len(asr.Allocations))
 	}
+}
+
+func TestAllocationSetRange_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)
+	tomorrow := time.Now().UTC().Truncate(day).Add(day)
 
+	ago4dAS := NewAllocationSet(ago4d, ago3d)
+	ago4dAS.Set(NewMockUnitAllocation("4", ago4d, day, nil))
+	ago3dAS := NewAllocationSet(ago3d, ago2d)
+	ago3dAS.Set(NewMockUnitAllocation("a", ago3d, day, nil))
+	ago2dAS := NewAllocationSet(ago2d, yesterday)
+	ago2dAS.Set(NewMockUnitAllocation("", ago2d, day, nil))
 	yesterdayAS := NewAllocationSet(yesterday, today)
-	yesterdayAS.Set(NewMockUnitAllocation("a", yesterday, day, nil))
+	yesterdayAS.Set(NewMockUnitAllocation("", yesterday, day, nil))
 	todayAS := NewAllocationSet(today, tomorrow)
-	todayAS.Set(NewMockUnitAllocation("b", today, day, nil))
-
-	nilAndNonEmptyCases := []struct {
-		asr        *AllocationSetRange
-		resolution time.Duration
-
-		expected float64
-		testId   string
-	}{
-		{
-			asr:        NewAllocationSetRange(nil, todayAS),
-			resolution: time.Hour * 2,
+	todayAS.Set(NewMockUnitAllocation("", today, day, nil))
 
-			expected: 6.0,
-			testId:   "AccumulateBy_Nils NonEmpty Test 1",
-		},
-		{
-			asr:        NewAllocationSetRange(todayAS, nil),
-			resolution: time.Hour * 24,
+	asr := NewAllocationSetRange(ago4dAS, ago3dAS, ago2dAS, yesterdayAS, todayAS)
+	asr, err := asr.Accumulate(AccumulateOptionAll)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
 
-			expected: 6.0,
-			testId:   "AccumulateBy_Nils NonEmpty Test 2",
-		},
-		{
-			asr:        NewAllocationSetRange(yesterdayAS, nil, todayAS, nil),
-			resolution: time.Hour * 24 * 2,
+	if len(asr.Allocations) != 1 {
+		t.Fatalf("expected 1 allocation set, got:%d", len(asr.Allocations))
+	}
 
-			expected: 12.0,
-			testId:   "AccumulateBy_Nils NonEmpty Test 3",
-		},
+	allocMap := asr.Allocations[0].Allocations
+	alloc := allocMap["cluster1/namespace1/pod1/container1"]
+	if alloc.Minutes() != 4320.0 {
+		t.Errorf("accumulating AllocationSetRange: expected %f minutes; actual %f", 4320.0, alloc.Minutes())
 	}
+}
 
-	for _, c := range nilAndNonEmptyCases {
-		result, err = c.asr.AccumulateBy(c.resolution)
-		sumCost := 0.0
+func TestAllocationSetRange_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)
+	nextHour := time.Now().UTC().Truncate(time.Hour).Add(time.Hour)
 
-		if result == nil {
-			t.Errorf("accumulating AllocationSetRange: expected AllocationSet; actual %s; TestId: %s", result, c.testId)
-		}
+	ago4hAS := NewAllocationSet(ago4h, ago3h)
+	ago4hAS.Set(NewMockUnitAllocation("4", ago4h, time.Hour, nil))
+	ago3hAS := NewAllocationSet(ago3h, ago2h)
+	ago3hAS.Set(NewMockUnitAllocation("a", ago3h, time.Hour, nil))
+	ago2hAS := NewAllocationSet(ago2h, ago1h)
+	ago2hAS.Set(NewMockUnitAllocation("", ago2h, time.Hour, nil))
+	ago1hAS := NewAllocationSet(ago1h, currentHour)
+	ago1hAS.Set(NewMockUnitAllocation("", ago1h, time.Hour, nil))
+	currentHourAS := NewAllocationSet(currentHour, nextHour)
+	currentHourAS.Set(NewMockUnitAllocation("", currentHour, time.Hour, nil))
 
-		for _, as := range result.Allocations {
-			sumCost += as.TotalCost()
-		}
+	asr := NewAllocationSetRange(ago4hAS, ago3hAS, ago2hAS, ago1hAS, currentHourAS)
+	asr, err := asr.Accumulate(AccumulateOptionNone)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
 
-		if sumCost != c.expected {
-			t.Errorf("accumulating AllocationSetRange: expected total cost %f; actual %f; TestId: %s", c.expected, sumCost, c.testId)
-		}
+	if len(asr.Allocations) != 5 {
+		t.Fatalf("expected 5 allocation sets, got:%d", len(asr.Allocations))
 	}
 
-	if err != nil {
-		t.Errorf("unexpected error accumulating nil AllocationSetRange: %s", err)
+	allocMap := asr.Allocations[0].Allocations
+	alloc := allocMap["4"]
+	if alloc.Minutes() != 60.0 {
+		t.Errorf("accumulating AllocationSetRange: expected %f minutes; actual %f", 60.0, alloc.Minutes())
 	}
 }
 
-func TestAllocationSetRange_AccumulateBy(t *testing.T) {
-	var err error
-	var result *AllocationSetRange
-
+func TestAllocationSetRange_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)
@@ -1933,223 +1928,142 @@ func TestAllocationSetRange_AccumulateBy(t *testing.T) {
 	todayAS := NewAllocationSet(today, tomorrow)
 	todayAS.Set(NewMockUnitAllocation("", today, day, nil))
 
-	yesterHour := time.Now().UTC().Truncate(time.Hour).Add(-1 * time.Hour)
+	asr := NewAllocationSetRange(ago4dAS, ago3dAS, ago2dAS, yesterdayAS, todayAS)
+	asr, err := asr.Accumulate(AccumulateOptionDay)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.Allocations) != 5 {
+		t.Fatalf("expected 5 allocation sets, got:%d", len(asr.Allocations))
+	}
+	allocMap := asr.Allocations[0].Allocations
+	alloc := allocMap["4"]
+	if alloc.Minutes() != 1440.0 {
+		t.Errorf("accumulating AllocationSetRange: expected %f minutes; actual %f", 1440.0, alloc.Minutes())
+	}
+}
+
+func TestAllocationSetRange_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)
 	nextHour := time.Now().UTC().Truncate(time.Hour).Add(time.Hour)
 
-	yesterHourAS := NewAllocationSet(yesterHour, currentHour)
-	yesterHourAS.Set(NewMockUnitAllocation("123", yesterHour, time.Hour, nil))
+	ago4hAS := NewAllocationSet(ago4h, ago3h)
+	ago4hAS.Set(NewMockUnitAllocation("", ago4h, time.Hour, nil))
+	ago3hAS := NewAllocationSet(ago3h, ago2h)
+	ago3hAS.Set(NewMockUnitAllocation("", ago3h, time.Hour, nil))
+	ago2hAS := NewAllocationSet(ago2h, ago1h)
+	ago2hAS.Set(NewMockUnitAllocation("", ago2h, time.Hour, nil))
+	ago1hAS := NewAllocationSet(ago1h, currentHour)
+	ago1hAS.Set(NewMockUnitAllocation("", ago1h, time.Hour, nil))
 	currentHourAS := NewAllocationSet(currentHour, nextHour)
-	currentHourAS.Set(NewMockUnitAllocation("456", currentHour, time.Hour, nil))
-
-	sumCost := 0.0
-
-	// Test nil & empty sets
-	cases := []struct {
-		asr        *AllocationSetRange
-		resolution time.Duration
-
-		expectedCost float64
-		expectedSets int
-
-		testId string
-	}{
-		{
-			asr:        NewAllocationSetRange(yesterdayAS, todayAS),
-			resolution: time.Hour * 24 * 2,
-
-			expectedCost: 12.0,
-			expectedSets: 1,
-
-			testId: "AccumulateBy Test 1",
-		},
-		{
-			asr:        NewAllocationSetRange(ago3dAS, ago2dAS),
-			resolution: time.Hour * 24,
-
-			expectedCost: 12.0,
-			expectedSets: 2,
-
-			testId: "AccumulateBy Test 2",
-		},
-		{
-			asr:        NewAllocationSetRange(ago2dAS, yesterdayAS, todayAS),
-			resolution: time.Hour * 13,
-
-			expectedCost: 18.0,
-			expectedSets: 3,
-
-			testId: "AccumulateBy Test 3",
-		},
-		{
-			asr:        NewAllocationSetRange(ago2dAS, yesterdayAS, todayAS),
-			resolution: time.Hour * 24 * 7,
+	currentHourAS.Set(NewMockUnitAllocation("", currentHour, time.Hour, nil))
 
-			expectedCost: 18.0,
-			expectedSets: 1,
-
-			testId: "AccumulateBy Test 4",
-		},
-		{
-			asr:        NewAllocationSetRange(yesterHourAS, currentHourAS),
-			resolution: time.Hour * 2,
-
-			//Due to how mock Allocation Sets are generated, hourly sets are still 6.0 cost per set
-			expectedCost: 12.0,
-			expectedSets: 1,
-
-			testId: "AccumulateBy Test 5",
-		},
-		{
-			asr:        NewAllocationSetRange(yesterHourAS, currentHourAS),
-			resolution: time.Hour,
-
-			expectedCost: 12.0,
-			expectedSets: 2,
-
-			testId: "AccumulateBy Test 6",
-		},
-		{
-			asr:        NewAllocationSetRange(yesterHourAS, currentHourAS),
-			resolution: time.Minute * 11,
-
-			expectedCost: 12.0,
-			expectedSets: 2,
-
-			testId: "AccumulateBy Test 7",
-		},
-		{
-			asr:        NewAllocationSetRange(yesterHourAS, currentHourAS),
-			resolution: time.Hour * 3,
-
-			expectedCost: 12.0,
-			expectedSets: 1,
-
-			testId: "AccumulateBy Test 8",
-		},
-		{
-			asr:        NewAllocationSetRange(ago2dAS, yesterdayAS, todayAS),
-			resolution: time.Hour * 24 * 2,
-
-			expectedCost: 18.0,
-			expectedSets: 2,
+	asr := NewAllocationSetRange(ago4hAS, ago3hAS, ago2hAS, ago1hAS, currentHourAS)
+	asr, err := asr.Accumulate(AccumulateOptionDay)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
 
-			testId: "AccumulateBy Test 9",
-		},
-		{
-			asr:        NewAllocationSetRange(ago3dAS, ago2dAS, yesterdayAS, todayAS),
-			resolution: time.Hour * 25,
+	if len(asr.Allocations) != 1 && len(asr.Allocations) != 2 {
+		t.Fatalf("expected 1 allocation set, got:%d", len(asr.Allocations))
+	}
 
-			expectedCost: 24.0,
-			expectedSets: 2,
+	allocMap := asr.Allocations[0].Allocations
+	alloc := allocMap["cluster1/namespace1/pod1/container1"]
+	if alloc.Minutes() > 300.0 {
+		t.Errorf("accumulating AllocationSetRange: expected %f or less minutes; actual %f", 300.0, alloc.Minutes())
+	}
+}
 
-			testId: "AccumulateBy Test 10",
-		},
-		{
-			asr:        NewAllocationSetRange(ago4dAS, ago3dAS, ago2dAS, yesterdayAS, todayAS),
-			resolution: time.Hour * 72,
+func TestAllocationSetRange_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)
+	tomorrow := time.Now().UTC().Truncate(day).Add(day)
 
-			expectedCost: 30.0,
-			expectedSets: 2,
+	ago9dAS := NewAllocationSet(ago9d, ago8d)
+	ago9dAS.Set(NewMockUnitAllocation("4", ago9d, day, nil))
+	ago8dAS := NewAllocationSet(ago8d, ago7d)
+	ago8dAS.Set(NewMockUnitAllocation("4", ago8d, day, nil))
+	ago7dAS := NewAllocationSet(ago7d, ago6d)
+	ago7dAS.Set(NewMockUnitAllocation("4", ago7d, day, nil))
+	ago6dAS := NewAllocationSet(ago6d, ago5d)
+	ago6dAS.Set(NewMockUnitAllocation("4", ago6d, day, nil))
+	ago5dAS := NewAllocationSet(ago5d, ago4d)
+	ago5dAS.Set(NewMockUnitAllocation("4", ago5d, day, nil))
+	ago4dAS := NewAllocationSet(ago4d, ago3d)
+	ago4dAS.Set(NewMockUnitAllocation("4", ago4d, day, nil))
+	ago3dAS := NewAllocationSet(ago3d, ago2d)
+	ago3dAS.Set(NewMockUnitAllocation("a", ago3d, day, nil))
+	ago2dAS := NewAllocationSet(ago2d, yesterday)
+	ago2dAS.Set(NewMockUnitAllocation("", ago2d, day, nil))
+	yesterdayAS := NewAllocationSet(yesterday, today)
+	yesterdayAS.Set(NewMockUnitAllocation("", yesterday, day, nil))
+	todayAS := NewAllocationSet(today, tomorrow)
+	todayAS.Set(NewMockUnitAllocation("", today, day, nil))
 
-			testId: "AccumulateBy Test 11",
-		},
+	asr := NewAllocationSetRange(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)
 	}
 
-	for _, c := range cases {
-		result, err = c.asr.AccumulateBy(c.resolution)
-		sumCost := 0.0
-		if result == nil {
-			t.Errorf("accumulating AllocationSetRange: expected AllocationSet; actual %s; TestId: %s", result, c.testId)
-		}
-		if result.Length() != c.expectedSets {
-			t.Errorf("accumulating AllocationSetRange: expected %v number of allocation sets; actual %v; TestId: %s", c.expectedSets, result.Length(), c.testId)
-		}
-
-		for _, as := range result.Allocations {
-			sumCost += as.TotalCost()
-		}
-		if sumCost != c.expectedCost {
-			t.Errorf("accumulating AllocationSetRange: expected total cost %f; actual %f; TestId: %s", c.expectedCost, sumCost, c.testId)
-		}
+	if len(asr.Allocations) != 2 && len(asr.Allocations) != 3 {
+		t.Fatalf("expected 2 or 3 allocation sets, got:%d", len(asr.Allocations))
 	}
 
-	if err != nil {
-		t.Errorf("unexpected error accumulating nil AllocationSetRange: %s", err)
+	for _, as := range asr.Allocations {
+		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())
+		}
 	}
+}
 
-	// // Accumulate three non-nil should result in sum of both with appropriate start, end
-	result, err = NewAllocationSetRange(ago2dAS, yesterdayAS, todayAS).AccumulateBy(time.Hour * 24 * 2)
+func TestAllocationSetRange_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)
+	nextMonth2ndDay := time.Date(2020, 02, 02, 0, 0, 0, 0, time.UTC)
+
+	prev1AS := NewAllocationSet(prevMonth1stDay, prevMonth2ndDay)
+	prev1AS.Set(NewMockUnitAllocation("", prevMonth1stDay, day, nil))
+	prev2AS := NewAllocationSet(prevMonth2ndDay, prevMonth3ndDay)
+	prev2AS.Set(NewMockUnitAllocation("", prevMonth2ndDay, day, nil))
+
+	prev3AS := NewAllocationSet(prevMonth3ndDay, nextMonth1stDay)
+	prev3AS.Set(NewMockUnitAllocation("", prevMonth3ndDay, day, nil))
+
+	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 {
-		t.Errorf("unexpected error accumulating AllocationSetRange of length 1: %s", err)
-	}
-	if result == nil {
-		t.Errorf("accumulating AllocationSetRange: expected AllocationSet; actual %s", result)
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
 	}
 
-	sumCost = 0.0
-	for _, as := range result.Allocations {
-		sumCost += as.TotalCost()
+	if len(asr.Allocations) != 2 {
+		t.Fatalf("expected 2 allocation sets, got:%d", len(asr.Allocations))
 	}
 
-	allocMap := result.Allocations[0].Allocations
-	if len(allocMap) != 1 {
-		t.Errorf("accumulating AllocationSetRange: expected length 1; actual length %d", len(allocMap))
-	}
-	alloc := allocMap["cluster1/namespace1/pod1/container1"]
-	if alloc == nil {
-		t.Fatalf("accumulating AllocationSetRange: expected allocation 'cluster1/namespace1/pod1/container1'")
-	}
-	if alloc.CPUCoreHours != 2.0 {
-		t.Errorf("accumulating AllocationSetRange: expected 2.0; actual %f", sumCost)
-	}
-	if alloc.CPUCost != 2.0 {
-		t.Errorf("accumulating AllocationSetRange: expected 2.0; actual %f", alloc.CPUCost)
-	}
-	if alloc.CPUEfficiency() != 1.0 {
-		t.Errorf("accumulating AllocationSetRange: expected 1.0; actual %f", alloc.CPUEfficiency())
-	}
-	if alloc.GPUHours != 2.0 {
-		t.Errorf("accumulating AllocationSetRange: expected 2.0; actual %f", alloc.GPUHours)
-	}
-	if alloc.GPUCost != 2.0 {
-		t.Errorf("accumulating AllocationSetRange: expected 2.0; actual %f", alloc.GPUCost)
-	}
-	if alloc.NetworkCost != 2.0 {
-		t.Errorf("accumulating AllocationSetRange: expected 2.0; actual %f", alloc.NetworkCost)
-	}
-	if alloc.LoadBalancerCost != 2.0 {
-		t.Errorf("accumulating AllocationSetRange: expected 2.0; actual %f", alloc.LoadBalancerCost)
-	}
-	if alloc.PVByteHours() != 2.0 {
-		t.Errorf("accumulating AllocationSetRange: expected 2.0; actual %f", alloc.PVByteHours())
-	}
-	if alloc.PVCost() != 2.0 {
-		t.Errorf("accumulating AllocationSetRange: expected 2.0; actual %f", alloc.PVCost())
-	}
-	if alloc.RAMByteHours != 2.0 {
-		t.Errorf("accumulating AllocationSetRange: expected 2.0; actual %f", alloc.RAMByteHours)
-	}
-	if alloc.RAMCost != 2.0 {
-		t.Errorf("accumulating AllocationSetRange: expected 2.0; actual %f", alloc.RAMCost)
-	}
-	if alloc.RAMEfficiency() != 1.0 {
-		t.Errorf("accumulating AllocationSetRange: expected 1.0; actual %f", alloc.RAMEfficiency())
-	}
-	if alloc.TotalCost() != 12.0 {
-		t.Errorf("accumulating AllocationSetRange: expected 12.0; actual %f", alloc.TotalCost())
-	}
-	if alloc.TotalEfficiency() != 1.0 {
-		t.Errorf("accumulating AllocationSetRange: expected 1.0; actual %f", alloc.TotalEfficiency())
-	}
-	if !alloc.Start.Equal(ago2d) {
-		t.Errorf("accumulating AllocationSetRange: expected to start %s; actual %s", ago2d, alloc.Start)
-	}
-	if !alloc.End.Equal(today) {
-		t.Errorf("accumulating AllocationSetRange: expected to end %s; actual %s", today, alloc.End)
-	}
-	if alloc.Minutes() != 2880.0 {
-		t.Errorf("accumulating AllocationSetRange: expected %f minutes; actual %f", 2880.0, alloc.Minutes())
+	for _, as := range asr.Allocations {
+		if as.Window.Duration() < time.Hour*24 || as.Window.Duration() > time.Hour*24*31 {
+			t.Fatalf("expected window duration to be between 1 and 7 days, got:%s", as.Window.Duration().String())
+		}
 	}
 }
 
@@ -2681,7 +2595,7 @@ func TestAllocationSet_Accumulate_Equals_AllocationSetRange_Accumulate(t *testin
 		asr.Append(as.Clone())
 	}
 
-	expected, err := asr.Accumulate()
+	expected, err := asr.accumulate()
 	if err != nil {
 		t.Errorf("TestAllocationSet_Accumulate_Equals_AllocationSetRange_Accumulate: AllocationSetRange.Accumulate() returned an error\n")
 	}

+ 59 - 0
pkg/kubecost/mock.go

@@ -744,3 +744,62 @@ func GenerateGCPMockCCIAndPID(mockProviderIDInt int, mockCloudIDInt int, labelKe
 		},
 	}, ""
 }
+
+// NewMockUnitSummaryAllocation creates an *SummaryAllocation with all of its float64 values set to 1 and generic properties if not provided in arg
+func NewMockUnitSummaryAllocation(name string, start time.Time, resolution time.Duration, props *AllocationProperties) *SummaryAllocation {
+	if name == "" {
+		name = "cluster1/namespace1/pod1/container1"
+	}
+
+	properties := &AllocationProperties{}
+	if props == nil {
+		properties.Cluster = "cluster1"
+		properties.Node = "node1"
+		properties.Namespace = "namespace1"
+		properties.ControllerKind = "deployment"
+		properties.Controller = "deployment1"
+		properties.Pod = "pod1"
+		properties.Container = "container1"
+	} else {
+		properties = props
+	}
+
+	end := start.Add(resolution)
+
+	alloc := &SummaryAllocation{
+		Name:                   name,
+		Properties:             properties,
+		Start:                  start,
+		End:                    end,
+		CPUCost:                1,
+		CPUCoreRequestAverage:  1,
+		CPUCoreUsageAverage:    1,
+		GPUCost:                1,
+		NetworkCost:            1,
+		LoadBalancerCost:       1,
+		RAMCost:                1,
+		RAMBytesRequestAverage: 1,
+		RAMBytesUsageAverage:   1,
+	}
+
+	// If idle allocation, remove non-idle costs, but maintain total cost
+	if alloc.IsIdle() {
+		alloc.NetworkCost = 0.0
+		alloc.LoadBalancerCost = 0.0
+		alloc.CPUCost += 1.0
+		alloc.RAMCost += 1.0
+	}
+
+	return alloc
+}
+
+// NewMockUnitSummaryAllocationSet creates an *SummaryAllocationSet
+func NewMockUnitSummaryAllocationSet(start time.Time, resolution time.Duration) *SummaryAllocationSet {
+
+	end := start.Add(resolution)
+	sas := &SummaryAllocationSet{
+		Window: NewWindow(&start, &end),
+	}
+
+	return sas
+}

+ 12 - 2
pkg/kubecost/query.go

@@ -34,8 +34,7 @@ type CloudUsageQuerier interface {
 
 // AllocationQueryOptions defines optional parameters for querying an Allocation Store
 type AllocationQueryOptions struct {
-	Accumulate              bool
-	AccumulateBy            time.Duration
+	Accumulate              AccumulateOption
 	AggregateBy             []string
 	Compute                 bool
 	DisableAggregatedStores bool
@@ -56,6 +55,17 @@ type AllocationQueryOptions struct {
 	Step                    time.Duration
 }
 
+type AccumulateOption string
+
+const (
+	AccumulateOptionNone  AccumulateOption = ""
+	AccumulateOptionAll   AccumulateOption = "all"
+	AccumulateOptionHour  AccumulateOption = "hour"
+	AccumulateOptionDay   AccumulateOption = "day"
+	AccumulateOptionWeek  AccumulateOption = "week"
+	AccumulateOptionMonth AccumulateOption = "month"
+)
+
 // AssetQueryOptions defines optional parameters for querying an Asset Store
 type AssetQueryOptions struct {
 	Accumulate              bool

+ 201 - 6
pkg/kubecost/summaryallocation.go

@@ -1303,7 +1303,7 @@ func NewSummaryAllocationSetRange(sass ...*SummaryAllocationSet) *SummaryAllocat
 
 // Accumulate sums each AllocationSet in the given range, returning a single cumulative
 // AllocationSet for the entire range.
-func (sasr *SummaryAllocationSetRange) Accumulate() (*SummaryAllocationSet, error) {
+func (sasr *SummaryAllocationSetRange) accumulate() (*SummaryAllocationSet, error) {
 	var result *SummaryAllocationSet
 	var err error
 
@@ -1320,9 +1320,9 @@ func (sasr *SummaryAllocationSetRange) Accumulate() (*SummaryAllocationSet, erro
 	return result, nil
 }
 
-// NewAccumulation clones the first available SummaryAllocationSet to use as the data structure to
+// newAccumulation clones the first available SummaryAllocationSet to use as the data structure to
 // accumulate the remaining data. This leaves the original SummaryAllocationSetRange intact.
-func (sasr *SummaryAllocationSetRange) NewAccumulation() (*SummaryAllocationSet, error) {
+func (sasr *SummaryAllocationSetRange) newAccumulation() (*SummaryAllocationSet, error) {
 	var result *SummaryAllocationSet
 	var err error
 
@@ -1374,9 +1374,6 @@ func (sasr *SummaryAllocationSetRange) AggregateBy(aggregateBy []string, options
 // Append appends the given AllocationSet to the end of the range. It does not
 // validate whether or not that violates window continuity.
 func (sasr *SummaryAllocationSetRange) Append(sas *SummaryAllocationSet) error {
-	if sasr.Step != 0 && sas.Window.Duration() != sasr.Step {
-		return fmt.Errorf("cannot append set with duration %s to range of step %s", sas.Window.Duration(), sasr.Step)
-	}
 
 	sasr.Lock()
 	defer sasr.Unlock()
@@ -1493,3 +1490,201 @@ func (sasr *SummaryAllocationSetRange) Print(verbose bool) {
 		}
 	}
 }
+
+func (sasr *SummaryAllocationSetRange) Accumulate(accumulateBy AccumulateOption) (*SummaryAllocationSetRange, error) {
+	switch accumulateBy {
+	case AccumulateOptionNone:
+		return sasr.accumulateByNone()
+	case AccumulateOptionAll:
+		return sasr.accumulateByAll()
+	case AccumulateOptionHour:
+		return sasr.accumulateByHour()
+	case AccumulateOptionDay:
+		return sasr.accumulateByDay()
+	case AccumulateOptionWeek:
+		return sasr.accumulateByWeek()
+	case AccumulateOptionMonth:
+		return sasr.accumulateByMonth()
+	default:
+		// this should never happen
+		return nil, fmt.Errorf("unexpected error, invalid accumulateByType: %s", accumulateBy)
+	}
+}
+
+func (sasr *SummaryAllocationSetRange) accumulateByNone() (*SummaryAllocationSetRange, error) {
+	result, err := sasr.clone()
+	return result, err
+}
+
+func (sasr *SummaryAllocationSetRange) accumulateByAll() (*SummaryAllocationSetRange, error) {
+	var err error
+	var result *SummaryAllocationSet
+	result, err = sasr.newAccumulation()
+
+	if err != nil {
+		return nil, fmt.Errorf("error running accumulate: %s", err)
+	}
+	accumulated := NewSummaryAllocationSetRange(result)
+	return accumulated, nil
+}
+
+func (sasr *SummaryAllocationSetRange) accumulateByHour() (*SummaryAllocationSetRange, error) {
+	// ensure that the summary allocation sets have a 1-hour window, if a set exists
+	if len(sasr.SummaryAllocationSets) > 0 && sasr.SummaryAllocationSets[0].Window.Duration() != time.Hour {
+		return nil, fmt.Errorf("window duration must equal 1 hour; got:%s", sasr.SummaryAllocationSets[0].Window.Duration())
+	}
+
+	result, err := sasr.clone()
+	return result, err
+}
+
+func (sasr *SummaryAllocationSetRange) accumulateByDay() (*SummaryAllocationSetRange, error) {
+	// if the summary allocation set window is 1-day, just return the existing summary allocation set range
+	if len(sasr.SummaryAllocationSets) > 0 && sasr.SummaryAllocationSets[0].Window.Duration() == time.Hour*24 {
+		return sasr, nil
+	}
+
+	var toAccumulate *SummaryAllocationSetRange
+	result := NewSummaryAllocationSetRange()
+	for i, as := range sasr.SummaryAllocationSets {
+
+		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 = NewSummaryAllocationSetRange()
+			as = as.Clone()
+		}
+
+		err := toAccumulate.Append(as)
+		if err != nil {
+			return nil, fmt.Errorf("error building accumulation: %s", err)
+		}
+		sas, err := toAccumulate.accumulate()
+		if err != nil {
+			return nil, fmt.Errorf("error accumulating result: %s", err)
+		}
+		toAccumulate = NewSummaryAllocationSetRange(sas)
+
+		if hour == 23 || i == len(sasr.SummaryAllocationSets)-1 {
+			if length := len(toAccumulate.SummaryAllocationSets); length != 1 {
+				return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
+			}
+			err = result.Append(toAccumulate.SummaryAllocationSets[0])
+			if err != nil {
+				return nil, fmt.Errorf("error building result accumulation: %s", err)
+			}
+			toAccumulate = nil
+		}
+	}
+	return result, nil
+}
+
+func (sasr *SummaryAllocationSetRange) accumulateByMonth() (*SummaryAllocationSetRange, error) {
+	var toAccumulate *SummaryAllocationSetRange
+	result := NewSummaryAllocationSetRange()
+	for i, as := range sasr.SummaryAllocationSets {
+
+		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 = NewSummaryAllocationSetRange()
+			as = as.Clone()
+		}
+
+		err := toAccumulate.Append(as)
+		if err != nil {
+			return nil, fmt.Errorf("error building monthly accumulation: %s", err)
+		}
+
+		sas, err := toAccumulate.accumulate()
+		if err != nil {
+			return nil, fmt.Errorf("error building monthly accumulation: %s", err)
+		}
+
+		toAccumulate = NewSummaryAllocationSetRange(sas)
+
+		// either the month has ended, or there are no more summary allocation sets
+		if month != nextDayMonth || i == len(sasr.SummaryAllocationSets)-1 {
+			if length := len(toAccumulate.SummaryAllocationSets); length != 1 {
+				return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
+			}
+			err = result.Append(toAccumulate.SummaryAllocationSets[0])
+			if err != nil {
+				return nil, fmt.Errorf("error building result accumulation: %s", err)
+			}
+			toAccumulate = nil
+		}
+	}
+	return result, nil
+}
+
+func (sasr *SummaryAllocationSetRange) accumulateByWeek() (*SummaryAllocationSetRange, error) {
+	var toAccumulate *SummaryAllocationSetRange
+	result := NewSummaryAllocationSetRange()
+	for i, as := range sasr.SummaryAllocationSets {
+		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 = NewSummaryAllocationSetRange()
+			as = as.Clone()
+		}
+
+		err := toAccumulate.Append(as)
+		if err != nil {
+			return nil, fmt.Errorf("error building accumulation: %s", err)
+		}
+		sas, err := toAccumulate.accumulate()
+		if err != nil {
+			return nil, fmt.Errorf("error accumulating result: %s", err)
+		}
+		toAccumulate = NewSummaryAllocationSetRange(sas)
+
+		// current assumption is the week always ends on Saturday, or when there are no more summary allocation sets
+		if dayOfWeek == time.Saturday || i == len(sasr.SummaryAllocationSets)-1 {
+			if length := len(toAccumulate.SummaryAllocationSets); length != 1 {
+				return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
+			}
+			err = result.Append(toAccumulate.SummaryAllocationSets[0])
+			if err != nil {
+				return nil, fmt.Errorf("error building result accumulation: %s", err)
+			}
+			toAccumulate = nil
+		}
+	}
+	return result, nil
+}
+
+// clone returns a new SummaryAllocationSetRange cloned from the existing SASR
+func (sasr *SummaryAllocationSetRange) clone() (*SummaryAllocationSetRange, error) {
+	sasrSource := NewSummaryAllocationSetRange()
+	sasrSource.Window = sasr.Window.Clone()
+	sasrSource.Step = sasr.Step
+	sasrSource.Message = sasr.Message
+
+	for _, sas := range sasr.SummaryAllocationSets {
+		var sasClone *SummaryAllocationSet = nil
+		if sas != nil {
+			sasClone = sas.Clone()
+		}
+
+		err := sasrSource.Append(sasClone)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return sasrSource, nil
+
+}

+ 249 - 0
pkg/kubecost/summaryallocation_test.go

@@ -877,3 +877,252 @@ func TestSummaryAllocationSet_TotalEfficiency(t *testing.T) {
 		})
 	}
 }
+
+func TestSummaryAllocationSetRange_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)
+
+	ago4dSAS := NewMockUnitSummaryAllocationSet(ago4d, day)
+	ago4dSAS.Insert(NewMockUnitSummaryAllocation("4", ago4d, day, nil))
+	ago3dSAS := NewMockUnitSummaryAllocationSet(ago3d, day)
+	ago3dSAS.Insert(NewMockUnitSummaryAllocation("a", ago3d, day, nil))
+	ago2dSAS := NewMockUnitSummaryAllocationSet(ago2d, day)
+	ago2dSAS.Insert(NewMockUnitSummaryAllocation("", ago2d, day, nil))
+	yesterdaySAS := NewMockUnitSummaryAllocationSet(yesterday, day)
+	yesterdaySAS.Insert(NewMockUnitSummaryAllocation("", yesterday, day, nil))
+	todaySAS := NewMockUnitSummaryAllocationSet(today, day)
+	todaySAS.Insert(NewMockUnitSummaryAllocation("", today, day, nil))
+
+	asr := NewSummaryAllocationSetRange(ago4dSAS, ago3dSAS, ago2dSAS, yesterdaySAS, todaySAS)
+	asr, err := asr.Accumulate(AccumulateOptionNone)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.SummaryAllocationSets) != 5 {
+		t.Fatalf("expected 5 allocation sets, got:%d", len(asr.SummaryAllocationSets))
+	}
+}
+
+func TestSummaryAllocationSetRange_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)
+
+	ago4dSAS := NewMockUnitSummaryAllocationSet(ago4d, day)
+	ago4dSAS.Insert(NewMockUnitSummaryAllocation("4", ago4d, day, nil))
+	ago3dSAS := NewMockUnitSummaryAllocationSet(ago3d, day)
+	ago3dSAS.Insert(NewMockUnitSummaryAllocation("a", ago3d, day, nil))
+	ago2dSAS := NewMockUnitSummaryAllocationSet(ago2d, day)
+	ago2dSAS.Insert(NewMockUnitSummaryAllocation("", ago2d, day, nil))
+	yesterdaySAS := NewMockUnitSummaryAllocationSet(yesterday, day)
+	yesterdaySAS.Insert(NewMockUnitSummaryAllocation("", yesterday, day, nil))
+	todaySAS := NewMockUnitSummaryAllocationSet(today, day)
+	todaySAS.Insert(NewMockUnitSummaryAllocation("", today, day, nil))
+
+	asr := NewSummaryAllocationSetRange(ago4dSAS, ago3dSAS, ago2dSAS, yesterdaySAS, todaySAS)
+	asr, err := asr.Accumulate(AccumulateOptionAll)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.SummaryAllocationSets) != 1 {
+		t.Fatalf("expected 1 allocation set, got:%d", len(asr.SummaryAllocationSets))
+	}
+
+	allocMap := asr.SummaryAllocationSets[0].SummaryAllocations
+	alloc := allocMap["cluster1/namespace1/pod1/container1"]
+	if alloc.Minutes() != 4320.0 {
+		t.Errorf("accumulating AllocationSetRange: expected %f minutes; actual %f", 4320.0, alloc.Minutes())
+	}
+}
+
+func TestSummaryAllocationSetRange_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 := NewMockUnitSummaryAllocationSet(ago4h, time.Hour)
+	ago4hAS.Insert(NewMockUnitSummaryAllocation("4", ago4h, time.Hour, nil))
+	ago3hAS := NewMockUnitSummaryAllocationSet(ago3h, time.Hour)
+	ago3hAS.Insert(NewMockUnitSummaryAllocation("a", ago3h, time.Hour, nil))
+	ago2hAS := NewMockUnitSummaryAllocationSet(ago2h, time.Hour)
+	ago2hAS.Insert(NewMockUnitSummaryAllocation("", ago2h, time.Hour, nil))
+	ago1hAS := NewMockUnitSummaryAllocationSet(ago1h, time.Hour)
+	ago1hAS.Insert(NewMockUnitSummaryAllocation("", ago1h, time.Hour, nil))
+	currentHourAS := NewMockUnitSummaryAllocationSet(currentHour, time.Hour)
+	currentHourAS.Insert(NewMockUnitSummaryAllocation("", currentHour, time.Hour, nil))
+
+	asr := NewSummaryAllocationSetRange(ago4hAS, ago3hAS, ago2hAS, ago1hAS, currentHourAS)
+	asr, err := asr.Accumulate(AccumulateOptionHour)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.SummaryAllocationSets) != 5 {
+		t.Fatalf("expected 5 allocation sets, got:%d", len(asr.SummaryAllocationSets))
+	}
+
+	allocMap := asr.SummaryAllocationSets[0].SummaryAllocations
+	alloc := allocMap["4"]
+	if alloc.Minutes() != 60.0 {
+		t.Errorf("accumulating AllocationSetRange: expected %f minutes; actual %f", 60.0, alloc.Minutes())
+	}
+}
+
+func TestSummaryAllocationSetRange_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)
+
+	ago4dSAS := NewMockUnitSummaryAllocationSet(ago4d, day)
+	ago4dSAS.Insert(NewMockUnitSummaryAllocation("4", ago4d, day, nil))
+	ago3dSAS := NewMockUnitSummaryAllocationSet(ago3d, day)
+	ago3dSAS.Insert(NewMockUnitSummaryAllocation("a", ago3d, day, nil))
+	ago2dSAS := NewMockUnitSummaryAllocationSet(ago2d, day)
+	ago2dSAS.Insert(NewMockUnitSummaryAllocation("", ago2d, day, nil))
+	yesterdaySAS := NewMockUnitSummaryAllocationSet(yesterday, day)
+	yesterdaySAS.Insert(NewMockUnitSummaryAllocation("", yesterday, day, nil))
+	todaySAS := NewMockUnitSummaryAllocationSet(today, day)
+	todaySAS.Insert(NewMockUnitSummaryAllocation("", today, day, nil))
+
+	asr := NewSummaryAllocationSetRange(ago4dSAS, ago3dSAS, ago2dSAS, yesterdaySAS, todaySAS)
+	asr, err := asr.Accumulate(AccumulateOptionNone)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.SummaryAllocationSets) != 5 {
+		t.Fatalf("expected 5 allocation sets, got:%d", len(asr.SummaryAllocationSets))
+	}
+	allocMap := asr.SummaryAllocationSets[0].SummaryAllocations
+	alloc := allocMap["4"]
+	if alloc.Minutes() != 1440.0 {
+		t.Errorf("accumulating AllocationSetRange: expected %f minutes; actual %f", 1440.0, alloc.Minutes())
+	}
+}
+
+func TestSummaryAllocationSetRange_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 := NewMockUnitSummaryAllocationSet(ago4h, time.Hour)
+	ago4hAS.Insert(NewMockUnitSummaryAllocation("", ago4h, time.Hour, nil))
+	ago3hAS := NewMockUnitSummaryAllocationSet(ago3h, time.Hour)
+	ago3hAS.Insert(NewMockUnitSummaryAllocation("", ago3h, time.Hour, nil))
+	ago2hAS := NewMockUnitSummaryAllocationSet(ago2h, time.Hour)
+	ago2hAS.Insert(NewMockUnitSummaryAllocation("", ago2h, time.Hour, nil))
+	ago1hAS := NewMockUnitSummaryAllocationSet(ago1h, time.Hour)
+	ago1hAS.Insert(NewMockUnitSummaryAllocation("", ago1h, time.Hour, nil))
+	currentHourAS := NewMockUnitSummaryAllocationSet(currentHour, time.Hour)
+	currentHourAS.Insert(NewMockUnitSummaryAllocation("", currentHour, time.Hour, nil))
+
+	asr := NewSummaryAllocationSetRange(ago4hAS, ago3hAS, ago2hAS, ago1hAS, currentHourAS)
+	asr, err := asr.Accumulate(AccumulateOptionDay)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.SummaryAllocationSets) != 1 && len(asr.SummaryAllocationSets) != 2 {
+		t.Fatalf("expected 1 allocation set, got:%d", len(asr.SummaryAllocationSets))
+	}
+
+	allocMap := asr.SummaryAllocationSets[0].SummaryAllocations
+	alloc := allocMap["cluster1/namespace1/pod1/container1"]
+	if alloc.Minutes() > 300.0 {
+		t.Errorf("accumulating AllocationSetRange: expected %f or less minutes; actual %f", 300.0, alloc.Minutes())
+	}
+}
+
+func TestSummaryAllocationSetRange_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 := NewMockUnitSummaryAllocationSet(ago9d, day)
+	ago9dAS.Insert(NewMockUnitSummaryAllocation("4", ago9d, day, nil))
+	ago8dAS := NewMockUnitSummaryAllocationSet(ago8d, day)
+	ago8dAS.Insert(NewMockUnitSummaryAllocation("4", ago8d, day, nil))
+	ago7dAS := NewMockUnitSummaryAllocationSet(ago7d, day)
+	ago7dAS.Insert(NewMockUnitSummaryAllocation("4", ago7d, day, nil))
+	ago6dAS := NewMockUnitSummaryAllocationSet(ago6d, day)
+	ago6dAS.Insert(NewMockUnitSummaryAllocation("4", ago6d, day, nil))
+	ago5dAS := NewMockUnitSummaryAllocationSet(ago5d, day)
+	ago5dAS.Insert(NewMockUnitSummaryAllocation("4", ago5d, day, nil))
+	ago4dAS := NewMockUnitSummaryAllocationSet(ago4d, day)
+	ago4dAS.Insert(NewMockUnitSummaryAllocation("4", ago4d, day, nil))
+	ago3dAS := NewMockUnitSummaryAllocationSet(ago3d, day)
+	ago3dAS.Insert(NewMockUnitSummaryAllocation("4", ago3d, day, nil))
+	ago2dAS := NewMockUnitSummaryAllocationSet(ago2d, day)
+	ago2dAS.Insert(NewMockUnitSummaryAllocation("4", ago2d, day, nil))
+	yesterdayAS := NewMockUnitSummaryAllocationSet(yesterday, day)
+	yesterdayAS.Insert(NewMockUnitSummaryAllocation("", yesterday, day, nil))
+	todayAS := NewMockUnitSummaryAllocationSet(today, day)
+	todayAS.Insert(NewMockUnitSummaryAllocation("4", today, day, nil))
+
+	asr := NewSummaryAllocationSetRange(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.SummaryAllocationSets) != 2 && len(asr.SummaryAllocationSets) != 3 {
+		t.Fatalf("expected 2 allocation sets, got:%d", len(asr.SummaryAllocationSets))
+	}
+
+	for _, as := range asr.SummaryAllocationSets {
+		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 TestSummaryAllocationSetRange_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 := NewMockUnitSummaryAllocationSet(prevMonth1stDay, day)
+	prev1AS.Insert(NewMockUnitSummaryAllocation("", prevMonth1stDay, day, nil))
+	prev2AS := NewMockUnitSummaryAllocationSet(prevMonth2ndDay, day)
+	prev2AS.Insert(NewMockUnitSummaryAllocation("", prevMonth2ndDay, day, nil))
+	prev3AS := NewMockUnitSummaryAllocationSet(prevMonth3ndDay, day)
+	prev3AS.Insert(NewMockUnitSummaryAllocation("", prevMonth3ndDay, day, nil))
+	nextAS := NewMockUnitSummaryAllocationSet(nextMonth1stDay, day)
+	nextAS.Insert(NewMockUnitSummaryAllocation("", nextMonth1stDay, day, nil))
+	asr := NewSummaryAllocationSetRange(prev1AS, prev2AS, prev3AS, nextAS)
+	asr, err := asr.Accumulate(AccumulateOptionMonth)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.SummaryAllocationSets) != 2 {
+		t.Fatalf("expected 2 allocation sets, got:%d", len(asr.SummaryAllocationSets))
+	}
+
+	for _, as := range asr.SummaryAllocationSets {
+		if as.Window.Duration() < time.Hour*24 || as.Window.Duration() > time.Hour*24*31 {
+			t.Fatalf("expected window duration to be between 1 and 7 days, got:%s", as.Window.Duration().String())
+		}
+	}
+}