Ver código fonte

Implement AllocationFilter.Equals()

Introduce support for logical equivalence between values of the
AllocationFilter type. This equivalence is for checking e.g. that two
ORs with filters in different orders are, at their core, filtering
the same thing: (or a b c) = (or b c a)

This is motivated by the need to check filter equivalence in unit tests
that don't have stable filter construction, like when constructing a
filter while iterating over a map.

Signed-off-by: Michael Dresser <michaelmdresser@gmail.com>
Michael Dresser 3 anos atrás
pai
commit
b1df8a2a78
2 arquivos alterados com 675 adições e 0 exclusões
  1. 120 0
      pkg/kubecost/allocationfilter.go
  2. 555 0
      pkg/kubecost/allocationfilter_test.go

+ 120 - 0
pkg/kubecost/allocationfilter.go

@@ -2,6 +2,7 @@ package kubecost
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"sort"
 	"strings"
 	"strings"
 
 
 	"github.com/opencost/opencost/pkg/log"
 	"github.com/opencost/opencost/pkg/log"
@@ -100,6 +101,14 @@ type AllocationFilter interface {
 	Flattened() AllocationFilter
 	Flattened() AllocationFilter
 
 
 	String() string
 	String() string
+
+	// Equals returns true if the two AllocationFilters are logically
+	// equivalent.
+	Equals(AllocationFilter) bool
+
+	// empty returns true if the filter isn't filtering anything, i.e. the
+	// filter is a no-op.
+	empty() bool
 }
 }
 
 
 // AllocationFilterCondition is the lowest-level type of filter. It represents
 // AllocationFilterCondition is the lowest-level type of filter. It represents
