diff --git a/report/topology.go b/report/topology.go index 0fb7b9765..dc8ffbd3c 100644 --- a/report/topology.go +++ b/report/topology.go @@ -2,6 +2,7 @@ package report import ( "fmt" + "sort" "strings" ) @@ -78,8 +79,9 @@ func (n Nodes) Merge(other Nodes) Nodes { // given node in a given topology, along with the edges emanating from the // node and metadata about those edges. type Node struct { - Metadata `json:"metadata,omitempty"` - Counters `json:"counters,omitempty"` + Metadata Metadata `json:"metadata,omitempty"` + Counters Counters `json:"counters,omitempty"` + Sets Sets `json:"sets,omitempty"` Adjacency IDList `json:"adjacency"` Edges EdgeMetadatas `json:"edges,omitempty"` } @@ -195,6 +197,100 @@ func (c Counters) Copy() Counters { return result } +// Sets is a string->set-of-strings map. +type Sets map[string]StringSet + +// Merge merges two sets maps into a fresh set, performing set-union merges as +// appropriate. +func (s Sets) Merge(other Sets) Sets { + result := s.Copy() + for k, v := range other { + result[k].Merge(v) + } + return result +} + +// Copy returns a value copy of the sets map. +func (s Sets) Copy() Sets { + result := Sets{} + for k, v := range s { + result[k] = v.Copy() + } + return result +} + +// StringSet is a sorted set of unique strings. Clients must use the Add +// method to add strings. +type StringSet []string + +// MakeStringSet makes a new StringSet with the given strings. +func MakeStringSet(strs ...string) StringSet { + if len(strs) <= 0 { + return StringSet{} + } + sort.Strings(strs) + for i := 1; i < len(strs); { // shuffle down any duplicates + if strs[i-1] == strs[i] { + strs = append(strs[:i-1], strs[i:]...) + continue + } + i++ + } + return StringSet(strs) +} + +// Add adds the strings to the StringSet. Add is the only valid way to grow a +// StringSet. Add returns the StringSet to enable chaining. +func (s StringSet) Add(strs ...string) StringSet { + for _, str := range strs { + i := sort.Search(len(s), func(i int) bool { return s[i] >= str }) + if i < len(s) && s[i] == str { + // The list already has the element. + continue + } + // It a new element, insert it in order. + s = append(s, "") + copy(s[i+1:], s[i:]) + s[i] = str + } + return s +} + +// Merge combines the two StringSets and returns a new result. +func (s StringSet) Merge(other StringSet) StringSet { + if len(other) == 0 { // Optimise special case, to avoid allocating + return s // (note unit test DeepEquals breaks if we don't do this) + } + result := make(StringSet, len(s)+len(other)) + for i, j, k := 0, 0, 0; ; k++ { + switch { + case i >= len(s): + copy(result[k:], other[j:]) + return result[:k+len(other)-j] + case j >= len(other): + copy(result[k:], s[i:]) + return result[:k+len(s)-i] + case s[i] < other[j]: + result[k] = s[i] + i++ + case s[i] > other[j]: + result[k] = other[j] + j++ + default: // equal + result[k] = s[i] + i++ + j++ + } + } +} + +// Copy returns a value copy of the StringSet. +func (s StringSet) Copy() StringSet { + result := make(StringSet, len(s)) + copy(result, s) + return result +} + // EdgeMetadatas collect metadata about each edge in a topology. Keys are the // remote node IDs, as in Adjacency. type EdgeMetadatas map[string]EdgeMetadata diff --git a/report/topology_test.go b/report/topology_test.go new file mode 100644 index 000000000..bd0a3a978 --- /dev/null +++ b/report/topology_test.go @@ -0,0 +1,66 @@ +package report_test + +import ( + "reflect" + "testing" + + "github.com/weaveworks/scope/report" +) + +func TestMakeStringSet(t *testing.T) { + for _, testcase := range []struct { + input []string + want report.StringSet + }{ + {input: []string{}, want: report.MakeStringSet()}, + {input: []string{"a"}, want: report.MakeStringSet("a")}, + {input: []string{"a", "a"}, want: report.MakeStringSet("a")}, + {input: []string{"b", "c", "a"}, want: report.MakeStringSet("a", "b", "c")}, + } { + if want, have := testcase.want, report.MakeStringSet(testcase.input...); !reflect.DeepEqual(want, have) { + t.Errorf("%v: want %v, have %v", testcase.input, want, have) + } + } +} + +func TestStringSetAdd(t *testing.T) { + for _, testcase := range []struct { + input report.StringSet + strs []string + want report.StringSet + }{ + {input: report.MakeStringSet(), strs: []string{}, want: report.MakeStringSet()}, + {input: report.MakeStringSet("a"), strs: []string{}, want: report.MakeStringSet("a")}, + {input: report.MakeStringSet(), strs: []string{"a"}, want: report.MakeStringSet("a")}, + {input: report.MakeStringSet("a"), strs: []string{"a"}, want: report.MakeStringSet("a")}, + {input: report.MakeStringSet("b"), strs: []string{"a", "b"}, want: report.MakeStringSet("a", "b")}, + {input: report.MakeStringSet("a"), strs: []string{"c", "b"}, want: report.MakeStringSet("a", "b", "c")}, + {input: report.MakeStringSet("a", "c"), strs: []string{"b", "b", "b"}, want: report.MakeStringSet("a", "b", "c")}, + } { + if want, have := testcase.want, testcase.input.Add(testcase.strs...); !reflect.DeepEqual(want, have) { + t.Errorf("%v + %v: want %v, have %v", testcase.input, testcase.strs, want, have) + } + } +} + +func TestStringSetMerge(t *testing.T) { + for _, testcase := range []struct { + input report.StringSet + other report.StringSet + want report.StringSet + }{ + {input: report.MakeStringSet(), other: report.MakeStringSet(), want: report.MakeStringSet()}, + {input: report.MakeStringSet("a"), other: report.MakeStringSet(), want: report.MakeStringSet("a")}, + {input: report.MakeStringSet(), other: report.MakeStringSet("a"), want: report.MakeStringSet("a")}, + {input: report.MakeStringSet("a"), other: report.MakeStringSet("b"), want: report.MakeStringSet("a", "b")}, + {input: report.MakeStringSet("b"), other: report.MakeStringSet("a"), want: report.MakeStringSet("a", "b")}, + {input: report.MakeStringSet("a"), other: report.MakeStringSet("a"), want: report.MakeStringSet("a")}, + {input: report.MakeStringSet("a", "c"), other: report.MakeStringSet("a", "b"), want: report.MakeStringSet("a", "b", "c")}, + {input: report.MakeStringSet("b"), other: report.MakeStringSet("a"), want: report.MakeStringSet("a", "b")}, + } { + if want, have := testcase.want, testcase.input.Merge(testcase.other); !reflect.DeepEqual(want, have) { + t.Errorf("%v + %v: want %v, have %v", testcase.input, testcase.other, want, have) + } + } + +}