diff --git a/clicker/clabernetes.go b/clicker/clabernetes.go index 51de1bac..50244b7a 100644 --- a/clicker/clabernetes.go +++ b/clicker/clabernetes.go @@ -396,8 +396,8 @@ func (c *clabernetes) buildPods( ImagePullPolicy: selfPod.Spec.Containers[0].ImagePullPolicy, SecurityContext: &k8scorev1.SecurityContext{ // we need privileged for setting syscalls and such - Privileged: clabernetesutil.BoolToPointer(true), - RunAsUser: clabernetesutil.Int64ToPointer(0), + Privileged: clabernetesutil.ToPointer(true), + RunAsUser: clabernetesutil.ToPointer(int64(0)), }, Env: []k8scorev1.EnvVar{ { diff --git a/controllers/topology/configmap_test.go b/controllers/topology/configmap_test.go index d0a37940..c93edaa2 100644 --- a/controllers/topology/configmap_test.go +++ b/controllers/topology/configmap_test.go @@ -42,7 +42,7 @@ const renderConfigMapTestName = "configmap/render-config-map" // TestRenderConfigMap ensures that we properly render the main tunnel/config configmap for a given // c9s deployment (containerlab CR). func TestRenderConfigMap(t *testing.T) { - testCases := []struct { + cases := []struct { name string obj ctrlruntimeclient.Object clabernetesConfigs map[string]*clabernetesutilcontainerlab.Config @@ -59,7 +59,7 @@ func TestRenderConfigMap(t *testing.T) { clabernetesConfigs: map[string]*clabernetesutilcontainerlab.Config{ "srl1": { Name: "clabernetes-srl1", - Prefix: clabernetesutil.StringToPointer(""), + Prefix: clabernetesutil.ToPointer(""), Topology: &clabernetesutilcontainerlab.Topology{ Defaults: &clabernetesutilcontainerlab.NodeDefinition{ Ports: defaultPorts, @@ -84,7 +84,7 @@ func TestRenderConfigMap(t *testing.T) { }, "srl2": { Name: "clabernetes-srl2", - Prefix: clabernetesutil.StringToPointer(""), + Prefix: clabernetesutil.ToPointer(""), Topology: &clabernetesutilcontainerlab.Topology{ Defaults: &clabernetesutilcontainerlab.NodeDefinition{ Ports: defaultPorts, @@ -142,7 +142,7 @@ func TestRenderConfigMap(t *testing.T) { clabernetesConfigs: map[string]*clabernetesutilcontainerlab.Config{ "srl1": { Name: "clabernetes-srl1", - Prefix: clabernetesutil.StringToPointer(""), + Prefix: clabernetesutil.ToPointer(""), Topology: &clabernetesutilcontainerlab.Topology{ Defaults: &clabernetesutilcontainerlab.NodeDefinition{ Ports: defaultPorts, @@ -158,7 +158,7 @@ func TestRenderConfigMap(t *testing.T) { }, "srl2": { Name: "clabernetes-srl2", - Prefix: clabernetesutil.StringToPointer(""), + Prefix: clabernetesutil.ToPointer(""), Topology: &clabernetesutilcontainerlab.Topology{ Defaults: &clabernetesutilcontainerlab.NodeDefinition{ Ports: defaultPorts, @@ -180,10 +180,12 @@ func TestRenderConfigMap(t *testing.T) { }, } - for _, testCase := range testCases { + for _, testCase := range cases { t.Run( testCase.name, func(t *testing.T) { + t.Logf("%s: starting", testCase.name) + reconciler := clabernetescontrollerstopology.Reconciler{ ResourceKind: "containerlab", ConfigManagerGetter: clabernetesconfig.GetFakeManager, diff --git a/controllers/topology/containerlab/config.go b/controllers/topology/containerlab/config.go index 42969696..d1043053 100644 --- a/controllers/topology/containerlab/config.go +++ b/controllers/topology/containerlab/config.go @@ -309,7 +309,7 @@ func (c *Controller) processConfigForNode( // what the user has provided *or* the default of "clab"). // since prefixes are only useful when multiple labs are scheduled on the same node, and // that will never be the case with clabernetes, the prefix is unnecessary. - Prefix: clabernetesutil.StringToPointer(""), + Prefix: clabernetesutil.ToPointer(""), } for _, link := range clabTopo.Links { diff --git a/controllers/topology/deployment.go b/controllers/topology/deployment.go index 1bea8298..b550ce43 100644 --- a/controllers/topology/deployment.go +++ b/controllers/topology/deployment.go @@ -318,8 +318,8 @@ func renderDeployment( Labels: labels, }, Spec: k8sappsv1.DeploymentSpec{ - Replicas: clabernetesutil.Int32ToPointer(1), - RevisionHistoryLimit: clabernetesutil.Int32ToPointer(0), + Replicas: clabernetesutil.ToPointer(int32(1)), + RevisionHistoryLimit: clabernetesutil.ToPointer(int32(0)), Selector: &metav1.LabelSelector{ MatchLabels: matchLabels, }, @@ -369,8 +369,8 @@ func renderDeployment( ), SecurityContext: &k8scorev1.SecurityContext{ // obviously we need privileged for dind setup - Privileged: clabernetesutil.BoolToPointer(true), - RunAsUser: clabernetesutil.Int64ToPointer(0), + Privileged: clabernetesutil.ToPointer(true), + RunAsUser: clabernetesutil.ToPointer(int64(0)), }, Env: []k8scorev1.EnvVar{ { diff --git a/controllers/topology/kne/config.go b/controllers/topology/kne/config.go index b8296bcb..873e9d52 100644 --- a/controllers/topology/kne/config.go +++ b/controllers/topology/kne/config.go @@ -82,7 +82,7 @@ func (c *Controller) processConfig( //nolint:funlen }, Links: nil, }, - Prefix: clabernetesutil.StringToPointer(""), + Prefix: clabernetesutil.ToPointer(""), } if kneModel != "" { diff --git a/controllers/topology/service.go b/controllers/topology/service.go index 6779d555..1dec01e9 100644 --- a/controllers/topology/service.go +++ b/controllers/topology/service.go @@ -12,7 +12,10 @@ import ( apimachinerytypes "k8s.io/apimachinery/pkg/types" ) -func serviceConforms( +// ServiceConforms asserts if a given service conforms with a rendered service -- this isn't +// checking if the services are exactly the same, just checking that the parts clabernetes cares +// about are the same. +func ServiceConforms( existingService, renderedService *k8scorev1.Service, expectedOwnerUID apimachinerytypes.UID, diff --git a/controllers/topology/serviceexpose.go b/controllers/topology/serviceexpose.go index a49fc106..54bafbf1 100644 --- a/controllers/topology/serviceexpose.go +++ b/controllers/topology/serviceexpose.go @@ -221,7 +221,7 @@ func (r *Reconciler) enforceExposeServices( return err } - if !serviceConforms(service, expectedService, obj.GetUID()) { + if !ServiceConforms(service, expectedService, obj.GetUID()) { r.Log.Debugf( "comparing existing expose service '%s/%s' spec does not conform to desired "+ "state, updating", diff --git a/controllers/topology/servicefabric.go b/controllers/topology/servicefabric.go index 5ec57006..3fd77d68 100644 --- a/controllers/topology/servicefabric.go +++ b/controllers/topology/servicefabric.go @@ -196,7 +196,7 @@ func (r *Reconciler) enforceFabricServices( return err } - if !serviceConforms(service, expectedService, obj.GetUID()) { + if !ServiceConforms(service, expectedService, obj.GetUID()) { r.Log.Debugf( "comparing existing service '%s/%s' spec does not conform to desired state, "+ "updating", diff --git a/controllers/topology/tunnels_test.go b/controllers/topology/tunnels_test.go index 6b4d3bd7..8a800d03 100644 --- a/controllers/topology/tunnels_test.go +++ b/controllers/topology/tunnels_test.go @@ -18,7 +18,7 @@ const testAllocateTunnelIdsTestName = "tunnels/allocate-tunnel-ids" // parts in play to ensure that we use the tunnel IDs consistently and also obviously don't stomp // on any existing tunnel IDs. func TestAllocateTunnelIds(t *testing.T) { - testCases := []struct { + cases := []struct { name string statusTunnels map[string][]*clabernetesapistopologyv1alpha1.Tunnel processedTunnels map[string][]*clabernetesapistopologyv1alpha1.Tunnel @@ -286,7 +286,7 @@ func TestAllocateTunnelIds(t *testing.T) { }, } - for _, testCase := range testCases { + for _, testCase := range cases { t.Run( testCase.name, func(t *testing.T) { diff --git a/util/bools_test.go b/util/bools_test.go new file mode 100644 index 00000000..f198ae88 --- /dev/null +++ b/util/bools_test.go @@ -0,0 +1,50 @@ +package util_test + +import ( + "testing" + + clabernetestesthelper "github.com/srl-labs/clabernetes/testhelper" + clabernetesutil "github.com/srl-labs/clabernetes/util" +) + +func TestAnyBoolTrue(t *testing.T) { + cases := []struct { + name string + in []bool + expected bool + }{ + { + name: "simple", + in: []bool{true}, + expected: true, + }, + { + name: "all-true", + in: []bool{true, true, true}, + expected: true, + }, + { + name: "one-true", + in: []bool{false, true, false}, + expected: true, + }, + { + name: "all-false", + in: []bool{false, false, false}, + expected: false, + }, + } + + for _, testCase := range cases { + t.Run( + testCase.name, + func(t *testing.T) { + t.Logf("%s: starting", testCase.name) + + actual := clabernetesutil.AnyBoolTrue(testCase.in...) + if actual != testCase.expected { + clabernetestesthelper.FailOutput(t, actual, testCase.expected) + } + }) + } +} diff --git a/util/files.go b/util/files.go index c0c4703b..50a95caf 100644 --- a/util/files.go +++ b/util/files.go @@ -2,11 +2,8 @@ package util import ( "errors" - "io" "io/fs" - "net/http" "os" - "strings" ) // MustCreateDirectory creates a directory at path `directory` with provided permissions, it panics @@ -25,33 +22,3 @@ func MustFileExists(f string) bool { return !errors.Is(err, os.ErrNotExist) } - -// ResolveAtFileOrURL returns the bytes from `path` where path is either a filepath or URL. -func ResolveAtFileOrURL(path string) ([]byte, error) { - var b []byte - - switch { - case strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://"): - resp, err := http.Get(path) //nolint:gosec,noctx - if err != nil { - return nil, err - } - - defer resp.Body.Close() //nolint - - b, err = io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - default: // fall-through to local filesystem - var err error - - b, err = os.ReadFile(path) //nolint:gosec - if err != nil { - return nil, err - } - } - - return b, nil -} diff --git a/util/hash_test.go b/util/hash_test.go new file mode 100644 index 00000000..bd656794 --- /dev/null +++ b/util/hash_test.go @@ -0,0 +1,35 @@ +package util_test + +import ( + "testing" + + clabernetestesthelper "github.com/srl-labs/clabernetes/testhelper" + clabernetesutil "github.com/srl-labs/clabernetes/util" +) + +func TestHashBytes(t *testing.T) { + cases := []struct { + name string + in []byte + expected string + }{ + { + name: "simple", + in: []byte("hashmeplz"), + expected: "b370f837831aa6b19d65cac4a0f8ef13e8d145027d3b95992232ccb8f0e564b5", + }, + } + + for _, testCase := range cases { + t.Run( + testCase.name, + func(t *testing.T) { + t.Logf("%s: starting", testCase.name) + + actual := clabernetesutil.HashBytes(testCase.in) + if actual != testCase.expected { + clabernetestesthelper.FailOutput(t, actual, testCase.expected) + } + }) + } +} diff --git a/util/kubernetes_test.go b/util/kubernetes_test.go new file mode 100644 index 00000000..7128d8d8 --- /dev/null +++ b/util/kubernetes_test.go @@ -0,0 +1,157 @@ +package util_test + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/api/resource" + + k8scorev1 "k8s.io/api/core/v1" + + clabernetestesthelper "github.com/srl-labs/clabernetes/testhelper" + clabernetesutil "github.com/srl-labs/clabernetes/util" +) + +func TestSafeConcatNameKubernetes(t *testing.T) { + cases := []struct { + name string + in []string + expected string + }{ + { + name: "simple", + in: []string{"afinename"}, + expected: "afinename", + }, + { + name: "simple-multi-word", + in: []string{"a", "fine", "name"}, + expected: "a-fine-name", + }, + { + name: "over-max-len", + in: []string{ + "a", + "fine", + "name", + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", //nolint:lll + }, + expected: "a-fine-name-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-8fa96d7", + }, + } + + for _, tc := range cases { + t.Logf("%s: starting", tc.name) + + actual := clabernetesutil.SafeConcatNameKubernetes(tc.in...) + if actual != tc.expected { + clabernetestesthelper.FailOutput(t, actual, tc.expected) + } + } +} + +func TestSafeConcatNameMax(t *testing.T) { + cases := []struct { + name string + in []string + max int + expected string + }{ + { + name: "simple", + in: []string{"afinename"}, + max: 30, + expected: "afinename", + }, + { + name: "simple-multi-word", + in: []string{"a", "fine", "name"}, + max: 30, + expected: "a-fine-name", + }, + { + name: "over-max-len", + in: []string{ + "a", + "fine", + "name", + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", //nolint:lll + }, + max: 30, + expected: "a-fine-name-xxxxxxxxxx-8fa96d7", + }, + } + + for _, tc := range cases { + t.Logf("%s: starting", tc.name) + + actual := clabernetesutil.SafeConcatNameMax(tc.in, tc.max) + if actual != tc.expected { + clabernetestesthelper.FailOutput(t, actual, tc.expected) + } + } +} + +func TestYAMLToK8sResourceRequirements(t *testing.T) { + cases := []struct { + name string + in string + expected *k8scorev1.ResourceRequirements + }{ + { + name: "simple", + in: `--- +requests: + memory: 128Mi + cpu: 50m +`, + expected: &k8scorev1.ResourceRequirements{ + Limits: k8scorev1.ResourceList{}, + Requests: k8scorev1.ResourceList{ + "memory": resource.MustParse("128Mi"), + "cpu": resource.MustParse("50m"), + }, + }, + }, + { + name: "simple", + in: `--- +requests: + memory: 128Mi + cpu: 50m +limits: + memory: 256Mi + cpu: 100m +`, + expected: &k8scorev1.ResourceRequirements{ + Limits: k8scorev1.ResourceList{ + "memory": resource.MustParse("256Mi"), + "cpu": resource.MustParse("100m"), + }, + Requests: k8scorev1.ResourceList{ + "memory": resource.MustParse("128Mi"), + "cpu": resource.MustParse("50m"), + }, + }, + }, + } + + for _, testCase := range cases { + t.Run( + testCase.name, + func(t *testing.T) { + t.Logf("%s: starting", testCase.name) + + actual, err := clabernetesutil.YAMLToK8sResourceRequirements(testCase.in) + if err != nil { + t.Fatalf( + "failed calling YAMLToK8sResourceRequirements, error: %s", err, + ) + } + + if !reflect.DeepEqual(actual, testCase.expected) { + clabernetestesthelper.FailOutput(t, actual, testCase.expected) + } + }) + } +} diff --git a/util/pointers.go b/util/pointers.go index 87fcf7eb..d5c5c265 100644 --- a/util/pointers.go +++ b/util/pointers.go @@ -1,13 +1,6 @@ package util -// Int32ToPointer returns i as a pointer. -func Int32ToPointer(i int32) *int32 { return &i } - -// Int64ToPointer returns i as a pointer. -func Int64ToPointer(i int64) *int64 { return &i } - -// BoolToPointer returns a pointer to b. -func BoolToPointer(b bool) *bool { return &b } - -// StringToPointer returns a pointer to s. -func StringToPointer(s string) *string { return &s } +// ToPointer accepts an object T and returns a pointer to it. +func ToPointer[T any](t T) *T { + return &t +} diff --git a/util/pointers_test.go b/util/pointers_test.go new file mode 100644 index 00000000..056491e2 --- /dev/null +++ b/util/pointers_test.go @@ -0,0 +1,51 @@ +package util_test + +import ( + "testing" + + clabernetestesthelper "github.com/srl-labs/clabernetes/testhelper" + clabernetesutil "github.com/srl-labs/clabernetes/util" +) + +func TestToPointer(t *testing.T) { + trueObj := true + strObj := "hihi" + i32Obj := int32(10) + i64Obj := int64(10) + + cases := []struct { + name string + in any + expected any + }{ + { + name: "bool", + in: trueObj, + }, + { + name: "string", + in: strObj, + }, + { + name: "i32", + in: i32Obj, + }, + { + name: "i64", + in: i64Obj, + }, + } + + for _, testCase := range cases { + t.Run( + testCase.name, + func(t *testing.T) { + t.Logf("%s: starting", testCase.name) + + actual := clabernetesutil.ToPointer(testCase.in) + if *actual != testCase.in { + clabernetestesthelper.FailOutput(t, *actual, testCase.in) + } + }) + } +} diff --git a/util/regex_test.go b/util/regex_test.go new file mode 100644 index 00000000..e41f43cc --- /dev/null +++ b/util/regex_test.go @@ -0,0 +1,47 @@ +package util_test + +import ( + "regexp" + "testing" + + clabernetestesthelper "github.com/srl-labs/clabernetes/testhelper" + clabernetesutil "github.com/srl-labs/clabernetes/util" +) + +func TestRegexStringSubmatchToMap(t *testing.T) { + cases := []struct { + name string + p *regexp.Regexp + in string + expected map[string]string + }{ + { + name: "bool", + p: regexp.MustCompile( + `(?mi)https?:\/\/(?:www\.)?github\.com\/(?P.*?\/.*?)\/(?:(blob)|(tree))(?P.*)`, //nolint:lll + ), + in: "https://github.com/srl-labs/containerlab/tree/main/lab-examples/srl02", + expected: map[string]string{ + "GroupRepo": "srl-labs/containerlab", + "Path": "/main/lab-examples/srl02", + }, + }, + } + + for _, testCase := range cases { + t.Run( + testCase.name, + func(t *testing.T) { + t.Logf("%s: starting", testCase.name) + + actual := clabernetesutil.RegexStringSubMatchToMap(testCase.p, testCase.in) + + for k, v := range testCase.expected { + expectedV, ok := actual[k] + if !ok || expectedV != v { + clabernetestesthelper.FailOutput(t, actual, testCase.expected) + } + } + }) + } +} diff --git a/util/sets_test.go b/util/sets_test.go new file mode 100644 index 00000000..894b0a38 --- /dev/null +++ b/util/sets_test.go @@ -0,0 +1,164 @@ +package util_test + +import ( + "testing" + + clabernetestesthelper "github.com/srl-labs/clabernetes/testhelper" + clabernetesutil "github.com/srl-labs/clabernetes/util" +) + +func TestStringSetAdd(t *testing.T) { + cases := []struct { + name string + vals []string + expected []string + }{ + { + name: "simple", + vals: []string{"one", "two", "three"}, + expected: []string{"one", "two", "three"}, + }, + { + name: "empty", + vals: []string{}, + expected: []string{}, + }, + } + + for _, testCase := range cases { + t.Run( + testCase.name, + func(t *testing.T) { + t.Logf("%s: starting", testCase.name) + + ss := clabernetesutil.NewStringSet() + + for _, s := range testCase.vals { + ss.Add(s) + } + + actual := ss.Items() + + // going through set changes order, so just doing this to confirm expectations + if !clabernetesutil.StringSliceContainsAll(actual, testCase.expected) && + !clabernetesutil.StringSliceContainsAll(testCase.expected, actual) { + clabernetestesthelper.FailOutput(t, actual, testCase.expected) + } + }) + } +} + +func TestStringSetRemove(t *testing.T) { + cases := []struct { + name string + vals []string + remove string + expected []string + }{ + { + name: "simple", + vals: []string{"one", "two", "three"}, + remove: "two", + expected: []string{"one", "three"}, + }, + { + name: "not present", + vals: []string{"one", "two", "three"}, + remove: "four", + expected: []string{"one", "two", "three"}, + }, + } + + for _, testCase := range cases { + t.Run( + testCase.name, + func(t *testing.T) { + t.Logf("%s: starting", testCase.name) + + ss := clabernetesutil.NewStringSetWithValues(testCase.vals...) + + ss.Remove(testCase.remove) + + actual := ss.Items() + + // going through set changes order, so just doing this to confirm expectations + if !clabernetesutil.StringSliceContainsAll(actual, testCase.expected) && + !clabernetesutil.StringSliceContainsAll(testCase.expected, actual) { + clabernetestesthelper.FailOutput(t, actual, testCase.expected) + } + }) + } +} + +func TestStringSetContains(t *testing.T) { + cases := []struct { + name string + vals []string + contains string + expected bool + }{ + { + name: "simple", + vals: []string{"one", "two", "three"}, + contains: "two", + expected: true, + }, + { + name: "simple", + vals: []string{"one", "two", "three"}, + contains: "four", + expected: false, + }, + } + + for _, testCase := range cases { + t.Run( + testCase.name, + func(t *testing.T) { + t.Logf("%s: starting", testCase.name) + + ss := clabernetesutil.NewStringSetWithValues(testCase.vals...) + + actual := ss.Contains(testCase.contains) + + if actual != testCase.expected { + clabernetestesthelper.FailOutput(t, actual, testCase.expected) + } + }) + } +} + +func TestStringSetLen(t *testing.T) { + cases := []struct { + name string + vals []string + expected int + }{ + { + name: "simple", + vals: []string{"one", "two", "three"}, + expected: 3, + }, + { + name: "simple", + vals: []string{}, + expected: 0, + }, + } + + for _, testCase := range cases { + t.Run( + testCase.name, + func(t *testing.T) { + t.Logf("%s: starting", testCase.name) + + ss := clabernetesutil.NewStringSetWithValues(testCase.vals...) + + actual := ss.Len() + + if actual != testCase.expected { + clabernetestesthelper.FailOutput(t, actual, testCase.expected) + } + }) + } +} diff --git a/util/slices.go b/util/slices.go index 35c94608..a80037cb 100644 --- a/util/slices.go +++ b/util/slices.go @@ -20,3 +20,44 @@ func StringSliceDifference(a, b []string) []string { return diff } + +// StringSliceContainsAll returns true if all values in vals are in string slice ss, otherwise +// false. +func StringSliceContainsAll(ss, vals []string) bool { + containsAll := true + + for _, v := range vals { + var found bool + + for _, s := range ss { + if s == v { + found = true + + break + } + } + + if !found { + containsAll = false + + break + } + } + + return containsAll +} + +// StringSliceEqual returns true if the string slices provided are equal, otherwise false. +func StringSliceEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + + for i, val := range a { + if val != b[i] { + return false + } + } + + return true +} diff --git a/util/slices_test.go b/util/slices_test.go new file mode 100644 index 00000000..af301aa9 --- /dev/null +++ b/util/slices_test.go @@ -0,0 +1,121 @@ +package util_test + +import ( + "testing" + + clabernetestesthelper "github.com/srl-labs/clabernetes/testhelper" + clabernetesutil "github.com/srl-labs/clabernetes/util" +) + +func TestStringSliceContainsAll(t *testing.T) { + cases := []struct { + name string + ss []string + vals []string + expected bool + }{ + { + name: "simple", + ss: []string{"one", "two", "three"}, + vals: []string{"two"}, + expected: true, + }, + { + name: "duplicate vals", + ss: []string{"one", "two", "three"}, + vals: []string{"two", "two"}, + expected: true, + }, + { + name: "more vals than ss", + ss: []string{"one", "two", "three"}, + vals: []string{"two", "two", "two", "two"}, + expected: true, + }, + { + name: "doesnt have all vals", + ss: []string{"one", "two", "three"}, + vals: []string{"one", "two", "three", "four"}, + expected: false, + }, + { + name: "no match", + ss: []string{"one", "two", "three"}, + vals: []string{"four"}, + expected: false, + }, + { + name: "empty ss", + ss: []string{}, + vals: []string{"two"}, + expected: false, + }, + { + name: "empty vals", + ss: []string{"one", "two", "three"}, + vals: []string{}, + expected: true, + }, + { + name: "empty ss and vals", + ss: []string{}, + vals: []string{}, + expected: true, + }, + } + + for _, testCase := range cases { + t.Run( + testCase.name, + func(t *testing.T) { + t.Logf("%s: starting", testCase.name) + + actual := clabernetesutil.StringSliceContainsAll(testCase.ss, testCase.vals) + + if actual != testCase.expected { + clabernetestesthelper.FailOutput(t, actual, testCase.expected) + } + }) + } +} + +func TestStringSliceDifference(t *testing.T) { + cases := []struct { + name string + a []string + b []string + expected []string + }{ + { + name: "simple", + a: []string{"one", "two", "two", "two", "three", "three"}, + b: []string{"one", "two", "two", "two", "three", "four"}, + expected: []string{"four"}, + }, + { + name: "a has more elements", + a: []string{"one", "two", "two", "two", "three", "four"}, + b: []string{"one", "two", "two", "two", "three"}, + expected: []string{}, + }, + { + name: "empty", + a: []string{}, + expected: []string{}, + }, + } + + for _, testCase := range cases { + t.Run( + testCase.name, + func(t *testing.T) { + t.Logf("%s: starting", testCase.name) + + actual := clabernetesutil.StringSliceDifference(testCase.a, testCase.b) + + if !clabernetesutil.StringSliceEqual(actual, testCase.expected) { + clabernetestesthelper.FailOutput(t, actual, testCase.expected) + } + }) + } +} diff --git a/util/strings_test.go b/util/strings_test.go new file mode 100644 index 00000000..d0637453 --- /dev/null +++ b/util/strings_test.go @@ -0,0 +1,50 @@ +package util_test + +import ( + "testing" + + clabernetestesthelper "github.com/srl-labs/clabernetes/testhelper" + clabernetesutil "github.com/srl-labs/clabernetes/util" +) + +func TestIndent(t *testing.T) { + cases := []struct { + name string + in string + indentCount int + expected string + }{ + { + name: "simple", + in: "a single line", + indentCount: 1, + expected: " a single line", + }, + { + name: "simple-more-indent", + in: "a single line", + indentCount: 4, + expected: " a single line", + }, + { + name: "multi-line", + in: "a single line\nanother line", + indentCount: 2, + expected: " a single line\n another line", + }, + } + + for _, testCase := range cases { + t.Run( + testCase.name, + func(t *testing.T) { + t.Logf("%s: starting", testCase.name) + + actual := clabernetesutil.Indent(testCase.in, testCase.indentCount) + + if actual != testCase.expected { + clabernetestesthelper.FailOutput(t, actual, testCase.expected) + } + }) + } +}