فهرست منبع

Refactor aggby test to use subtest

Sean Holcomb 5 سال پیش
والد
کامیت
c3b821ebfb
1فایلهای تغییر یافته به همراه787 افزوده شده و 599 حذف شده
  1. 787 599
      pkg/kubecost/allocation_test.go

+ 787 - 599
pkg/kubecost/allocation_test.go

@@ -1054,6 +1054,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 	idleTotalCost := 30.0
 	sharedOverheadHourlyCost := 7.0
 
+	// Match Functions
 	isNamespace3 := func(a *Allocation) bool {
 		ns := a.Properties.Namespace
 		return ns == "namespace3"
@@ -1067,328 +1068,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 		return false
 	}
 
-	end := time.Now().UTC().Truncate(day)
-	start := end.Add(-day)
-
-	// Tests:
-
-	// 1  Single-aggregation
-
-	// 1a AggregationProperties=(Cluster)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationClusterProp}, nil)
-	assertAllocationSetTotals(t, as, "1a", err, numClusters+numIdle, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "1a", map[string]float64{
-		"cluster1": 46.00,
-		"cluster2": 36.00,
-		IdleSuffix: 30.00,
-	})
-	assertAllocationWindow(t, as, "1a", startYesterday, endYesterday, 1440.0)
-
-	// 1b AggregationProperties=(Namespace)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationNamespaceProp}, nil)
-	assertAllocationSetTotals(t, as, "1b", err, numNamespaces+numIdle, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "1b", map[string]float64{
-		"namespace1": 28.00,
-		"namespace2": 36.00,
-		"namespace3": 18.00,
-		IdleSuffix:   30.00,
-	})
-	assertAllocationWindow(t, as, "1b", startYesterday, endYesterday, 1440.0)
-
-	// 1c AggregationProperties=(Pod)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationPodProp}, nil)
-	assertAllocationSetTotals(t, as, "1c", err, numPods+numIdle, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "1c", map[string]float64{
-		"pod-jkl":  6.00,
-		"pod-stu":  6.00,
-		"pod-abc":  6.00,
-		"pod-pqr":  6.00,
-		"pod-def":  6.00,
-		"pod-vwx":  12.00,
-		"pod1":     16.00,
-		"pod-mno":  12.00,
-		"pod-ghi":  12.00,
-		IdleSuffix: 30.00,
-	})
-	assertAllocationWindow(t, as, "1c", startYesterday, endYesterday, 1440.0)
-
-	// 1d AggregationProperties=(Container)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationContainerProp}, nil)
-	assertAllocationSetTotals(t, as, "1d", err, numContainers+numIdle, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "1d", map[string]float64{
-		"container2": 6.00,
-		"container9": 6.00,
-		"container6": 12.00,
-		"container3": 6.00,
-		"container4": 12.00,
-		"container7": 6.00,
-		"container8": 6.00,
-		"container5": 12.00,
-		"container1": 16.00,
-		IdleSuffix:   30.00,
-	})
-	assertAllocationWindow(t, as, "1d", startYesterday, endYesterday, 1440.0)
-
-	// 1e AggregationProperties=(ControllerKind)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationControllerKindProp}, nil)
-	assertAllocationSetTotals(t, as, "1e", err, numControllerKinds+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "1e", map[string]float64{
-		"daemonset":       12.00,
-		"deployment":      42.00,
-		"statefulset":     12.00,
-		IdleSuffix:        30.00,
-		UnallocatedSuffix: 16.00,
-	})
-	assertAllocationWindow(t, as, "1e", startYesterday, endYesterday, 1440.0)
-
-	// 1f AggregationProperties=(Controller)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationControllerProp}, nil)
-	assertAllocationSetTotals(t, as, "1f", err, numControllers+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "1f", map[string]float64{
-		"deployment:deployment2":   24.00,
-		"daemonset:daemonset1":     12.00,
-		"deployment:deployment3":   6.00,
-		"statefulset:statefulset1": 12.00,
-		"deployment:deployment1":   12.00,
-		IdleSuffix:                 30.00,
-		UnallocatedSuffix:          16.00,
-	})
-	assertAllocationWindow(t, as, "1f", startYesterday, endYesterday, 1440.0)
-
-	// 1g AggregationProperties=(Service)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationServiceProp}, nil)
-	assertAllocationSetTotals(t, as, "1g", err, numServices+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "1g", map[string]float64{
-		"service1":        12.00,
-		IdleSuffix:        30.00,
-		UnallocatedSuffix: 70.00,
-	})
-	assertAllocationWindow(t, as, "1g", startYesterday, endYesterday, 1440.0)
-
-	// 1h AggregationProperties=(Label:app)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{"label:app"}, nil)
-	assertAllocationSetTotals(t, as, "1h", err, numLabelApps+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "1h", map[string]float64{
-		"app=app1":        16.00,
-		"app=app2":        24.00,
-		IdleSuffix:        30.00,
-		UnallocatedSuffix: 42.00,
-	})
-	assertAllocationWindow(t, as, "1h", startYesterday, endYesterday, 1440.0)
-
-	// 1i AggregationProperties=(deployment)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationDeploymentProp}, nil)
-	assertAllocationSetTotals(t, as, "1i", err, 3+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "1i", map[string]float64{
-		"deployment1":     12.00,
-		"deployment2":     24.00,
-		"deployment3":     6.00,
-		IdleSuffix:        30.00,
-		UnallocatedSuffix: 40.00,
-	})
-	assertAllocationWindow(t, as, "1i", startYesterday, endYesterday, 1440.0)
-
-	// 1j AggregationProperties=(Annotation:team)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{"annotation:team"}, nil)
-	assertAllocationSetTotals(t, as, "1j", err, 2+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "1j", map[string]float64{
-		"team=team1":      12.00,
-		"team=team2":      6.00,
-		IdleSuffix:        30.00,
-		UnallocatedSuffix: 64.00,
-	})
-	assertAllocationWindow(t, as, "1j", startYesterday, endYesterday, 1440.0)
-
-	// 1k AggregationProperties=(daemonSet)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationDaemonSetProp}, nil)
-	assertAllocationSetTotals(t, as, "1k", err, 1+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "1k", map[string]float64{
-		"daemonset1":      12.00,
-		IdleSuffix:        30.00,
-		UnallocatedSuffix: 70.00,
-	})
-	assertAllocationWindow(t, as, "1k", startYesterday, endYesterday, 1440.0)
-
-	// 1l AggregationProperties=(statefulSet)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationStatefulSetProp}, nil)
-	assertAllocationSetTotals(t, as, "1l", err, 1+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "1l", map[string]float64{
-		"statefulset1":    12.00,
-		IdleSuffix:        30.00,
-		UnallocatedSuffix: 70.00,
-	})
-	assertAllocationWindow(t, as, "1l", startYesterday, endYesterday, 1440.0)
-
-	// 2  Multi-aggregation
-
-	// 2a AggregationProperties=(Cluster, Namespace)
-	// 2b AggregationProperties=(Namespace, Label:app)
-	// 2c AggregationProperties=(Cluster, Namespace, Pod, Container)
-
-	// 2d AggregationProperties=(Label:app, Label:environment)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{"label:app;env"}, nil)
-	// sets should be {idle, unallocated, app1/env1, app2/env2, app2/unallocated}
-	assertAllocationSetTotals(t, as, "2d", err, numIdle+numUnallocated+3, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "2d", map[string]float64{
-		"app=app1/env=env1":             16.00,
-		"app=app2/env=env2":             12.00,
-		"app=app2/" + UnallocatedSuffix: 12.00,
-		IdleSuffix:                      30.00,
-		UnallocatedSuffix:               42.00,
-	})
-
-	// 2e AggregationProperties=(Cluster, Label:app, Label:environment)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationClusterProp, "label:app;env"}, nil)
-	assertAllocationSetTotals(t, as, "2e", err, 6, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "2e", map[string]float64{
-		"cluster1/app=app2/env=env2":             12.00,
-		"__idle__":                               30.00,
-		"cluster1/app=app1/env=env1":             16.00,
-		"cluster1/" + UnallocatedSuffix:          18.00,
-		"cluster2/app=app2/" + UnallocatedSuffix: 12.00,
-		"cluster2/" + UnallocatedSuffix:          24.00,
-	})
-
-	// 2f AggregationProperties=(annotation:team, pod)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationPodProp, "annotation:team"}, nil)
-	assertAllocationSetTotals(t, as, "2f", err, 11, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "2f", map[string]float64{
-		"pod-jkl/" + UnallocatedSuffix: 6.00,
-		"pod-stu/team=team1":           6.00,
-		"pod-abc/" + UnallocatedSuffix: 6.00,
-		"pod-pqr/" + UnallocatedSuffix: 6.00,
-		"pod-def/" + UnallocatedSuffix: 6.00,
-		"pod-vwx/team=team1":           6.00,
-		"pod-vwx/team=team2":           6.00,
-		"pod1/" + UnallocatedSuffix:    16.00,
-		"pod-mno/" + UnallocatedSuffix: 12.00,
-		"pod-ghi/" + UnallocatedSuffix: 12.00,
-		IdleSuffix:                     30.00,
-	})
-
-	// // TODO niko/etl
-
-	// // 3  Share idle
-
-	// 3a AggregationProperties=(Namespace) ShareIdle=ShareWeighted
-	// namespace1: 42.6875 = 28.00 + 5.00*(3.00/6.00) + 15.0*(13.0/16.0)
-	// namespace2: 46.3125 = 36.00 + 5.0*(3.0/6.0) + 15.0*(3.0/16.0) + 5.0*(3.0/6.0) + 5.0*(3.0/6.0)
-	// namespace3: 23.0000 = 18.00 + 5.0*(3.0/6.0) + 5.0*(3.0/6.0)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{ShareIdle: ShareWeighted})
-	assertAllocationSetTotals(t, as, "3a", err, numNamespaces, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "3a", map[string]float64{
-		"namespace1": 42.69,
-		"namespace2": 46.31,
-		"namespace3": 23.00,
-	})
-	assertAllocationWindow(t, as, "3a", startYesterday, endYesterday, 1440.0)
-
-	// 3b AggregationProperties=(Namespace) ShareIdle=ShareEven
-	// namespace1: 38.0000 = 28.00 + 5.00*(1.0/2.0) + 15.0*(1.0/2.0)
-	// namespace2: 51.0000 = 36.00 + 5.0*(1.0/2.0) + 15.0*(1.0/2.0) + 5.0*(1.0/2.0) + 5.0*(1.0/2.0)
-	// namespace3: 23.0000 = 18.00 + 5.0*(1.0/2.0) + 5.0*(1.0/2.0)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{ShareIdle: ShareEven})
-	assertAllocationSetTotals(t, as, "3a", err, numNamespaces, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "3a", map[string]float64{
-		"namespace1": 38.00,
-		"namespace2": 51.00,
-		"namespace3": 23.00,
-	})
-	assertAllocationWindow(t, as, "3b", startYesterday, endYesterday, 1440.0)
-
-	// 4  Share resources
-
-	// 4a Share namespace ShareEven
-	// namespace1: 37.5000 = 28.00 + 18.00*(1.0/2.0)
-	// namespace2: 45.5000 = 36.00 + 18.00*(1.0/2.0)
-	// idle:       30.0000
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
-		ShareFuncs: []AllocationMatchFunc{isNamespace3},
-		ShareSplit: ShareEven,
-	})
-	assertAllocationSetTotals(t, as, "4a", err, numNamespaces, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "4a", map[string]float64{
-		"namespace1": 37.00,
-		"namespace2": 45.00,
-		IdleSuffix:   30.00,
-	})
-	assertAllocationWindow(t, as, "4a", startYesterday, endYesterday, 1440.0)
-
-	// 4b Share namespace ShareWeighted
-	// namespace1: 32.5000 =
-	// namespace2: 37.5000 =
-	// idle:       30.0000
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
-		ShareFuncs: []AllocationMatchFunc{isNamespace3},
-		ShareSplit: ShareWeighted,
-	})
-	assertAllocationSetTotals(t, as, "4b", err, numNamespaces, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "4b", map[string]float64{
-		"namespace1": 35.88,
-		"namespace2": 46.125,
-		IdleSuffix:   30.00,
-	})
-	assertAllocationWindow(t, as, "4b", startYesterday, endYesterday, 1440.0)
-
-	// 4c Share label ShareEven
-	// namespace1: 17.3333 = 28.00 - 16.00 + 16.00*(1.0/3.0)
-	// namespace2: 41.3333 = 36.00 + 16.00*(1.0/3.0)
-	// namespace3: 23.3333 = 18.00 + 16.00*(1.0/3.0)
-	// idle:       30.0000
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
-		ShareFuncs: []AllocationMatchFunc{isApp1},
-		ShareSplit: ShareEven,
-	})
-	assertAllocationSetTotals(t, as, "4c", err, numNamespaces+numIdle, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "4c", map[string]float64{
-		"namespace1": 17.33,
-		"namespace2": 41.33,
-		"namespace3": 23.33,
-		IdleSuffix:   30.00,
-	})
-	assertAllocationWindow(t, as, "4c", startYesterday, endYesterday, 1440.0)
-
-	// 4d Share overhead ShareWeighted
-	// namespace1: 85.366 = 28.00 + (7.0*24.0)*(28.00/82.00)
-	// namespace2: 109.756 = 36.00 + (7.0*24.0)*(36.00/82.00)
-	// namespace3: 54.878 = 18.00 + (7.0*24.0)*(18.00/82.00)
-	// idle:       30.0000
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
-		SharedHourlyCosts: map[string]float64{"total": sharedOverheadHourlyCost},
-		ShareSplit:        ShareWeighted,
-	})
-	assertAllocationSetTotals(t, as, "4d", err, numNamespaces+numIdle, activeTotalCost+idleTotalCost+(sharedOverheadHourlyCost*24.0))
-	assertAllocationTotals(t, as, "4d", map[string]float64{
-		"namespace1": 85.366,
-		"namespace2": 109.756,
-		"namespace3": 54.878,
-		IdleSuffix:   30.00,
-	})
-	assertAllocationWindow(t, as, "4d", startYesterday, endYesterday, 1440.0)
-
-	// 5  Filters
-
+	// Filters
 	isCluster := func(matchCluster string) func(*Allocation) bool {
 		return func(a *Allocation) bool {
 			cluster := a.Properties.Cluster
@@ -1403,287 +1083,795 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 		}
 	}
 