@@ -130,9 +139,21 @@ func (afc AllocationFilterCondition) String() string {
 
 
 // Flattened returns itself because you cannot flatten a base condition further
 // Flattened returns itself because you cannot flatten a base condition further
 func (filter AllocationFilterCondition) Flattened() AllocationFilter {
 func (filter AllocationFilterCondition) Flattened() AllocationFilter {
+
 	return filter
 	return filter
 }
 }
 
 
+func (left AllocationFilterCondition) Equals(right AllocationFilter) bool {
+	if rightAFC, ok := right.(AllocationFilterCondition); ok {
+		return left == rightAFC
+	}
+	return false
+}
+
+func (filter AllocationFilterCondition) empty() bool {
+	return false
+}
+
 // AllocationFilterOr is a set of filters that should be evaluated as a logical
 // AllocationFilterOr is a set of filters that should be evaluated as a logical
 // OR.
 // OR.
 type AllocationFilterOr struct {
 type AllocationFilterOr struct {
@@ -185,6 +206,51 @@ func (filter AllocationFilterOr) Flattened() AllocationFilter {
 	return AllocationFilterOr{Filters: flattenedFilters}
 	return AllocationFilterOr{Filters: flattenedFilters}
 }
 }
 
 
+func (filter AllocationFilterOr) sort() {
+	for _, inner := range filter.Filters {
+		if and, ok := inner.(AllocationFilterAnd); ok {
+			and.sort()
+		} else if or, ok := inner.(AllocationFilterOr); ok {
+			or.sort()
+		}
+	}
+
+	// While a slight hack, we can rely on the string serialization of the
+	// inner filters.
+	sort.SliceStable(filter.Filters, func(i, j int) bool {
+		return filter.Filters[i].String() < filter.Filters[j].String()
+	})
+}
+
+func (left AllocationFilterOr) Equals(right AllocationFilter) bool {
+	if left.empty() {
+		return right == nil || right.empty()
+	}
+
+	rightOr, ok := right.(AllocationFilterOr)
+	if !ok {
+		return false
+	}
+
+	// Once sorted, the string representations should be equal. We can sort
+	// because ordering of logical OR statements does not matter.
+	left.sort()
+	rightOr.sort()
+	return left.String() == rightOr.String()
+}
+
+func (filter AllocationFilterOr) empty() bool {
+	for _, inner := range filter.Filters {
+		if inner == nil {
+			continue
+		}
+		if !inner.empty() {
+			return false
+		}
+	}
+	return true
+}
+
 // AllocationFilterOr is a set of filters that should be evaluated as a logical
 // AllocationFilterOr is a set of filters that should be evaluated as a logical
 // AND.
 // AND.
 type AllocationFilterAnd struct {
 type AllocationFilterAnd struct {
@@ -221,6 +287,51 @@ func (filter AllocationFilterAnd) Flattened() AllocationFilter {
 	return AllocationFilterAnd{Filters: flattenedFilters}
 	return AllocationFilterAnd{Filters: flattenedFilters}
 }
 }
 
 
+func (filter AllocationFilterAnd) sort() {
+	for _, inner := range filter.Filters {
+		if and, ok := inner.(AllocationFilterAnd); ok {
+			and.sort()
+		} else if or, ok := inner.(AllocationFilterOr); ok {
+			or.sort()
+		}
+	}
+
+	// While a slight hack, we can rely on the string serialization of the
+	// inner filters.
+	sort.SliceStable(filter.Filters, func(i, j int) bool {
+		return filter.Filters[i].String() < filter.Filters[j].String()
+	})
+}
+
+func (left AllocationFilterAnd) Equals(right AllocationFilter) bool {
+	if left.empty() {
+		return right == nil || right.empty()
+	}
+
+	rightAnd, ok := right.(AllocationFilterAnd)
+	if !ok {
+		return false
+	}
+
+	// Once sorted, the string representations should be equal. We can sort
+	// because ordering of logical AND statements does not matter.
+	left.sort()
+	rightAnd.sort()
+	return left.String() == rightAnd.String()
+}
+
+func (filter AllocationFilterAnd) empty() bool {
+	for _, inner := range filter.Filters {
+		if inner == nil {
+			continue
+		}
+		if !inner.empty() {
+			return false
+		}
+	}
+	return true
+}
+
 func (filter AllocationFilterCondition) Matches(a *Allocation) bool {
 func (filter AllocationFilterCondition) Matches(a *Allocation) bool {
 	if a == nil {
 	if a == nil {
 		return false
 		return false
@@ -428,3 +539,12 @@ func (afn AllocationFilterNone) String() string { return "(none)" }
 func (afn AllocationFilterNone) Flattened() AllocationFilter { return afn }
 func (afn AllocationFilterNone) Flattened() AllocationFilter { return afn }
 
 
 func (afn AllocationFilterNone) Matches(a *Allocation) bool { return false }
 func (afn AllocationFilterNone) Matches(a *Allocation) bool { return false }
+
+func (left AllocationFilterNone) Equals(right AllocationFilter) bool {
+	_, ok := right.(AllocationFilterNone)
+	return ok
+}
+
+func (afn AllocationFilterNone) empty() bool {
+	return false
+}

+ 555 - 0
pkg/kubecost/allocationfilter_test.go

@@ -1,6 +1,7 @@
 package kubecost
 package kubecost
 
 
 import (
 import (
+	"fmt"
 	"reflect"
 	"reflect"
 	"testing"
 	"testing"
 )
 )
@@ -1130,3 +1131,557 @@ func Test_AllocationFilter_Flattened(t *testing.T) {
 		})
 		})
 	}
 	}
 }
 }
