diff --git a/deployment/address_book.go b/deployment/address_book.go index dae4374b0fd..e123a2116bd 100644 --- a/deployment/address_book.go +++ b/deployment/address_book.go @@ -2,7 +2,6 @@ package deployment import ( "fmt" - "sort" "strings" "sync" @@ -33,21 +32,22 @@ var ( ) type TypeAndVersion struct { - Type ContractType - Version semver.Version - Labels LabelSet + Type ContractType `json:"type"` + Version semver.Version `json:"version"` + Labels LabelSet `json:"labels,omitempty"` } func (tv TypeAndVersion) String() string { if len(tv.Labels) == 0 { return fmt.Sprintf("%s %s", tv.Type, tv.Version.String()) } - mdSlice := tv.Labels.AsSlice() - sort.Strings(mdSlice) // stable order + + // Use the LabelSet's String method for sorted labels + sortedLabels := tv.Labels.String() return fmt.Sprintf("%s %s %s", tv.Type, tv.Version.String(), - strings.Join(mdSlice, " "), + sortedLabels, ) } @@ -307,15 +307,11 @@ type typeVersionKey struct { } func tvKey(tv TypeAndVersion) typeVersionKey { - sortedLabels := make([]string, 0, len(tv.Labels)) - for lbl := range tv.Labels { - sortedLabels = append(sortedLabels, lbl) - } - sort.Strings(sortedLabels) + sortedLabels := tv.Labels.String() return typeVersionKey{ Type: tv.Type, Version: tv.Version.String(), - Labels: strings.Join(sortedLabels, ","), + Labels: sortedLabels, } } @@ -332,8 +328,8 @@ func AddressesContainBundle(addrs map[string]TypeAndVersion, wantTypes []TypeAnd // They match exactly (Type, Version, Labels) counts[wantKey]++ if counts[wantKey] > 1 { - return false, fmt.Errorf("found more than one instance of contract %s %s (labels=%v)", - wantTV.Type, wantTV.Version.String(), wantTV.Labels) + return false, fmt.Errorf("found more than one instance of contract %s %s (labels=%s)", + wantTV.Type, wantTV.Version.String(), wantTV.Labels.String()) } } } diff --git a/deployment/address_book_labels.go b/deployment/address_book_labels.go index 4917a9cb0eb..f559a39078a 100644 --- a/deployment/address_book_labels.go +++ b/deployment/address_book_labels.go @@ -1,5 +1,10 @@ package deployment +import ( + "sort" + "strings" +) + // LabelSet represents a set of labels on an address book entry. type LabelSet map[string]struct{} @@ -28,13 +33,24 @@ func (ls LabelSet) Contains(labels string) bool { return ok } -// AsSlice returns the labels in a slice. Useful for printing or serialization. -func (ls LabelSet) AsSlice() []string { - out := make([]string, 0, len(ls)) - for labels := range ls { - out = append(out, labels) +// String returns the labels as a sorted, space-separated string. +// It implements the fmt.Stringer interface. +func (ls LabelSet) String() string { + if len(ls) == 0 { + return "" + } + + // Collect labels into a slice + labels := make([]string, 0, len(ls)) + for label := range ls { + labels = append(labels, label) } - return out + + // Sort the labels to ensure consistent ordering + sort.Strings(labels) + + // Concatenate the sorted labels into a single string + return strings.Join(labels, " ") } // Equal checks if two LabelSets are equal. diff --git a/deployment/address_book_labels_test.go b/deployment/address_book_labels_test.go index 1c27e4863c4..f42e3568cba 100644 --- a/deployment/address_book_labels_test.go +++ b/deployment/address_book_labels_test.go @@ -55,20 +55,57 @@ func TestLabelSet_Contains(t *testing.T) { assert.False(t, ms.Contains("baz")) } -func TestLabelSet_AsSlice(t *testing.T) { - ms := NewLabelSet("foo", "bar") - slice := ms.AsSlice() - - // We can't rely on order in a map-based set, so we only check membership and length - assert.Len(t, slice, 2, "expected 2 distinct labels in slice") +// TestLabelSet_String tests the String() method of the LabelSet type. +func TestLabelSet_String(t *testing.T) { + tests := []struct { + name string + labels LabelSet + expected string + }{ + { + name: "Empty LabelSet", + labels: NewLabelSet(), + expected: "", + }, + { + name: "Single label", + labels: NewLabelSet("alpha"), + expected: "alpha", + }, + { + name: "Multiple labels in random order", + labels: NewLabelSet("beta", "gamma", "alpha"), + expected: "alpha beta gamma", + }, + { + name: "Labels with special characters", + labels: NewLabelSet("beta", "gamma!", "@alpha"), + expected: "@alpha beta gamma!", + }, + { + name: "Labels with spaces", + labels: NewLabelSet("beta", "gamma delta", "alpha"), + expected: "alpha beta gamma delta", + }, + { + name: "Labels added in different orders", + labels: NewLabelSet("delta", "beta", "alpha"), + expected: "alpha beta delta", + }, + { + name: "Labels with duplicate additions", + labels: NewLabelSet("alpha", "beta", "alpha", "gamma", "beta"), + expected: "alpha beta gamma", + }, + } - // Convert slice to a map for quick membership checks - found := make(map[string]bool) - for _, item := range slice { - found[item] = true + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + result := tt.labels.String() + assert.Equal(t, tt.expected, result, "LabelSet.String() should return the expected sorted string") + }) } - assert.True(t, found["foo"], "expected 'foo' in slice") - assert.True(t, found["bar"], "expected 'bar' in slice") } func TestLabelSet_Equal(t *testing.T) { diff --git a/deployment/address_book_test.go b/deployment/address_book_test.go index 526f02f9042..0c6b228da2e 100644 --- a/deployment/address_book_test.go +++ b/deployment/address_book_test.go @@ -356,36 +356,40 @@ func TestTypeAndVersionFromString(t *testing.T) { t.Parallel() tests := []struct { - name string - input string - wantErr bool - wantType ContractType - wantVersion semver.Version - wantMeta []string + name string + input string + wantErr bool + wantType ContractType + wantVersion semver.Version + wantLabels LabelSet + wantTypeAndVersion string }{ { - name: "valid - no labels", - input: "CallProxy 1.0.0", - wantErr: false, - wantType: "CallProxy", - wantVersion: Version1_0_0, - wantMeta: nil, // no labels + name: "valid - no labels", + input: "CallProxy 1.0.0", + wantErr: false, + wantType: "CallProxy", + wantVersion: Version1_0_0, + wantLabels: NewLabelSet(), + wantTypeAndVersion: "CallProxy 1.0.0", }, { - name: "valid - multiple labels, normal spacing", - input: "CallProxy 1.0.0 SA staging", - wantErr: false, - wantType: "CallProxy", - wantVersion: Version1_0_0, - wantMeta: []string{"SA", "staging"}, + name: "valid - multiple labels, normal spacing", + input: "CallProxy 1.0.0 SA staging", + wantErr: false, + wantType: "CallProxy", + wantVersion: Version1_0_0, + wantLabels: NewLabelSet("SA", "staging"), + wantTypeAndVersion: "CallProxy 1.0.0 SA staging", }, { - name: "valid - multiple labels, extra spacing", - input: " CallProxy 1.0.0 SA staging ", - wantErr: false, - wantType: "CallProxy", - wantVersion: Version1_0_0, - wantMeta: []string{"SA", "staging"}, + name: "valid - multiple labels, extra spacing", + input: " CallProxy 1.0.0 SA staging ", + wantErr: false, + wantType: "CallProxy", + wantVersion: Version1_0_0, + wantLabels: NewLabelSet("SA", "staging"), + wantTypeAndVersion: "CallProxy 1.0.0 SA staging", }, { name: "invalid - not enough parts", @@ -418,11 +422,10 @@ func TestTypeAndVersionFromString(t *testing.T) { require.Equal(t, tt.wantVersion.String(), gotTV.Version.String(), "incorrect version") // Check labels - gotMeta := gotTV.Labels.AsSlice() - require.Equal(t, len(tt.wantMeta), len(gotMeta), "labels length mismatch") - for _, wantMd := range tt.wantMeta { - require.Contains(t, gotMeta, wantMd, "missing labels item") - } + require.Equal(t, tt.wantLabels, gotTV.Labels, "labels mismatch") + + // Check type and version + require.Equal(t, tt.wantTypeAndVersion, gotTV.String(), "type and version mismatch") }) } }