-	// 5a Filter by cluster with separate idle
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationClusterProp}, &AllocationAggregationOptions{
-		FilterFuncs: []AllocationMatchFunc{isCluster("cluster1")},
-		ShareIdle:   ShareNone,
-	})
-	assertAllocationSetTotals(t, as, "5a", err, 2, 66.0)
-	assertAllocationTotals(t, as, "5a", map[string]float64{
-		"cluster1": 46.00,
-		IdleSuffix: 20.00,
-	})
-	assertAllocationWindow(t, as, "5a", startYesterday, endYesterday, 1440.0)
-
-	// 5b Filter by cluster with shared idle
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationClusterProp}, &AllocationAggregationOptions{
-		FilterFuncs: []AllocationMatchFunc{isCluster("cluster1")},
-		ShareIdle:   ShareWeighted,
-	})
-	assertAllocationSetTotals(t, as, "5b", err, 1, 66.0)
-	assertAllocationTotals(t, as, "5b", map[string]float64{
-		"cluster1": 66.00,
-	})
-	assertAllocationWindow(t, as, "5b", startYesterday, endYesterday, 1440.0)
-
-	// 5c Filter by cluster, agg by namespace, with separate idle
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
-		FilterFuncs: []AllocationMatchFunc{isCluster("cluster1")},
-		ShareIdle:   ShareNone,
-	})
-	assertAllocationSetTotals(t, as, "5c", err, 3, 66.0)
-	assertAllocationTotals(t, as, "5c", map[string]float64{
-		"namespace1": 28.00,
-		"namespace2": 18.00,
-		IdleSuffix:   20.00,
-	})
-	assertAllocationWindow(t, as, "5c", startYesterday, endYesterday, 1440.0)
-
-	// 5d Filter by namespace, agg by cluster, with separate idle
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationClusterProp}, &AllocationAggregationOptions{
-		FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
-		ShareIdle:   ShareNone,
-	})
-	assertAllocationSetTotals(t, as, "5d", err, 3, 46.31)
-	assertAllocationTotals(t, as, "5d", map[string]float64{
-		"cluster1": 18.00,
-		"cluster2": 18.00,
-		IdleSuffix: 10.31,
-	})
-	assertAllocationWindow(t, as, "5d", startYesterday, endYesterday, 1440.0)
-
-	// 6  Combinations and options
-
-	// 6a SplitIdle
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{SplitIdle: true})
-	assertAllocationSetTotals(t, as, "6a", err, numNamespaces+numSplitIdle, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "6a", map[string]float64{
-		"namespace1":                           28.00,
-		"namespace2":                           36.00,
-		"namespace3":                           18.00,
-		fmt.Sprintf("cluster1/%s", IdleSuffix): 20.00,
-		fmt.Sprintf("cluster2/%s", IdleSuffix): 10.00,
-	})
-	assertAllocationWindow(t, as, "6a", startYesterday, endYesterday, 1440.0)
+	end := time.Now().UTC().Truncate(day)
+	start := end.Add(-day)
 