+
+func Test_AllocationFilter_Equals(t *testing.T) {
+	cases := []struct {
+		left     AllocationFilter
+		right    AllocationFilter
+		expected bool
+	}{
+		// AFC
+		{
+			left:     AllocationFilterCondition{},
+			right:    AllocationFilterCondition{},
+			expected: true,
+		},
+		{
+			left: AllocationFilterCondition{
+				Field: FilterNamespace,
+				Op:    FilterStartsWith,
+				Value: "kubecost-abc",
+			},
+			right: AllocationFilterCondition{
+				Field: FilterNamespace,
+				Op:    FilterStartsWith,
+				Value: "kubecost-abc",
+			},
+			expected: true,
+		},
+		{
+			left: AllocationFilterCondition{
+				Field: FilterLabel,
+				Op:    FilterEquals,
+				Key:   "app",
+				Value: "kubecost-abc",
+			},
+			right: AllocationFilterCondition{
+				Field: FilterLabel,
+				Op:    FilterEquals,
+				Key:   "app",
+				Value: "kubecost-abc",
+			},
+			expected: true,
+		},
+		{
+			left: AllocationFilterCondition{
+				Field: FilterLabel,
+				Op:    FilterEquals,
+				Key:   "app",
+				Value: "kubecost-abc",
+			},
+			right: AllocationFilterCondition{
+				Field: FilterLabel,
+				Op:    FilterEquals,
+				Value: "kubecost-abc",
+			},
+			expected: false,
+		},
+		{
+			left: AllocationFilterCondition{
+				Field: FilterLabel,
+				Op:    FilterEquals,
+				Value: "kubecost-abc",
+			},
+			right: AllocationFilterCondition{
+				Field: FilterLabel,
+				Op:    FilterEquals,
+				Key:   "app",
+				Value: "kubecost-abc",
+			},
+			expected: false,
+		},
+		{
+			left: AllocationFilterCondition{
+				Field: FilterNamespace,
+				Op:    FilterStartsWith,
+				Value: "kubecost-abc",
+			},
+			right: AllocationFilterCondition{
+				Field: FilterNamespace,
+				Op:    FilterStartsWith,
+				Value: "kubecost-abcd",
+			},
+			expected: false,
+		},
+		// OR
+		// EMPTY
+		{
+			left:     AllocationFilterOr{},
+			right:    nil,
+			expected: true,
+		},
+		{
+			left:     AllocationFilterOr{Filters: []AllocationFilter{}},
+			right:    nil,
+			expected: true,
+		},
+
+		{
+			left:     AllocationFilterOr{},
+			right:    AllocationFilterOr{},
+			expected: true,
+		},
+		{
+			left:     AllocationFilterOr{},
+			right:    AllocationFilterOr{Filters: []AllocationFilter{}},
+			expected: true,
+		},
+
+		{
+			left:     AllocationFilterOr{Filters: []AllocationFilter{}},
+			right:    AllocationFilterOr{},
+			expected: true,
+		},
+		{
+			left:     AllocationFilterOr{Filters: []AllocationFilter{}},
+			right:    AllocationFilterOr{Filters: []AllocationFilter{}},
+			expected: true,
+		},
+		// FILLED
+		{
+			left: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			expected: true,
+		},
+		{
+			left: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterNone{},
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+					},
+				},
+			}},
+			expected: true,
+		},
+		{
+			left: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+			}},
+			expected: true,
+		},
+		{
+			left: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns3",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			expected: false,
+		},
+		// AND
+		// EMPTY
+		{
+			left:     AllocationFilterAnd{},
+			right:    nil,
+			expected: true,
+		},
+		{
+			left:     AllocationFilterAnd{Filters: []AllocationFilter{}},
+			right:    nil,
+			expected: true,
+		},
+
+		{
+			left:     AllocationFilterAnd{},
+			right:    AllocationFilterAnd{},
+			expected: true,
+		},
+		{
+			left:     AllocationFilterAnd{},
+			right:    AllocationFilterAnd{Filters: []AllocationFilter{}},
+			expected: true,
+		},
+
+		{
+			left:     AllocationFilterAnd{Filters: []AllocationFilter{}},
+			right:    AllocationFilterAnd{},
+			expected: true,
+		},
+		{
+			left:     AllocationFilterAnd{Filters: []AllocationFilter{}},
+			right:    AllocationFilterAnd{Filters: []AllocationFilter{}},
+			expected: true,
+		},
+		// FILLED
+		{
+			left: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			expected: true,
+		},
+		{
+			left: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterNone{},
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+					},
+				},
+			}},
+			expected: true,
+		},
+		{
+			left: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+			}},
+			expected: true,
+		},
+		{
+			left: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns3",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			expected: false,
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(fmt.Sprintf("'%s' = '%s'", c.left, c.right), func(t *testing.T) {
+			if c.left.Equals(c.right) != c.expected {
+				t.Fatalf("Expected: %t", c.expected)
+			}
+		})
+	}
+}