diff --git a/Gopkg.lock b/Gopkg.lock index 7749a60..7cdc374 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -25,6 +25,26 @@ revision = "cbaa98ba5575e67703b32b4b19f73c91f3c4159e" version = "v1.7.1" +[[projects]] + name = "github.com/docker/distribution" + packages = [ + ".", + "digestset", + "manifest", + "manifest/manifestlist", + "manifest/schema1", + "manifest/schema2", + "reference" + ] + revision = "40b7b5830a2337bb07627617740c0e39eb92800c" + version = "v2.7.0" + +[[projects]] + branch = "master" + name = "github.com/docker/libtrust" + packages = ["."] + revision = "aabc10ec26b754e797f9028f4589c5b7bd90dc20" + [[projects]] name = "github.com/fatih/color" packages = ["."] @@ -37,6 +57,12 @@ packages = ["."] revision = "c221c11516236e1a31ad2c85eb751ae9a6699b32" +[[projects]] + name = "github.com/go-ozzo/ozzo-validation" + packages = ["."] + revision = "106681dbb37bfa3e7683c4c8129cb7f5925ea3e9" + version = "v3.5.0" + [[projects]] name = "github.com/gofunky/automi" packages = [ @@ -52,8 +78,8 @@ "stream", "util" ] - revision = "7820dd7b95c19d275b3947657fc17fe14432185f" - version = "0.3.0" + revision = "af9d4ec3512afb6a9b499ef363d75953a5d3d55c" + version = "0.3.2" [[projects]] name = "github.com/hashicorp/errwrap" @@ -67,6 +93,12 @@ revision = "886a7fbe3eb1c874d46f623bfa70af45f425b3d1" version = "v1.0.0" +[[projects]] + name = "github.com/konsorten/go-windows-terminal-sequences" + packages = ["."] + revision = "5c8c8bd35d3832f5d134ae1e1e375b69a4d25242" + version = "v1.0.1" + [[projects]] name = "github.com/mattn/go-colorable" packages = ["."] @@ -85,6 +117,27 @@ revision = "3d22a244be8aa6fb16ac24af0e195c08b7d973aa" version = "v1.0.0" +[[projects]] + branch = "master" + name = "github.com/nokia/docker-registry-client" + packages = ["registry"] + revision = "bf401ccb7530925d4c1bda64849a593c2caba565" + +[[projects]] + name = "github.com/opencontainers/go-digest" + packages = ["."] + revision = "279bed98673dd5bef374d3b6e4b09e2af76183bf" + version = "v1.0.0-rc1" + +[[projects]] + name = "github.com/opencontainers/image-spec" + packages = [ + "specs-go", + "specs-go/v1" + ] + revision = "d60099175f88c47cd379c4738d158884749ed235" + version = "v1.0.1" + [[projects]] name = "github.com/posener/complete" packages = [ @@ -96,15 +149,30 @@ revision = "3ef9b31a6a0613ae832e7ecf208374027c3b2343" version = "v1.2.1" +[[projects]] + name = "github.com/sirupsen/logrus" + packages = ["."] + revision = "e1e72e9de974bd926e5c56f83753fba2df402ce5" + version = "v1.3.0" + +[[projects]] + branch = "master" + name = "golang.org/x/crypto" + packages = ["ssh/terminal"] + revision = "ff983b9c42bc9fbf91556e191cc8efb585c16908" + [[projects]] branch = "master" name = "golang.org/x/sys" - packages = ["unix"] - revision = "b4a75ba826a64a70990f11a225237acd6ef35c9f" + packages = [ + "unix", + "windows" + ] + revision = "badf5585203e739f88c2c6cd34188a6f54b5d619" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "ff8a98fe315774395f4a2832218c1481b2db6948fcb9a7dfa4e91af5fcefda07" + inputs-digest = "e62a2206b4af85c745c2a52f888a577cb5ee306d5d87a5fae1a3369ebb479246" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index d1b7bb2..a801d3c 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -27,4 +27,5 @@ [[constraint]] name = "github.com/gofunky/automi" - version = "0.3.0" + version = "0.3.2" + diff --git a/pkg/tupliplib/lib.go b/pkg/tupliplib/lib.go index cc108b6..46e00b0 100644 --- a/pkg/tupliplib/lib.go +++ b/pkg/tupliplib/lib.go @@ -1,12 +1,19 @@ package tupliplib import ( + "errors" "github.com/deckarep/golang-set" + "github.com/go-ozzo/ozzo-validation" "github.com/gofunky/automi/emitters" "github.com/gofunky/automi/stream" + "github.com/nokia/docker-registry-client/registry" "io" + "strings" ) +// DockerRegistry is the Docker Hub registry URL. +const DockerRegistry = "https://registry-1.docker.io/" + // VersionSeparator is the separator that separates the alias form the semantic version. const VersionSeparator = ":" @@ -35,6 +42,8 @@ type Tuplip struct { AddLatest bool // Separator to split the separate tag vector aliases. The default separator is single space. Separator string + // Docker repository of the root tag vector in the format `organization/repository`. + Repository string } // tuplipSource is the intermediarily-built Tuplip stream containing only the source parsing steps. @@ -50,25 +59,69 @@ func (t *Tuplip) check() { } } -// FromReader builds a tuplip source from a pipe reader. +// FromReader builds a tuplip source from a io.Reader as scanner. func (t *Tuplip) FromReader(src io.Reader) *tuplipSource { t.check() stm := stream.New(emitters.Scanner(src, nil)) stm.FlatMap(t.splitBySeparator) - stm.Filter(t.nonEmpty) + stm.Filter(nonEmpty) return &tuplipSource{t, stm} } -// Build builds a tuplip stream from a io.Reader as scanner. The returned stream has no configured sink. -func (s *tuplipSource) Build() *stream.Stream { - stm := s.stream - stm.Map(s.tuplip.splitVersion) - stm.Map(s.tuplip.packInSet) - stm.Reduce(mapset.NewSet(), s.tuplip.mergeSets) - stm.Map(s.tuplip.power) - stm.Map(s.tuplip.addLatestTag) - stm.FlatMap(s.tuplip.failOnEmpty) - stm.FlatMap(s.tuplip.join) - stm.Filter(s.tuplip.nonEmpty) - return stm +// Build defines a tuplip stream that builds a complete set of Docker tags. The returned stream has no configured sink. +func (s *tuplipSource) Build() (stream *stream.Stream) { + stream = s.stream + stream.Map(s.tuplip.splitVersion) + stream.Map(packInSet) + stream.Reduce(mapset.NewSet(), mergeSets) + stream.Map(power) + stream.Map(s.tuplip.addLatestTag) + stream.FlatMap(failOnEmpty) + stream.FlatMap(s.tuplip.join) + stream.Filter(nonEmpty) + return +} + +// getTags fetches the set of tags for the given Docker repository. +func (t *Tuplip) getTags() (tagMap map[string]mapset.Set, err error) { + if err = validation.Validate(t.Repository, + validation.Required, + ); err != nil { + return nil, err + } + if hub, err := registry.New(DockerRegistry, "", ""); err != nil { + return nil, err + } else { + if tags, err := hub.Tags(t.Repository); err != nil { + return nil, err + } else { + if len(tags) == 0 { + return nil, errors.New("no Docker tags could be found on the given remote") + } + tagMap := make(map[string]mapset.Set) + for _, tag := range tags { + tagVectors := strings.Split(tag, DockerTagSeparator) + vectorSet := mapset.NewSet() + for _, v := range tagVectors { + vectorSet.Add(v) + } + tagMap[tag] = vectorSet + } + return tagMap, nil + } + } +} + +// Find defines a tuplip stream that finds the matching Docker tag in the Docker Hub. +// The returned stream has no configured sink. +func (s *tuplipSource) Find() (stream *stream.Stream, err error) { + stream = s.stream + if tagMap, err := s.tuplip.getTags(); err != nil { + return nil, err + } else { + stream.Map(s.tuplip.splitVersion) + stream.Reduce(tagMap, removeCommon) + stream.Map(keyForSmallest) + } + return } diff --git a/pkg/tupliplib/lib_test.go b/pkg/tupliplib/lib_test.go index facf798..dfaa5a1 100644 --- a/pkg/tupliplib/lib_test.go +++ b/pkg/tupliplib/lib_test.go @@ -107,7 +107,7 @@ func TestTuplipStream_BuildFromReader(t *testing.T) { select { case gotErr := <-tStream.Open(): if (gotErr != nil) != tt.wantErr { - t.Errorf("Tuplip.Open() error = %v, wantErr %v", gotErr, tt.wantErr) + t.Errorf("Tuplip.Build() error = %v, wantErr %v", gotErr, tt.wantErr) return } case <-time.After(500 * time.Millisecond): @@ -119,9 +119,137 @@ func TestTuplipStream_BuildFromReader(t *testing.T) { wantSet.Add(w) } if !gotOutput.Equal(wantSet) { + t.Errorf("Tuplip.Build() = %v, want %v, difference %v", + gotOutput, wantSet, gotOutput.Difference(wantSet)) + } + }) + } +} + +func TestTuplipStream_FindFromReader(t *testing.T) { + type args struct { + input []string + } + tests := []struct { + name string + t Tuplip + args args + want string + wantErr bool + }{ + { + name: "Empty Repository", + args: args{[]string{""}}, + wantErr: true, + }, + { + name: "Input No Match", + t: Tuplip{Repository: "gofunky/git"}, + args: args{[]string{"unknown"}}, + wantErr: true, + }, + { + name: "Simple Unary Tag", + t: Tuplip{Repository: "gofunky/git"}, + args: args{[]string{"envload"}}, + want: "envload", + }, + { + name: "Simple Binary Tag", + t: Tuplip{Repository: "gofunky/git"}, + args: args{[]string{"envload", "alpine:3.8"}}, + want: "alpine3.8-envload", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + src := strings.NewReader(strings.Join(tt.args.input, " ")) + tuplipSrc := tt.t.FromReader(src) + tStream, srcErr := tuplipSrc.Find() + collector := collectors.Slice() + var gotErr error + if tStream != nil { + tStream.Into(collector) + select { + case gotErr = <-tStream.Open(): + + case <-time.After(500 * time.Millisecond): + t.Fatal("Waited too long ...") + return + } + } + if (gotErr != nil || srcErr != nil) != tt.wantErr { + t.Errorf("Tuplip.Open() error = %v, wantErr %v", gotErr, tt.wantErr) + return + } + gotOutput := mapset.NewSetFromSlice(collector.Get()) + wantSet := mapset.NewSet(tt.want) + if !tt.wantErr && !gotOutput.Equal(wantSet) { t.Errorf("Tuplip.Open() = %v, want %v, difference %v", gotOutput, wantSet, gotOutput.Difference(wantSet)) } }) } } + +func TestTuplip_getTags(t *testing.T) { + type fields struct { + ExcludeMajor bool + ExcludeMinor bool + ExcludeBase bool + AddLatest bool + Separator string + Repository string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "Unknown Repository", + fields: fields{Repository: "gofunky/unknown"}, + wantErr: true, + }, + { + name: "Invalid Repository Name", + fields: fields{Repository: "$%"}, + wantErr: true, + }, + { + name: "Empty Repository Name", + fields: fields{Repository: ""}, + wantErr: true, + }, + { + name: "Unary Repository", + fields: fields{Repository: "alpine"}, + wantErr: true, // TODO: find a way to check official images + }, + { + name: "Binary Repository", + fields: fields{Repository: "gofunky/git"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tu := &Tuplip{ + ExcludeMajor: tt.fields.ExcludeMajor, + ExcludeMinor: tt.fields.ExcludeMinor, + ExcludeBase: tt.fields.ExcludeBase, + AddLatest: tt.fields.AddLatest, + Separator: tt.fields.Separator, + Repository: tt.fields.Repository, + } + tagSet, err := tu.getTags() + if (err != nil) != tt.wantErr { + t.Errorf("Tuplip.getTags() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && len(tagSet) == 0 { + t.Errorf("Tuplip.getTags() returned no tags") + return + } + }) + } +} diff --git a/pkg/tupliplib/members.go b/pkg/tupliplib/members.go index 503b930..7c5e4ee 100644 --- a/pkg/tupliplib/members.go +++ b/pkg/tupliplib/members.go @@ -1,7 +1,6 @@ package tupliplib import ( - "errors" "fmt" "github.com/blang/semver" "github.com/deckarep/golang-set" @@ -100,11 +99,6 @@ func (t Tuplip) splitVersion(inputTag string) (result mapset.Set, err error) { } } -// nonEmpty marks if a string is not empty. -func (t Tuplip) nonEmpty(input string) bool { - return input != "" -} - // splitBySeparator separates the input string by the chosen character and trims superfluous spaces. func (t Tuplip) splitBySeparator(input string) (result []string) { result = strings.Split(input, t.Separator) @@ -114,29 +108,6 @@ func (t Tuplip) splitBySeparator(input string) (result []string) { return } -// packInSet packs a set as subset into a new set. -func (t Tuplip) packInSet(subSet mapset.Set) (result mapset.Set) { - return mapset.NewSetWith(subSet) -} - -// mergeSets merges the second given set into the first one. -func (t Tuplip) mergeSets(left mapset.Set, right mapset.Set) (result mapset.Set) { - return left.Union(right) -} - -// power build a power of the given set. -func (t Tuplip) power(inputSet mapset.Set) mapset.Set { - return inputSet.PowerSet() -} - -// failOnEmpty returns an error if the given power set is empty (i.e, has cardinality < 2). -func (t Tuplip) failOnEmpty(inputSet mapset.Set) (mapset.Set, error) { - if inputSet.Cardinality() <= 1 { - return nil, errors.New("no input tags could be detected") - } - return inputSet, nil -} - // join joins all subtags (i.e., elements of the given set) to all possible representations by building a cartesian // product of them. The subtags are separated by the given Docker separator. The subtags are ordered alphabetically // to ensure that a root tag vector (i.e., a tag without an alias) is mentioned before alias tags. diff --git a/pkg/tupliplib/members_test.go b/pkg/tupliplib/members_test.go index 48832b8..1517317 100644 --- a/pkg/tupliplib/members_test.go +++ b/pkg/tupliplib/members_test.go @@ -222,36 +222,6 @@ func TestTuplip_splitVersion(t *testing.T) { } } -func TestTuplip_nonEmpty(t *testing.T) { - type args struct { - input string - } - tests := []struct { - name string - t Tuplip - args args - want bool - }{ - { - name: "Empty String", - args: args{""}, - want: false, - }, - { - name: "Nonempty String", - args: args{"foo"}, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.t.nonEmpty(tt.args.input); got != tt.want { - t.Errorf("Tuplip.nonEmpty() = %v, want %v", got, tt.want) - } - }) - } -} - func TestTuplip_splitBySeparator(t *testing.T) { type args struct { input string @@ -286,6 +256,7 @@ func TestTuplip_splitBySeparator(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + tt.t.check() if gotResult := tt.t.splitBySeparator(tt.args.input); !reflect.DeepEqual(gotResult, tt.wantResult) { t.Errorf("Tuplip.splitBySeparator() = %v, want %v", gotResult, tt.wantResult) } @@ -293,78 +264,6 @@ func TestTuplip_splitBySeparator(t *testing.T) { } } -func TestTuplip_packInSet(t *testing.T) { - type args struct { - subSet mapset.Set - } - tests := []struct { - name string - t Tuplip - args args - wantResult mapset.Set - }{ - { - name: "Empty Set", - args: args{mapset.NewSet()}, - wantResult: mapset.NewSetWith(mapset.NewSet()), - }, - { - name: "Unary Set", - args: args{mapset.NewSet("foo")}, - wantResult: mapset.NewSetWith(mapset.NewSet("foo")), - }, - { - name: "Tuple Set", - args: args{mapset.NewSet("foo", "boo")}, - wantResult: mapset.NewSetWith(mapset.NewSet("foo", "boo")), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if gotResult := tt.t.packInSet(tt.args.subSet); !reflect.DeepEqual(gotResult.ToSlice(), - tt.wantResult.ToSlice()) { - t.Errorf("Tuplip.packInSet() = %v, want %v", gotResult, tt.wantResult) - } - }) - } -} - -func TestTuplip_mergeSets(t *testing.T) { - type args struct { - left mapset.Set - right mapset.Set - } - tests := []struct { - name string - t Tuplip - args args - wantResult mapset.Set - }{ - { - name: "Merge Empty Sets", - args: args{mapset.NewSet(), mapset.NewSet()}, - wantResult: mapset.NewSet(), - }, - { - name: "Merge Empty Set With Nonempty Set", - args: args{mapset.NewSet(), mapset.NewSet("foo")}, - wantResult: mapset.NewSet("foo"), - }, - { - name: "Merge Two Nonempty Sets", - args: args{mapset.NewSet("foo", "boo"), mapset.NewSet("hoo")}, - wantResult: mapset.NewSet("foo", "boo", "hoo"), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if gotResult := tt.t.mergeSets(tt.args.left, tt.args.right); !reflect.DeepEqual(gotResult, tt.wantResult) { - t.Errorf("Tuplip.mergeSets() = %v, want %v", gotResult, tt.wantResult) - } - }) - } -} - func TestTuplip_join(t *testing.T) { type args struct { inputSet mapset.Set @@ -414,99 +313,3 @@ func TestTuplip_join(t *testing.T) { }) } } - -func TestTuplip_power(t *testing.T) { - type args struct { - inputSet mapset.Set - } - tests := []struct { - name string - t Tuplip - args args - want []mapset.Set - }{ - { - name: "Empty Set", - args: args{mapset.NewSet()}, - want: []mapset.Set{ - mapset.NewSet(), - }, - }, - { - name: "Unary Set", - args: args{mapset.NewSet("alias")}, - want: []mapset.Set{ - mapset.NewSet(), - mapset.NewSet("alias"), - }, - }, - { - name: "Binary Set", - args: args{mapset.NewSet("alias", "foo")}, - want: []mapset.Set{ - mapset.NewSet(), - mapset.NewSet("alias"), - mapset.NewSet("foo"), - mapset.NewSet("alias", "foo"), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := tt.t.power(tt.args.inputSet).ToSlice() - for _, tagSet := range tt.want { - var found bool - for _, val := range got { - if tagSet.Equal(val.(mapset.Set)) { - found = true - } - } - if !found { - t.Errorf("Tuplip.power() = %v, want %v, missing %v", got, tt.want, tagSet) - } - } - }) - } -} - -func TestTuplip_failOnEmpty(t *testing.T) { - nonemptySet := mapset.NewSet(mapset.NewSet(), mapset.NewSet("alias")) - type args struct { - inputSet mapset.Set - } - tests := []struct { - name string - t Tuplip - args args - want mapset.Set - wantErr bool - }{ - { - name: "Empty Set", - args: args{mapset.NewSet()}, - wantErr: true, - }, - { - name: "Empty Power Set", - args: args{mapset.NewSet(mapset.NewSet())}, - wantErr: true, - }, - { - name: "Nonempty Power Set", - args: args{nonemptySet}, - want: nonemptySet, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := tt.t.failOnEmpty(tt.args.inputSet) - if (err != nil) != tt.wantErr { - t.Errorf("Tuplip.failOnEmpty() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Tuplip.failOnEmpty() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/pkg/tupliplib/statics.go b/pkg/tupliplib/statics.go new file mode 100644 index 0000000..fd356af --- /dev/null +++ b/pkg/tupliplib/statics.go @@ -0,0 +1,95 @@ +package tupliplib + +import ( + "errors" + "fmt" + "github.com/deckarep/golang-set" + "math" + "sort" + "strings" +) + +// packInSet packs a set as subset into a new set. +func packInSet(subSet mapset.Set) (result mapset.Set) { + return mapset.NewSetWith(subSet) +} + +// mergeSets merges the second given set into the first one. +func mergeSets(left mapset.Set, right mapset.Set) (result mapset.Set) { + return left.Union(right) +} + +// power build a power of the given set. +func power(inputSet mapset.Set) mapset.Set { + return inputSet.PowerSet() +} + +// failOnEmpty returns an error if the given power set is empty (i.e, has cardinality < 2). +func failOnEmpty(inputSet mapset.Set) (mapset.Set, error) { + if inputSet.Cardinality() <= 1 { + return nil, errors.New("no input tags could be detected") + } + return inputSet, nil +} + +// nonEmpty marks if a string is not empty. +func nonEmpty(input string) bool { + return input != "" +} + +// removeCommon finds common denominators in the seed and removes them. +// Empty sets are removed on the second run to be able to find equal sets in the subsequent run. +// Fails if none of next's elements is found in any of seed's sets. +func removeCommon(seed map[string]mapset.Set, next mapset.Set) (result map[string]mapset.Set, err error) { + var found = false + for k, v := range seed { + if v.Cardinality() == 0 { + delete(seed, k) + } else { + if v.Intersect(next).Cardinality() > 0 { + found = true + } + seed[k] = v.Difference(next) + } + } + if !found { + return nil, fmt.Errorf("the given tag vector '%v' was not found in any remote tags", next) + } + return seed, nil +} + +// keyForSmallest finds the smallest set in the map and returns its key. +func keyForSmallest(seed map[string]mapset.Set) (result string) { + smallestSets := make([]string, 0) + minVal := minVal(seed) + for k, v := range seed { + if v.Cardinality() == minVal { + smallestSets = append(smallestSets, k) + } + } + return mostSeparators(smallestSets, DockerTagSeparator) +} + +// minVal finds the smallest set in the given map. +func minVal(numbers map[string]mapset.Set) (minNumber int) { + minNumber = math.MaxInt8 + for _, v := range numbers { + if c := v.Cardinality(); c < minNumber { + minNumber = c + } + } + return minNumber +} + +// mostSeparators finds the element in the given slice that contains the most separators. +func mostSeparators(values []string, sep string) (result string) { + sort.Strings(values) + maxNumber := math.MinInt8 + for _, v := range values { + if c := strings.Count(v, sep); c > maxNumber { + maxNumber = c + result = v + } + } + return result +} diff --git a/pkg/tupliplib/statics_test.go b/pkg/tupliplib/statics_test.go new file mode 100644 index 0000000..d1a2627 --- /dev/null +++ b/pkg/tupliplib/statics_test.go @@ -0,0 +1,396 @@ +package tupliplib + +import ( + "github.com/deckarep/golang-set" + "reflect" + "testing" +) + +func TestTuplip_nonEmpty(t *testing.T) { + type args struct { + input string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "Empty String", + args: args{""}, + want: false, + }, + { + name: "Nonempty String", + args: args{"foo"}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := nonEmpty(tt.args.input); got != tt.want { + t.Errorf("Tuplip.nonEmpty() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestTuplip_packInSet(t *testing.T) { + type args struct { + subSet mapset.Set + } + tests := []struct { + name string + args args + wantResult mapset.Set + }{ + { + name: "Empty Set", + args: args{mapset.NewSet()}, + wantResult: mapset.NewSetWith(mapset.NewSet()), + }, + { + name: "Unary Set", + args: args{mapset.NewSet("foo")}, + wantResult: mapset.NewSetWith(mapset.NewSet("foo")), + }, + { + name: "Tuple Set", + args: args{mapset.NewSet("foo", "boo")}, + wantResult: mapset.NewSetWith(mapset.NewSet("foo", "boo")), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotResult := packInSet(tt.args.subSet); !reflect.DeepEqual(gotResult.ToSlice(), + tt.wantResult.ToSlice()) { + t.Errorf("Tuplip.packInSet() = %v, want %v", gotResult, tt.wantResult) + } + }) + } +} + +func TestTuplip_mergeSets(t *testing.T) { + type args struct { + left mapset.Set + right mapset.Set + } + tests := []struct { + name string + args args + wantResult mapset.Set + }{ + { + name: "Merge Empty Sets", + args: args{mapset.NewSet(), mapset.NewSet()}, + wantResult: mapset.NewSet(), + }, + { + name: "Merge Empty Set With Nonempty Set", + args: args{mapset.NewSet(), mapset.NewSet("foo")}, + wantResult: mapset.NewSet("foo"), + }, + { + name: "Merge Two Nonempty Sets", + args: args{mapset.NewSet("foo", "boo"), mapset.NewSet("hoo")}, + wantResult: mapset.NewSet("foo", "boo", "hoo"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotResult := mergeSets(tt.args.left, tt.args.right); !reflect.DeepEqual(gotResult, tt.wantResult) { + t.Errorf("Tuplip.mergeSets() = %v, want %v", gotResult, tt.wantResult) + } + }) + } +} + +func TestTuplip_power(t *testing.T) { + type args struct { + inputSet mapset.Set + } + tests := []struct { + name string + args args + want []mapset.Set + }{ + { + name: "Empty Set", + args: args{mapset.NewSet()}, + want: []mapset.Set{ + mapset.NewSet(), + }, + }, + { + name: "Unary Set", + args: args{mapset.NewSet("alias")}, + want: []mapset.Set{ + mapset.NewSet(), + mapset.NewSet("alias"), + }, + }, + { + name: "Binary Set", + args: args{mapset.NewSet("alias", "foo")}, + want: []mapset.Set{ + mapset.NewSet(), + mapset.NewSet("alias"), + mapset.NewSet("foo"), + mapset.NewSet("alias", "foo"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := power(tt.args.inputSet).ToSlice() + for _, tagSet := range tt.want { + var found bool + for _, val := range got { + if tagSet.Equal(val.(mapset.Set)) { + found = true + } + } + if !found { + t.Errorf("Tuplip.power() = %v, want %v, missing %v", got, tt.want, tagSet) + } + } + }) + } +} + +func TestTuplip_failOnEmpty(t *testing.T) { + nonemptySet := mapset.NewSet(mapset.NewSet(), mapset.NewSet("alias")) + type args struct { + inputSet mapset.Set + } + tests := []struct { + name string + args args + want mapset.Set + wantErr bool + }{ + { + name: "Empty Set", + args: args{mapset.NewSet()}, + wantErr: true, + }, + { + name: "Empty Power Set", + args: args{mapset.NewSet(mapset.NewSet())}, + wantErr: true, + }, + { + name: "Nonempty Power Set", + args: args{nonemptySet}, + want: nonemptySet, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := failOnEmpty(tt.args.inputSet) + if (err != nil) != tt.wantErr { + t.Errorf("Tuplip.failOnEmpty() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Tuplip.failOnEmpty() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_removeCommon(t *testing.T) { + type args struct { + seed map[string]mapset.Set + next mapset.Set + } + tests := []struct { + name string + args args + want map[string]mapset.Set + wantErr bool + }{ + { + name: "Unary seed", + args: args{ + seed: map[string]mapset.Set{ + "tag": mapset.NewSet("tag"), + }, + next: mapset.NewSet("tag"), + }, + want: map[string]mapset.Set{ + "tag": mapset.NewSet(), + }, + }, + { + name: "No matching set elements", + args: args{ + seed: map[string]mapset.Set{ + "tag": mapset.NewSet("tag"), + }, + next: mapset.NewSet("unknown"), + }, + wantErr: true, + }, + { + name: "Binary seed", + args: args{ + seed: map[string]mapset.Set{ + "tag": mapset.NewSet("tag"), + "second-tag": mapset.NewSet("tag", "second"), + }, + next: mapset.NewSet("tag"), + }, + want: map[string]mapset.Set{ + "tag": mapset.NewSet(), + "second-tag": mapset.NewSet("second"), + }, + }, + { + name: "Complex seed", + args: args{ + seed: map[string]mapset.Set{ + "empty": mapset.NewSet(), + "tag": mapset.NewSet("tag"), + "second-tag": mapset.NewSet("tag", "second"), + "1.2.3-alias-tag1.2": mapset.NewSet("tag1.2", "alias", "1.2.3"), + }, + next: mapset.NewSet("tag", "tag1", "tag1.2", "tag1.2.3"), + }, + want: map[string]mapset.Set{ + "tag": mapset.NewSet(), + "second-tag": mapset.NewSet("second"), + "1.2.3-alias-tag1.2": mapset.NewSet("alias", "1.2.3"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := removeCommon(tt.args.seed, tt.args.next) + if (err != nil) != tt.wantErr { + t.Errorf("Tuplip.failOnEmpty() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("removeCommon() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_keyForSmallest(t *testing.T) { + type args struct { + seed map[string]mapset.Set + } + tests := []struct { + name string + args args + wantResult string + }{ + { + name: "Empty Input", + args: args{seed: map[string]mapset.Set{}}, + }, + { + name: "Unary Input", + args: args{seed: map[string]mapset.Set{ + "tag": mapset.NewSet(), + }}, + wantResult: "tag", + }, + { + name: "Binary Input", + args: args{seed: map[string]mapset.Set{ + "tag": mapset.NewSet(), + "second-tag": mapset.NewSet("second"), + }}, + wantResult: "tag", + }, + { + name: "Complex Input", + args: args{seed: map[string]mapset.Set{ + "tag": mapset.NewSet(), + "second-tag": mapset.NewSet("second"), + "1.2.3-alias-tag1.2": mapset.NewSet("alias", "1.2.3"), + }}, + wantResult: "tag", + }, + { + name: "Duplicates Input", + args: args{seed: map[string]mapset.Set{ + "first-tag": mapset.NewSet(), + "second-tag": mapset.NewSet("second"), + "1.2.3-alias-tag1.2": mapset.NewSet(), + }}, + wantResult: "1.2.3-alias-tag1.2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotResult := keyForSmallest(tt.args.seed); gotResult != tt.wantResult { + t.Errorf("keyForSmallest() = %v, want %v", gotResult, tt.wantResult) + } + }) + } +} + +func Test_minVal(t *testing.T) { + type args struct { + numbers map[string]mapset.Set + } + tests := []struct { + name string + args args + wantMinNumber int + }{ + { + name: "Sample Input With Duplicates", + args: args{numbers: map[string]mapset.Set{ + "foo": mapset.NewSet("a", "b"), + "boo": mapset.NewSet("c", "d"), + "other": mapset.NewSet("a", "b", "c"), + }}, + wantMinNumber: 2, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotMinNumber := minVal(tt.args.numbers); gotMinNumber != tt.wantMinNumber { + t.Errorf("minVal() = %v, want %v", gotMinNumber, tt.wantMinNumber) + } + }) + } +} + +func Test_mostSeparators(t *testing.T) { + type args struct { + values []string + sep string + } + tests := []struct { + name string + args args + wantResult string + }{ + { + name: "Sample Input With Duplicates", + args: args{ + values: []string{ + "a;b", + "d;b;c", + "a;b;c", + "x", + }, + sep: ";", + }, + wantResult: "a;b;c", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotResult := mostSeparators(tt.args.values, tt.args.sep); gotResult != tt.wantResult { + t.Errorf("mostSeparators() = %v, want %v", gotResult, tt.wantResult) + } + }) + } +}