-	// 6b Share idle weighted with filters
-	// Should match values from unfiltered aggregation (3a)
-	// namespace2: 46.3125 = 36.00 + 5.0*(3.0/6.0) + 15.0*(3.0/16.0) + 5.0*(3.0/6.0) + 5.0*(3.0/6.0)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
-		FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
-		ShareIdle:   ShareWeighted,
-	})
-	assertAllocationSetTotals(t, as, "6b", err, 1, 46.31)
-	assertAllocationTotals(t, as, "6b", map[string]float64{
-		"namespace2": 46.31,
-	})
-	assertAllocationWindow(t, as, "6b", startYesterday, endYesterday, 1440.0)
+	// Tests:
+	cases := map[string]struct {
+		start       time.Time
+		aggBy       []string
+		aggOpts     *AllocationAggregationOptions
+		numResults  int
+		totalCost   float64
+		results     map[string]float64
+		windowStart time.Time
+		windowEnd   time.Time
+		expMinutes  float64
+	}{
+		// 1  Single-aggregation
 
-	// 6c Share idle even with filters
-	// Should match values from unfiltered aggregation (3b)
-	// namespace2: 51.0000 = 36.00 + 5.0*(1.0/2.0) + 15.0*(1.0/2.0) + 5.0*(1.0/2.0) + 5.0*(1.0/2.0)
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
-		FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
-		ShareIdle:   ShareEven,
-	})
-	assertAllocationSetTotals(t, as, "6b", err, 1, 51.00)
-	assertAllocationTotals(t, as, "6b", map[string]float64{
-		"namespace2": 51.00,
-	})
-	assertAllocationWindow(t, as, "6b", startYesterday, endYesterday, 1440.0)
-
-	// 6d Share overhead with filters
-	// namespace1: 85.366 = 28.00 + (7.0*24.0)*(28.00/82.00)
-	// namespace2: 109.756 = 36.00 + (7.0*24.0)*(36.00/82.00)
-	// namespace3: 54.878 = 18.00 + (7.0*24.0)*(18.00/82.00)
-	// idle:       30.0000
-	// Then namespace 2 is filtered.
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
-		FilterFuncs:       []AllocationMatchFunc{isNamespace("namespace2")},
-		SharedHourlyCosts: map[string]float64{"total": sharedOverheadHourlyCost},
-		ShareSplit:        ShareWeighted,
-	})
-	assertAllocationSetTotals(t, as, "6d", err, 2, 139.756)
-	assertAllocationTotals(t, as, "6d", map[string]float64{
-		"namespace2": 109.756,
-		IdleSuffix:   30.00,
-	})
-	assertAllocationWindow(t, as, "6d", startYesterday, endYesterday, 1440.0)
-
-	// 6e Share resources with filters
-	// --- Shared ---
-	// namespace1: 28.00 (gets shared among namespace2 and namespace3)
-	// --- Filtered ---
-	// namespace3: 27.33 = 18.00 + (28.00)*(18.00/54.00) (filtered out)
-	// --- Results ---
-	// namespace2: 54.667 = 36.00 + (28.00)*(36.00/54.00)
-	// idle:       30.0000
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
-		FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
-		ShareFuncs:  []AllocationMatchFunc{isNamespace("namespace1")},
-		ShareSplit:  ShareWeighted,
-	})
-	assertAllocationSetTotals(t, as, "6e", err, 2, 84.667)
-	assertAllocationTotals(t, as, "6e", map[string]float64{
-		"namespace2": 54.667,
-		IdleSuffix:   30.00,
-	})
-	assertAllocationWindow(t, as, "6e", startYesterday, endYesterday, 1440.0)
-
-	// 6f Share idle weighted and share resources weighted
-	//
-	// First, share idle weighted produces:
-	//
-	// namespace1:      42.6875
-	//   initial cost   28.0000
-	//   cluster1.cpu    2.5000 = 5.00*(3.00/6.00)
-	//   cluster1.ram   12.1875 = 15.00*(13.0/16.0)
-	//
-	// namespace2:      46.3125
-	//   initial cost   36.0000
-	//   cluster1.cpu    2.5000 = 5.00*(3.0/6.0)
-	//   cluster1.ram    2.8125 = 15.00*(3.0/16.0)
-	//   cluster2.cpu    2.5000 = 5.00*(3.0/6.0)
-	//   cluster2.ram    2.5000 = 5.00*(3.0/6.0)
-	//
-	// namespace3:      23.0000
-	//   initial cost   18.0000
-	//   cluster2.cpu    2.5000 = 5.00*(3.0/6.0)
-	//   cluster2.ram    2.5000 = 5.00*(3.0/6.0)
-	//
-	// Then, sharing namespace1 means sharing 39.6875 according to coefficients
-	// computed before allocating idle (so that weighting idle differently
-	// doesn't adversely affect the sharing mechanism):
-	//
-	// namespace2:      74.7708
-	//   initial cost   30.0000
-	//   idle cost      10.3125
-	//   shared cost    28.4583 = (42.6875)*(36.0/54.0)
-	//
-	// namespace3:      37.2292
-	//   initial cost   18.0000
-	//   idle cost       5.0000
-	//   shared cost    14.2292 = (42.6875)*(18.0/54.0)
-	//
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
-		ShareFuncs: []AllocationMatchFunc{isNamespace("namespace1")},
-		ShareSplit: ShareWeighted,
-		ShareIdle:  ShareWeighted,
-	})
-	assertAllocationSetTotals(t, as, "6f", err, 2, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "6f", map[string]float64{
-		"namespace2": 74.77,
-		"namespace3": 37.23,
-	})
-	assertAllocationWindow(t, as, "6f", startYesterday, endYesterday, 1440.0)
-
-	// 6g Share idle, share resources, and filter
-	//
-	// First, share idle weighted produces:
-	//
-	// namespace1:      42.6875
-	//   initial cost   28.0000
-	//   cluster1.cpu    2.5000 = 5.00*(3.00/6.00)
-	//   cluster1.ram   12.1875 = 15.00*(13.0/16.0)
-	//
-	// namespace2:      46.3125
-	//   initial cost   36.0000
-	//   cluster1.cpu    2.5000 = 5.00*(3.0/6.0)
-	//   cluster1.ram    2.8125 = 15.00*(3.0/16.0)
-	//   cluster2.cpu    2.5000 = 5.00*(3.0/6.0)
-	//   cluster2.ram    2.5000 = 5.00*(3.0/6.0)
-	//
-	// namespace3:      23.0000
-	//   initial cost   18.0000
-	//   cluster2.cpu    2.5000 = 5.00*(3.0/6.0)
-	//   cluster2.ram    2.5000 = 5.00*(3.0/6.0)
-	//
-	// Then, sharing namespace1 means sharing 39.6875 according to coefficients
-	// computed before allocating idle (so that weighting idle differently
-	// doesn't adversely affect the sharing mechanism):
-	//
-	// namespace2:      74.7708
-	//   initial cost   36.0000
-	//   idle cost      10.3125
-	//   shared cost    28.4583 = (42.6875)*(36.0/54.0)
-	//
-	// namespace3:      37.2292
-	//   initial cost   18.0000
-	//   idle cost       5.0000
-	//   shared cost    14.2292 = (42.6875)*(18.0/54.0)
-	//
-	// Then, filter for namespace2: 74.7708
-	//
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
-		FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
-		ShareFuncs:  []AllocationMatchFunc{isNamespace("namespace1")},
-		ShareSplit:  ShareWeighted,
-		ShareIdle:   ShareWeighted,
-	})
-	assertAllocationSetTotals(t, as, "6g", err, 1, 74.77)
-	assertAllocationTotals(t, as, "6g", map[string]float64{
-		"namespace2": 74.77,
-	})
-	assertAllocationWindow(t, as, "6g", startYesterday, endYesterday, 1440.0)
-
-	// 6h Share idle, share resources, share overhead
-	//
-	// Share idle weighted:
-	//
-	// namespace1:      42.6875
-	//   initial cost   28.0000
-	//   cluster1.cpu    2.5000 = 5.00*(3.00/6.00)
-	//   cluster1.ram   12.1875 = 15.00*(13.0/16.0)
-	//
-	// namespace2:      46.3125
-	//   initial cost   36.0000
-	//   cluster1.cpu    2.5000 = 5.00*(3.0/6.0)
-	//   cluster1.ram    2.8125 = 15.00*(3.0/16.0)
-	//   cluster2.cpu    2.5000 = 5.00*(3.0/6.0)
-	//   cluster2.ram    2.5000 = 5.00*(3.0/6.0)
-	//
-	// namespace3:      23.0000
-	//   initial cost   18.0000
-	//   cluster2.cpu    2.5000 = 5.00*(3.0/6.0)
-	//   cluster2.ram    2.5000 = 5.00*(3.0/6.0)
-	//
-	// Then share overhead:
-	//
-	// namespace1:     100.0533 = 42.6875 + (7.0*24.0)*(28.00/82.00)
-	// namespace2:     120.0686 = 46.3125 + (7.0*24.0)*(36.00/82.00)
-	// namespace3:      59.8780 = 23.0000 + (7.0*24.0)*(18.00/82.00)
-	//
-	// Then namespace 2 is filtered.
-	as = generateAllocationSet(start)
-	err = as.AggregateBy([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
-		FilterFuncs:       []AllocationMatchFunc{isNamespace("namespace2")},
-		ShareSplit:        ShareWeighted,
-		ShareIdle:         ShareWeighted,
-		SharedHourlyCosts: map[string]float64{"total": sharedOverheadHourlyCost},
-	})
-	assertAllocationSetTotals(t, as, "6h", err, 1, 120.07)
-	assertAllocationTotals(t, as, "6h", map[string]float64{
-		"namespace2": 120.07,
-	})
-	assertAllocationWindow(t, as, "6h", startYesterday, endYesterday, 1440.0)
+		// 1a AggregationProperties=(Cluster)
+		"1a": {
+			start:      start,
+			aggBy:      []string{AllocationClusterProp},
+			aggOpts:    nil,
+			numResults: numClusters + numIdle,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"cluster1": 46.00,
+				"cluster2": 36.00,
+				IdleSuffix: 30.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 1b AggregationProperties=(Namespace)
+		"1b": {
+			start:      start,
+			aggBy:      []string{AllocationNamespaceProp},
+			aggOpts:    nil,
+			numResults: numNamespaces + numIdle,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"namespace1": 28.00,
+				"namespace2": 36.00,
+				"namespace3": 18.00,
+				IdleSuffix:   30.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 1c AggregationProperties=(Pod)
+		"1c": {
+			start:      start,
+			aggBy:      []string{AllocationPodProp},
+			aggOpts:    nil,
+			numResults: numPods + numIdle,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"pod-jkl":  6.00,
+				"pod-stu":  6.00,
+				"pod-abc":  6.00,
+				"pod-pqr":  6.00,
+				"pod-def":  6.00,
+				"pod-vwx":  12.00,
+				"pod1":     16.00,
+				"pod-mno":  12.00,
+				"pod-ghi":  12.00,
+				IdleSuffix: 30.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 1d AggregationProperties=(Container)
+		"1d": {
+			start:      start,
+			aggBy:      []string{AllocationContainerProp},
+			aggOpts:    nil,
+			numResults: numContainers + numIdle,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"container2": 6.00,
+				"container9": 6.00,
+				"container6": 12.00,
+				"container3": 6.00,
+				"container4": 12.00,
+				"container7": 6.00,
+				"container8": 6.00,
+				"container5": 12.00,
+				"container1": 16.00,
+				IdleSuffix:   30.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 1e AggregationProperties=(ControllerKind)
+		"1e": {
+			start:      start,
+			aggBy:      []string{AllocationControllerKindProp},
+			aggOpts:    nil,
+			numResults: numControllerKinds + numIdle + numUnallocated,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"daemonset":       12.00,
+				"deployment":      42.00,
+				"statefulset":     12.00,
+				IdleSuffix:        30.00,
+				UnallocatedSuffix: 16.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 1f AggregationProperties=(Controller)
+		"1f": {
+			start:      start,
+			aggBy:      []string{AllocationControllerProp},
+			aggOpts:    nil,
+			numResults: numControllers + numIdle + numUnallocated,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"deployment:deployment2":   24.00,
+				"daemonset:daemonset1":     12.00,
+				"deployment:deployment3":   6.00,
+				"statefulset:statefulset1": 12.00,
+				"deployment:deployment1":   12.00,
+				IdleSuffix:                 30.00,
+				UnallocatedSuffix:          16.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 1g AggregationProperties=(Service)
+		"1g": {
+			start:      start,
+			aggBy:      []string{AllocationServiceProp},
+			aggOpts:    nil,
+			numResults: numServices + numIdle + numUnallocated,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"service1":        12.00,
+				IdleSuffix:        30.00,
+				UnallocatedSuffix: 70.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 1h AggregationProperties=(Label:app)
+		"1h": {
+			start:      start,
+			aggBy:      []string{"label:app"},
+			aggOpts:    nil,
+			numResults: numLabelApps + numIdle + numUnallocated,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"app=app1":        16.00,
+				"app=app2":        24.00,
+				IdleSuffix:        30.00,
+				UnallocatedSuffix: 42.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 1i AggregationProperties=(deployment)
+		"1i": {
+			start:      start,
+			aggBy:      []string{AllocationDeploymentProp},
+			aggOpts:    nil,
+			numResults: 3 + numIdle + numUnallocated,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"deployment1":     12.00,
+				"deployment2":     24.00,
+				"deployment3":     6.00,
+				IdleSuffix:        30.00,
+				UnallocatedSuffix: 40.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 1j AggregationProperties=(Annotation:team)
+		"1j": {
+			start:      start,
+			aggBy:      []string{"annotation:team"},
+			aggOpts:    nil,
+			numResults: 2 + numIdle + numUnallocated,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"team=team1":      12.00,
+				"team=team2":      6.00,
+				IdleSuffix:        30.00,
+				UnallocatedSuffix: 64.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 1k AggregationProperties=(daemonSet)
+		"1k": {
+			start:      start,
+			aggBy:      []string{AllocationDaemonSetProp},
+			aggOpts:    nil,
+			numResults: 1 + numIdle + numUnallocated,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"daemonset1":      12.00,
+				IdleSuffix:        30.00,
+				UnallocatedSuffix: 70.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 1l AggregationProperties=(statefulSet)
+		"1l": {
+			start:      start,
+			aggBy:      []string{AllocationStatefulSetProp},
+			aggOpts:    nil,
+			numResults: 1 + numIdle + numUnallocated,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"statefulset1":    12.00,
+				IdleSuffix:        30.00,
+				UnallocatedSuffix: 70.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 2  Multi-aggregation
+
+		// 2a AggregationProperties=(Cluster, Namespace)
+		// 2b AggregationProperties=(Namespace, Label:app)
+		// 2c AggregationProperties=(Cluster, Namespace, Pod, Container)
+		// 2d AggregationProperties=(Label:app, Label:environment)
+		"2d": {
+			start:      start,
+			aggBy:      []string{"label:app;env"},
+			aggOpts:    nil,
+			numResults: 3 + numIdle + numUnallocated,
+			totalCost:  activeTotalCost + idleTotalCost,
+			// sets should be {idle, unallocated, app1/env1, app2/env2, app2/unallocated}
+			results: map[string]float64{
+				"app=app1/env=env1":             16.00,
+				"app=app2/env=env2":             12.00,
+				"app=app2/" + UnallocatedSuffix: 12.00,
+				IdleSuffix:                      30.00,
+				UnallocatedSuffix:               42.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 2e AggregationProperties=(Cluster, Label:app, Label:environment)
+		"2e": {
+			start:      start,
+			aggBy:      []string{AllocationClusterProp, "label:app;env"},
+			aggOpts:    nil,
+			numResults: 6,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"cluster1/app=app2/env=env2":             12.00,
+				"__idle__":                               30.00,
+				"cluster1/app=app1/env=env1":             16.00,
+				"cluster1/" + UnallocatedSuffix:          18.00,
+				"cluster2/app=app2/" + UnallocatedSuffix: 12.00,
+				"cluster2/" + UnallocatedSuffix:          24.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 2f AggregationProperties=(annotation:team, pod)
+		"2f": {
+			start:      start,
+			aggBy:      []string{AllocationPodProp, "annotation:team"},
+			aggOpts:    nil,
+			numResults: 11,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"pod-jkl/" + UnallocatedSuffix: 6.00,
+				"pod-stu/team=team1":           6.00,
+				"pod-abc/" + UnallocatedSuffix: 6.00,
+				"pod-pqr/" + UnallocatedSuffix: 6.00,
+				"pod-def/" + UnallocatedSuffix: 6.00,
+				"pod-vwx/team=team1":           6.00,
+				"pod-vwx/team=team2":           6.00,
+				"pod1/" + UnallocatedSuffix:    16.00,
+				"pod-mno/" + UnallocatedSuffix: 12.00,
+				"pod-ghi/" + UnallocatedSuffix: 12.00,
+				IdleSuffix:                     30.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 3  Share idle
+
+		// 3a AggregationProperties=(Namespace) ShareIdle=ShareWeighted
+		// namespace1: 42.6875 = 28.00 + 5.00*(3.00/6.00) + 15.0*(13.0/16.0)
+		// namespace2: 46.3125 = 36.00 + 5.0*(3.0/6.0) + 15.0*(3.0/16.0) + 5.0*(3.0/6.0) + 5.0*(3.0/6.0)
+		// namespace3: 23.0000 = 18.00 + 5.0*(3.0/6.0) + 5.0*(3.0/6.0)
+		"3a": {
+			start:      start,
+			aggBy:      []string{AllocationNamespaceProp},
+			aggOpts:    &AllocationAggregationOptions{ShareIdle: ShareWeighted},
+			numResults: numNamespaces,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"namespace1": 42.69,
+				"namespace2": 46.31,
+				"namespace3": 23.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 3b AggregationProperties=(Namespace) ShareIdle=ShareEven
+		// namespace1: 38.0000 = 28.00 + 5.00*(1.0/2.0) + 15.0*(1.0/2.0)
+		// namespace2: 51.0000 = 36.00 + 5.0*(1.0/2.0) + 15.0*(1.0/2.0) + 5.0*(1.0/2.0) + 5.0*(1.0/2.0)
+		// namespace3: 23.0000 = 18.00 + 5.0*(1.0/2.0) + 5.0*(1.0/2.0)
+		"3b": {
+			start:      start,
+			aggBy:      []string{AllocationNamespaceProp},
+			aggOpts:    &AllocationAggregationOptions{ShareIdle: ShareEven},
+			numResults: numNamespaces,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"namespace1": 38.00,
+				"namespace2": 51.00,
+				"namespace3": 23.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 4  Share resources
+
+		// 4a Share namespace ShareEven
+		// namespace1: 37.5000 = 28.00 + 18.00*(1.0/2.0)
+		// namespace2: 45.5000 = 36.00 + 18.00*(1.0/2.0)
+		// idle:       30.0000
+		"4a": {
+			start:      start,
+			aggBy:      []string{AllocationNamespaceProp},
+			aggOpts:    &AllocationAggregationOptions{
+				ShareFuncs: []AllocationMatchFunc{isNamespace3},
+				ShareSplit: ShareEven,
+			},
+			numResults: numNamespaces,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"namespace1": 37.00,
+				"namespace2": 45.00,
+				IdleSuffix:   30.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 4b Share namespace ShareWeighted
+		// namespace1: 32.5000 =
+		// namespace2: 37.5000 =
+		// idle:       30.0000
+		"4b": {
+			start:      start,
+			aggBy:      []string{AllocationNamespaceProp},
+			aggOpts:    &AllocationAggregationOptions{
+				ShareFuncs: []AllocationMatchFunc{isNamespace3},
+				ShareSplit: ShareWeighted,
+			},
+			numResults: numNamespaces,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"namespace1": 35.88,
+				"namespace2": 46.125,
+				IdleSuffix:   30.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 4c Share label ShareEven
+		// namespace1: 17.3333 = 28.00 - 16.00 + 16.00*(1.0/3.0)
+		// namespace2: 41.3333 = 36.00 + 16.00*(1.0/3.0)
+		// namespace3: 23.3333 = 18.00 + 16.00*(1.0/3.0)
+		// idle:       30.0000
+		"4c": {
+			start:      start,
+			aggBy:      []string{AllocationNamespaceProp},
+			aggOpts:    &AllocationAggregationOptions{
+				ShareFuncs: []AllocationMatchFunc{isApp1},
+				ShareSplit: ShareEven,
+			},
+			numResults: numNamespaces + numIdle,
+			totalCost:  activeTotalCost + idleTotalCost,
+			results: map[string]float64{
+				"namespace1": 17.33,
+				"namespace2": 41.33,
+				"namespace3": 23.33,
+				IdleSuffix:   30.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 4d Share overhead ShareWeighted
+		// namespace1: 85.366 = 28.00 + (7.0*24.0)*(28.00/82.00)
+		// namespace2: 109.756 = 36.00 + (7.0*24.0)*(36.00/82.00)
+		// namespace3: 54.878 = 18.00 + (7.0*24.0)*(18.00/82.00)
+		// idle:       30.0000
+		"4d": {
+			start:      start,
+			aggBy:      []string{AllocationNamespaceProp},
+			aggOpts:    &AllocationAggregationOptions{
+				SharedHourlyCosts: map[string]float64{"total": sharedOverheadHourlyCost},
+				ShareSplit:        ShareWeighted,
+			},
+			numResults: numNamespaces + numIdle,
+			totalCost:  activeTotalCost+idleTotalCost+(sharedOverheadHourlyCost*24.0),
+			results: map[string]float64{
+				"namespace1": 85.366,
+				"namespace2": 109.756,
+				"namespace3": 54.878,
+				IdleSuffix:   30.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 5  Filters
+
+		// 5a Filter by cluster with separate idle
+		"5a": {
+			start:      start,
+			aggBy:      []string{AllocationClusterProp},
+			aggOpts:    &AllocationAggregationOptions{
+				FilterFuncs: []AllocationMatchFunc{isCluster("cluster1")},
+				ShareIdle:   ShareNone,
+			},
+			numResults: 1 + numIdle,
+			totalCost: 66.0,
+			results: map[string]float64{
+				"cluster1": 46.00,
+				IdleSuffix: 20.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 5b Filter by cluster with shared idle
+		"5b": {
+			start:      start,
+			aggBy:      []string{AllocationClusterProp},
+			aggOpts:    &AllocationAggregationOptions{
+				FilterFuncs: []AllocationMatchFunc{isCluster("cluster1")},
+				ShareIdle:   ShareWeighted,
+			},
+			numResults: 1,
+			totalCost: 66.0,
+			results: map[string]float64{
+				"cluster1": 66.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 5c Filter by cluster, agg by namespace, with separate idle
+		"5c": {
+			start:      start,
+			aggBy:      []string{AllocationNamespaceProp},
+			aggOpts:    &AllocationAggregationOptions{
+				FilterFuncs: []AllocationMatchFunc{isCluster("cluster1")},
+				ShareIdle:   ShareNone,
+			},
+			numResults: 2 + numIdle,
+			totalCost: 66.0,
+			results: map[string]float64{
+				"namespace1": 28.00,
+				"namespace2": 18.00,
+				IdleSuffix:   20.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 5d Filter by namespace, agg by cluster, with separate idle
+		"5d": {
+			start:      start,
+			aggBy:      []string{AllocationClusterProp},
+			aggOpts:    &AllocationAggregationOptions{
+				FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
+				ShareIdle:   ShareNone,
+			},
+			numResults: numClusters + numIdle,
+			totalCost: 46.31,
+			results: map[string]float64{
+				"cluster1": 18.00,
+				"cluster2": 18.00,
+				IdleSuffix: 10.31,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 6  Combinations and options
+
+		// 6a SplitIdle
+		"6a": {
+			start:      start,
+			aggBy:      []string{AllocationNamespaceProp},
+			aggOpts:    &AllocationAggregationOptions{
+				SplitIdle: true,
+			},
+			numResults: numNamespaces+numSplitIdle,
+			totalCost: activeTotalCost+idleTotalCost,
+			results: map[string]float64{
+				"namespace1":                           28.00,
+				"namespace2":                           36.00,
+				"namespace3":                           18.00,
+				fmt.Sprintf("cluster1/%s", IdleSuffix): 20.00,
+				fmt.Sprintf("cluster2/%s", IdleSuffix): 10.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 6b Share idle weighted with filters
+		// Should match values from unfiltered aggregation (3a)
+		// namespace2: 46.3125 = 36.00 + 5.0*(3.0/6.0) + 15.0*(3.0/16.0) + 5.0*(3.0/6.0) + 5.0*(3.0/6.0)
+		"6b": {
+			start:      start,
+			aggBy:      []string{AllocationNamespaceProp},
+			aggOpts:    &AllocationAggregationOptions{
+				FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
+				ShareIdle:   ShareWeighted,
+			},
+			numResults: 1,
+			totalCost: 46.31,
+			results: map[string]float64{
+				"namespace2": 46.31,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 6c Share idle even with filters
+		// Should match values from unfiltered aggregation (3b)
+		// namespace2: 51.0000 = 36.00 + 5.0*(1.0/2.0) + 15.0*(1.0/2.0) + 5.0*(1.0/2.0) + 5.0*(1.0/2.0)
+		"6c": {
+			start:      start,
+			aggBy:      []string{AllocationNamespaceProp},
+			aggOpts:    &AllocationAggregationOptions{
+				FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
+				ShareIdle:   ShareEven,
+			},
+			numResults: 1,
+			totalCost: 51.00,
+			results: map[string]float64{
+				"namespace2": 51.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 6d Share overhead with filters
+		// namespace1: 85.366 = 28.00 + (7.0*24.0)*(28.00/82.00)
+		// namespace2: 109.756 = 36.00 + (7.0*24.0)*(36.00/82.00)
+		// namespace3: 54.878 = 18.00 + (7.0*24.0)*(18.00/82.00)
+		// idle:       30.0000
+		// Then namespace 2 is filtered.
+		"6d": {
+			start:      start,
+			aggBy:      []string{AllocationNamespaceProp},
+			aggOpts:    &AllocationAggregationOptions{
+				FilterFuncs:       []AllocationMatchFunc{isNamespace("namespace2")},
+				SharedHourlyCosts: map[string]float64{"total": sharedOverheadHourlyCost},
+				ShareSplit:        ShareWeighted,
+			},
+			numResults: 1 + numIdle,
+			totalCost: 139.756,
+			results: map[string]float64{
+				"namespace2": 109.756,
+				IdleSuffix:   30.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 6e Share resources with filters
+		// --- Shared ---
+		// namespace1: 28.00 (gets shared among namespace2 and namespace3)
+		// --- Filtered ---
+		// namespace3: 27.33 = 18.00 + (28.00)*(18.00/54.00) (filtered out)
+		// --- Results ---
+		// namespace2: 54.667 = 36.00 + (28.00)*(36.00/54.00)
+		// idle:       30.0000
+		"6e": {
+			start:      start,
+			aggBy:      []string{AllocationNamespaceProp},
+			aggOpts:    &AllocationAggregationOptions{
+				FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
+				ShareFuncs:  []AllocationMatchFunc{isNamespace("namespace1")},
+				ShareSplit:  ShareWeighted,
+			},
+			numResults: 1 + numIdle,
+			totalCost: 84.667,
+			results: map[string]float64{
+				"namespace2": 54.667,
+				IdleSuffix:   30.00,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 6f Share idle weighted and share resources weighted
+		//
+		// First, share idle weighted produces:
+		//
+		// namespace1:      42.6875
+		//   initial cost   28.0000
+		//   cluster1.cpu    2.5000 = 5.00*(3.00/6.00)
+		//   cluster1.ram   12.1875 = 15.00*(13.0/16.0)
+		//
+		// namespace2:      46.3125
+		//   initial cost   36.0000
+		//   cluster1.cpu    2.5000 = 5.00*(3.0/6.0)
+		//   cluster1.ram    2.8125 = 15.00*(3.0/16.0)
+		//   cluster2.cpu    2.5000 = 5.00*(3.0/6.0)
+		//   cluster2.ram    2.5000 = 5.00*(3.0/6.0)
+		//
+		// namespace3:      23.0000
+		//   initial cost   18.0000
+		//   cluster2.cpu    2.5000 = 5.00*(3.0/6.0)
+		//   cluster2.ram    2.5000 = 5.00*(3.0/6.0)
+		//
+		// Then, sharing namespace1 means sharing 39.6875 according to coefficients
+		// computed before allocating idle (so that weighting idle differently
+		// doesn't adversely affect the sharing mechanism):
+		//
+		// namespace2:      74.7708
+		//   initial cost   30.0000
+		//   idle cost      10.3125
+		//   shared cost    28.4583 = (42.6875)*(36.0/54.0)
+		//
+		// namespace3:      37.2292
+		//   initial cost   18.0000
+		//   idle cost       5.0000
+		//   shared cost    14.2292 = (42.6875)*(18.0/54.0)
+		"6f": {
+			start:      start,
+			aggBy:      []string{AllocationNamespaceProp},
+			aggOpts:    &AllocationAggregationOptions{
+				ShareFuncs: []AllocationMatchFunc{isNamespace("namespace1")},
+				ShareSplit: ShareWeighted,
+				ShareIdle:  ShareWeighted,
+			},
+			numResults: 2,
+			totalCost: activeTotalCost+idleTotalCost,
+			results: map[string]float64{
+				"namespace2": 74.77,
+				"namespace3": 37.23,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 6g Share idle, share resources, and filter
+		//
+		// First, share idle weighted produces:
+		//
+		// namespace1:      42.6875
+		//   initial cost   28.0000
+		//   cluster1.cpu    2.5000 = 5.00*(3.00/6.00)
+		//   cluster1.ram   12.1875 = 15.00*(13.0/16.0)
+		//
+		// namespace2:      46.3125
+		//   initial cost   36.0000
+		//   cluster1.cpu    2.5000 = 5.00*(3.0/6.0)
+		//   cluster1.ram    2.8125 = 15.00*(3.0/16.0)
+		//   cluster2.cpu    2.5000 = 5.00*(3.0/6.0)
+		//   cluster2.ram    2.5000 = 5.00*(3.0/6.0)
+		//
+		// namespace3:      23.0000
+		//   initial cost   18.0000
+		//   cluster2.cpu    2.5000 = 5.00*(3.0/6.0)
+		//   cluster2.ram    2.5000 = 5.00*(3.0/6.0)
+		//
+		// Then, sharing namespace1 means sharing 39.6875 according to coefficients
+		// computed before allocating idle (so that weighting idle differently
+		// doesn't adversely affect the sharing mechanism):
+		//
+		// namespace2:      74.7708
+		//   initial cost   36.0000
+		//   idle cost      10.3125
+		//   shared cost    28.4583 = (42.6875)*(36.0/54.0)
+		//
+		// namespace3:      37.2292
+		//   initial cost   18.0000
+		//   idle cost       5.0000
+		//   shared cost    14.2292 = (42.6875)*(18.0/54.0)
+		//
+		// Then, filter for namespace2: 74.7708
+		"6g": {
+			start:      start,
+			aggBy:      []string{AllocationNamespaceProp},
+			aggOpts:    &AllocationAggregationOptions{
+				FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
+				ShareFuncs:  []AllocationMatchFunc{isNamespace("namespace1")},
+				ShareSplit:  ShareWeighted,
+				ShareIdle:   ShareWeighted,
+			},
+			numResults: 1,
+			totalCost: 74.77,
+			results: map[string]float64{
+				"namespace2": 74.77,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 6h Share idle, share resources, share overhead
+		//
+		// Share idle weighted:
+		//
+		// namespace1:      42.6875
+		//   initial cost   28.0000
+		//   cluster1.cpu    2.5000 = 5.00*(3.00/6.00)
+		//   cluster1.ram   12.1875 = 15.00*(13.0/16.0)
+		//
+		// namespace2:      46.3125
+		//   initial cost   36.0000
+		//   cluster1.cpu    2.5000 = 5.00*(3.0/6.0)
+		//   cluster1.ram    2.8125 = 15.00*(3.0/16.0)
+		//   cluster2.cpu    2.5000 = 5.00*(3.0/6.0)
+		//   cluster2.ram    2.5000 = 5.00*(3.0/6.0)
+		//
+		// namespace3:      23.0000
+		//   initial cost   18.0000
+		//   cluster2.cpu    2.5000 = 5.00*(3.0/6.0)
+		//   cluster2.ram    2.5000 = 5.00*(3.0/6.0)
+		//
+		// Then share overhead:
+		//
+		// namespace1:     100.0533 = 42.6875 + (7.0*24.0)*(28.00/82.00)
+		// namespace2:     120.0686 = 46.3125 + (7.0*24.0)*(36.00/82.00)
+		// namespace3:      59.8780 = 23.0000 + (7.0*24.0)*(18.00/82.00)
+		//
+		// Then namespace 2 is filtered.
+		"6h": {
+			start:      start,
+			aggBy:      []string{AllocationNamespaceProp},
+			aggOpts:    &AllocationAggregationOptions{
+				FilterFuncs:       []AllocationMatchFunc{isNamespace("namespace2")},
+				ShareSplit:        ShareWeighted,
+				ShareIdle:         ShareWeighted,
+				SharedHourlyCosts: map[string]float64{"total": sharedOverheadHourlyCost},
+			},
+			numResults: 1,
+			totalCost: 120.07,
+			results: map[string]float64{
+				"namespace2": 120.07,
+			},
+			windowStart: startYesterday,
+			windowEnd:   endYesterday,
+			expMinutes:  1440.0,
+		},
+		// 7  Edge cases and errors
 
-	// 7  Edge cases and errors
+		// 7a Empty AggregationProperties
+		// 7b Filter all
+		// 7c Share all
+		// 7d Share and filter the same allocations
+	}
 
-	// 7a Empty AggregationProperties
-	// 7b Filter all
-	// 7c Share all
-	// 7d Share and filter the same allocations
+	for name, testcase := range cases {
+		t.Run(name, func(t *testing.T) {
+			as = generateAllocationSet(testcase.start)
+			err = as.AggregateBy(testcase.aggBy, testcase.aggOpts)
+			assertAllocationSetTotals(t, as, name, err, testcase.numResults, testcase.totalCost)
+			assertAllocationTotals(t, as, name, testcase.results)
+			assertAllocationWindow(t, as, name, testcase.windowStart, testcase.windowEnd, testcase.expMinutes)
+		})
+	}
 }
 
 // TODO niko/etl