diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..5139491 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,46 @@ +name: CI + +on: + pull_request: + branches: + - main + paths-ignore: + - .github/workflows/publish-helm-chart.yaml + - .github/workflows/publish-images.yaml + - .github/workflows/release-drafter.yaml + - '**.md' + - '.gitignore' + +jobs: + test: + runs-on: ubuntu-latest + name: Test + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: actions/setup-go@v3 + with: + go-version: '1.16.x' + + - run: go version + - uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Install kubebuilder + run: | + curl -L -O https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.2/kubebuilder_2.3.2_linux_amd64.tar.gz + tar zxvf kubebuilder_2.3.2_linux_amd64.tar.gz + sudo mv kubebuilder_2.3.2_linux_amd64 /usr/local/kubebuilder + + - name: Run tests + run: make test + + - name: Verify manifests are up-to-date + run: | + make manifests + git diff --exit-code diff --git a/Makefile b/Makefile index 13b89d6..336e58e 100644 --- a/Makefile +++ b/Makefile @@ -89,3 +89,75 @@ GOBIN=$(PROJECT_DIR)/bin go get $(2) ;\ rm -rf $$TMP_DIR ;\ } endef + +# find or download etcd +etcd: +ifeq (, $(shell which etcd)) +ifeq (, $(wildcard $(TEST_ASSETS)/etcd)) + @{ \ + set -xe ;\ + INSTALL_TMP_DIR=$$(mktemp -d) ;\ + cd $$INSTALL_TMP_DIR ;\ + wget https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.2/kubebuilder_2.3.2_$(OS_NAME)_amd64.tar.gz ;\ + mkdir -p $(TEST_ASSETS) ;\ + tar zxvf kubebuilder_2.3.2_$(OS_NAME)_amd64.tar.gz ;\ + mv kubebuilder_2.3.2_$(OS_NAME)_amd64/bin/etcd $(TEST_ASSETS)/etcd ;\ + mv kubebuilder_2.3.2_$(OS_NAME)_amd64/bin/kube-apiserver $(TEST_ASSETS)/kube-apiserver ;\ + mv kubebuilder_2.3.2_$(OS_NAME)_amd64/bin/kubectl $(TEST_ASSETS)/kubectl ;\ + rm -rf $$INSTALL_TMP_DIR ;\ + } +ETCD_BIN=$(TEST_ASSETS)/etcd +else +ETCD_BIN=$(TEST_ASSETS)/etcd +endif +else +ETCD_BIN=$(shell which etcd) +endif + +# find or download kube-apiserver +kube-apiserver: +ifeq (, $(shell which kube-apiserver)) +ifeq (, $(wildcard $(TEST_ASSETS)/kube-apiserver)) + @{ \ + set -xe ;\ + INSTALL_TMP_DIR=$$(mktemp -d) ;\ + cd $$INSTALL_TMP_DIR ;\ + wget https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.2/kubebuilder_2.3.2_$(OS_NAME)_amd64.tar.gz ;\ + mkdir -p $(TEST_ASSETS) ;\ + tar zxvf kubebuilder_2.3.2_$(OS_NAME)_amd64.tar.gz ;\ + mv kubebuilder_2.3.2_$(OS_NAME)_amd64/bin/etcd $(TEST_ASSETS)/etcd ;\ + mv kubebuilder_2.3.2_$(OS_NAME)_amd64/bin/kube-apiserver $(TEST_ASSETS)/kube-apiserver ;\ + mv kubebuilder_2.3.2_$(OS_NAME)_amd64/bin/kubectl $(TEST_ASSETS)/kubectl ;\ + rm -rf $$INSTALL_TMP_DIR ;\ + } +KUBE_APISERVER_BIN=$(TEST_ASSETS)/kube-apiserver +else +KUBE_APISERVER_BIN=$(TEST_ASSETS)/kube-apiserver +endif +else +KUBE_APISERVER_BIN=$(shell which kube-apiserver) +endif + +# find or download kubectl +kubectl: +ifeq (, $(shell which kubectl)) +ifeq (, $(wildcard $(TEST_ASSETS)/kubectl)) + @{ \ + set -xe ;\ + INSTALL_TMP_DIR=$$(mktemp -d) ;\ + cd $$INSTALL_TMP_DIR ;\ + wget https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.2/kubebuilder_2.3.2_$(OS_NAME)_amd64.tar.gz ;\ + mkdir -p $(TEST_ASSETS) ;\ + tar zxvf kubebuilder_2.3.2_$(OS_NAME)_amd64.tar.gz ;\ + mv kubebuilder_2.3.2_$(OS_NAME)_amd64/bin/etcd $(TEST_ASSETS)/etcd ;\ + mv kubebuilder_2.3.2_$(OS_NAME)_amd64/bin/kube-apiserver $(TEST_ASSETS)/kube-apiserver ;\ + mv kubebuilder_2.3.2_$(OS_NAME)_amd64/bin/kubectl $(TEST_ASSETS)/kubectl ;\ + rm -rf $$INSTALL_TMP_DIR ;\ + } +KUBECTL_BIN=$(TEST_ASSETS)/kubectl +else +KUBECTL_BIN=$(TEST_ASSETS)/kubectl +endif +else +KUBECTL_BIN=$(shell which kubectl) +endif diff --git a/agent/agent.go b/agent/agent.go index 7bb01ca..938032a 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -2,6 +2,7 @@ package agent import ( "context" + "reflect" "time" "github.com/go-logr/logr" @@ -46,6 +47,9 @@ func (r *VersionTrackerReconciler) Reconcile(ctx context.Context, req ctrl.Reque log.Info("starting reconciliation", "interval", r.Config.Interval) var v v1alpha1.VersionTracker + var status v1alpha1.VersionTrackerStatus + var sv SubjectVersion + if err := r.Get(ctx, req.NamespacedName, &v); err != nil { log.Error(err, "unable to fetch VersionTracker") reconciliationErrorsTotal.Inc() @@ -92,22 +96,48 @@ func (r *VersionTrackerReconciler) Reconcile(ctx context.Context, req ctrl.Reque return ctrl.Result{}, err } + status.ID = &v.Spec.Name + status.Namespace = &v.ObjectMeta.Namespace + status.LocalVersion = &v.Spec.LocalVersion + status.RemoteVersion = &v.Spec.RemoteVersion + // Get items based on the resource type items := GetItems(resources) if len(items) == 0 { log.Info("no resources found") + count := 0 + status.TotalResourceCount = &count } else { // Extract versions from resources - sv := r.ExtractSubjectVersion(v, items) - - // Ship the version information to the Control Plane - if len(sv.Versions) > 0 && r.Config.ControlPlaneUrl != "" { - err := r.ShipToControlPlane(sv) - if err != nil { - log.Error(err, "failed to ship the version to control plane") - reconciliationErrorsTotal.Inc() - return ctrl.Result{}, err - } + sv = r.ExtractSubjectVersion(v, items) + var uniqVersions []*string + for _, v := range sv.UniqVersions { + uniqVersions = append(uniqVersions, &v) + } + status.TotalResourceCount = &sv.TotalResourceCount + status.UniqVersions = uniqVersions + status.Versions = sv.Versions + } + + // Update the VersionTracker status + if !reflect.DeepEqual(v.Status, status) { + updated := v.DeepCopy() + updated.Status = status + if err := r.Status().Patch(ctx, updated, client.MergeFrom(&v)); err != nil { + log.Info("Failed to patch VersionTracker", "error", err) + return ctrl.Result{ + Requeue: true, + }, nil + } + } + + // Ship the version information to the Control Plane + if len(sv.Versions) > 0 && r.Config.ControlPlaneUrl != "" { + err := r.ShipToControlPlane(sv) + if err != nil { + log.Error(err, "failed to ship the version to control plane") + reconciliationErrorsTotal.Inc() + return ctrl.Result{}, err } } @@ -115,6 +145,7 @@ func (r *VersionTrackerReconciler) Reconcile(ctx context.Context, req ctrl.Reque lastReconciliationTimestamp.SetToCurrentTime() reconciliationDuration.Set(float64(elapsed.Milliseconds())) log.Info("done reconciling", "interval", r.Config.Interval) + return ctrl.Result{ RequeueAfter: r.Config.Interval, }, nil diff --git a/agent/api/v1alpha1/versiontracker_types.go b/agent/api/v1alpha1/versiontracker_types.go index dce6eb8..d2c9b26 100644 --- a/agent/api/v1alpha1/versiontracker_types.go +++ b/agent/api/v1alpha1/versiontracker_types.go @@ -145,8 +145,22 @@ type Regex struct { Result string `json:"result"` } +type Version struct { + ResourceCount int `json:"resourceCount"` + ResourceKind string `json:"resourceKind"` + ExtractedFrom string `json:"extractedFrom"` + Version string `json:"version"` +} + // VersionTrackerStatus defines the observed state of VersionTracker type VersionTrackerStatus struct { + ID *string `json:"id,omitempty"` + Namespace *string `json:"namespace,omitempty"` + TotalResourceCount *int `json:"totalResourceCount,omitempty"` + UniqVersions []*string `json:"uniqVersions,omitempty"` + Versions []*Version `json:"versions,omitempty"` + LocalVersion *LocalVersion `json:"localVersion,omitempty"` + RemoteVersion *RemoteVersion `json:"remoteVersion,omitempty"` } //+kubebuilder:object:root=true diff --git a/agent/api/v1alpha1/zz_generated.deepcopy.go b/agent/api/v1alpha1/zz_generated.deepcopy.go index 14cb513..a0de522 100644 --- a/agent/api/v1alpha1/zz_generated.deepcopy.go +++ b/agent/api/v1alpha1/zz_generated.deepcopy.go @@ -113,13 +113,28 @@ func (in *Resources) DeepCopy() *Resources { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Version) DeepCopyInto(out *Version) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Version. +func (in *Version) DeepCopy() *Version { + if in == nil { + return nil + } + out := new(Version) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VersionTracker) DeepCopyInto(out *VersionTracker) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VersionTracker. @@ -193,6 +208,53 @@ func (in *VersionTrackerSpec) DeepCopy() *VersionTrackerSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VersionTrackerStatus) DeepCopyInto(out *VersionTrackerStatus) { *out = *in + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **out = **in + } + if in.Namespace != nil { + in, out := &in.Namespace, &out.Namespace + *out = new(string) + **out = **in + } + if in.TotalResourceCount != nil { + in, out := &in.TotalResourceCount, &out.TotalResourceCount + *out = new(int) + **out = **in + } + if in.UniqVersions != nil { + in, out := &in.UniqVersions, &out.UniqVersions + *out = make([]*string, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(string) + **out = **in + } + } + } + if in.Versions != nil { + in, out := &in.Versions, &out.Versions + *out = make([]*Version, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Version) + **out = **in + } + } + } + if in.LocalVersion != nil { + in, out := &in.LocalVersion, &out.LocalVersion + *out = new(LocalVersion) + **out = **in + } + if in.RemoteVersion != nil { + in, out := &in.RemoteVersion, &out.RemoteVersion + *out = new(RemoteVersion) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VersionTrackerStatus. diff --git a/agent/extract_version.go b/agent/extract_version.go index fdf0fac..c8325da 100644 --- a/agent/extract_version.go +++ b/agent/extract_version.go @@ -17,17 +17,10 @@ type SubjectVersion struct { Namespace string TotalResourceCount int UniqVersions []string - Versions []*Version + Versions []*v1alpha1.Version RemoteVersion v1alpha1.RemoteVersion } -type Version struct { - ResourceCount int - ResourceKind string - ExtractedFrom string - Version string -} - // ExtractSubjectVersion looks at the feild of each individuel resource and extracts the version // based on the extraction configuration in the VersionTracker func (r *VersionTrackerReconciler) ExtractSubjectVersion(v v1alpha1.VersionTracker, items []interface{}) SubjectVersion { @@ -77,7 +70,7 @@ func (r *VersionTrackerReconciler) ExtractSubjectVersion(v v1alpha1.VersionTrack // add the version to the list of unique versions if it's not already there if !utils.Contains(uniqueVersions, version) { uniqueVersions = append(uniqueVersions, version) - appVersion.Versions = append(appVersion.Versions, &Version{ + appVersion.Versions = append(appVersion.Versions, &v1alpha1.Version{ Version: version, ExtractedFrom: fieldValue, ResourceKind: v.GetResourceKind(), diff --git a/agent/utils_test.go b/agent/utils_test.go new file mode 100644 index 0000000..29f25fd --- /dev/null +++ b/agent/utils_test.go @@ -0,0 +1,64 @@ +package agent + +import ( + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" +) + +func Test_getFeilds(t *testing.T) { + type args struct { + jsonPath string + resource interface{} + } + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "Test_getFeilds", + args: args{ + jsonPath: ".status.nodeInfo.kubeletVersion", + resource: &corev1.Node{ + Status: corev1.NodeStatus{ + NodeInfo: corev1.NodeSystemInfo{ + KubeletVersion: "v1.18.0", + }, + }, + }, + }, + want: []string{"v1.18.0"}, + wantErr: false, + }, + { + name: "Test_getFeilds_invalid_jsonpath", + args: args{ + jsonPath: "invalid_jsonpath", + resource: &corev1.Node{ + Status: corev1.NodeStatus{ + NodeInfo: corev1.NodeSystemInfo{ + KubeletVersion: "v1.18.0", + }, + }, + }, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getFeilds(tt.args.jsonPath, tt.args.resource) + if (err != nil) != tt.wantErr { + t.Errorf("getFeilds() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getFeilds() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/charts/opvic/crds/opvic.skillz.com_versiontrackers.yaml b/charts/opvic/crds/opvic.skillz.com_versiontrackers.yaml index 1e9876a..0aab5c3 100644 --- a/charts/opvic/crds/opvic.skillz.com_versiontrackers.yaml +++ b/charts/opvic/crds/opvic.skillz.com_versiontrackers.yaml @@ -174,6 +174,102 @@ spec: type: object status: description: VersionTrackerStatus defines the observed state of VersionTracker + properties: + id: + type: string + localVersion: + properties: + extraction: + properties: + regex: + description: Regex to extract the version from the field + properties: + pattern: + description: Regex pattern to extract the version from + the field + type: string + result: + default: $1 + type: string + required: + - pattern + - result + type: object + type: object + fieldSelector: + description: Jsonpath to extract the version from the resource + type: string + strategy: + default: ImageTag + type: string + required: + - strategy + type: object + namespace: + type: string + remoteVersion: + properties: + chart: + description: Helm chart name to track. Required if `provider` + is `helm-repo` + type: string + constraint: + type: string + extraction: + properties: + regex: + description: Regex to extract the version from the field + properties: + pattern: + description: Regex pattern to extract the version from + the field + type: string + result: + default: $1 + type: string + required: + - pattern + - result + type: object + type: object + provider: + default: github + type: string + repo: + description: Repository to get the remote version from. e.g owner/repo + or https://charts.bitnami.com/bitnami + type: string + strategy: + type: string + required: + - provider + - repo + - strategy + type: object + totalResourceCount: + type: integer + uniqVersions: + items: + type: string + type: array + versions: + items: + properties: + extractedFrom: + type: string + resourceCount: + type: integer + resourceKind: + type: string + version: + type: string + required: + - extractedFrom + - resourceCount + - resourceKind + - version + type: object + type: array type: object type: object served: true diff --git a/config/crd/bases/opvic.skillz.com_versiontrackers.yaml b/config/crd/bases/opvic.skillz.com_versiontrackers.yaml index 1e9876a..0aab5c3 100644 --- a/config/crd/bases/opvic.skillz.com_versiontrackers.yaml +++ b/config/crd/bases/opvic.skillz.com_versiontrackers.yaml @@ -174,6 +174,102 @@ spec: type: object status: description: VersionTrackerStatus defines the observed state of VersionTracker + properties: + id: + type: string + localVersion: + properties: + extraction: + properties: + regex: + description: Regex to extract the version from the field + properties: + pattern: + description: Regex pattern to extract the version from + the field + type: string + result: + default: $1 + type: string + required: + - pattern + - result + type: object + type: object + fieldSelector: + description: Jsonpath to extract the version from the resource + type: string + strategy: + default: ImageTag + type: string + required: + - strategy + type: object + namespace: + type: string + remoteVersion: + properties: + chart: + description: Helm chart name to track. Required if `provider` + is `helm-repo` + type: string + constraint: + type: string + extraction: + properties: + regex: + description: Regex to extract the version from the field + properties: + pattern: + description: Regex pattern to extract the version from + the field + type: string + result: + default: $1 + type: string + required: + - pattern + - result + type: object + type: object + provider: + default: github + type: string + repo: + description: Repository to get the remote version from. e.g owner/repo + or https://charts.bitnami.com/bitnami + type: string + strategy: + type: string + required: + - provider + - repo + - strategy + type: object + totalResourceCount: + type: integer + uniqVersions: + items: + type: string + type: array + versions: + items: + properties: + extractedFrom: + type: string + resourceCount: + type: integer + resourceKind: + type: string + version: + type: string + required: + - extractedFrom + - resourceCount + - resourceKind + - version + type: object + type: array type: object type: object served: true diff --git a/controlplane/providers/github/github.go b/controlplane/providers/github/github.go index 4d11f7a..5967d64 100644 --- a/controlplane/providers/github/github.go +++ b/controlplane/providers/github/github.go @@ -151,7 +151,7 @@ func (p *Provider) getTags(repo string) ([]*github.RepositoryTag, error) { if err != nil { return nil, err } - // get releases by pagination (max 100) + // get tags by pagination (max 100) opt := &github.ListOptions{ PerPage: 100, } diff --git a/utils/utils_test.go b/utils/utils_test.go new file mode 100644 index 0000000..d641104 --- /dev/null +++ b/utils/utils_test.go @@ -0,0 +1,248 @@ +package utils + +import ( + "reflect" + "testing" +) + +func TestGetResultsFromRegex(t *testing.T) { + type args struct { + pattern string + tmpl string + content string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "correct_result", + args: args{ + pattern: `^my-app-([0-9]+\.[0-9]+\.[0-9]+)$`, + tmpl: `$1`, + content: `my-app-0.0.1`, + }, + want: "0.0.1", + wantErr: false, + }, + { + name: "empty_result", + args: args{ + pattern: `^my-app-([0-9]+\.[0-9]+\.[0-9]+)$`, + tmpl: `$1`, + content: `my-app-0.0.1-SNAPSHOT`, + }, + want: "", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetResultsFromRegex(tt.args.pattern, tt.args.tmpl, tt.args.content) + if (err != nil) != tt.wantErr { + t.Errorf("GetResultsFromRegex() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetResultsFromRegex() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMatchPattern(t *testing.T) { + type args struct { + pattern string + tmpl string + version string + } + tests := []struct { + name string + args args + want bool + want1 string + wantErr bool + }{ + { + name: "should_match", + args: args{ + pattern: `^my-app-([0-9]+\.[0-9]+\.[0-9]+)$`, + tmpl: `$1`, + version: "my-app-0.0.1", + }, + want: true, + want1: "0.0.1", + wantErr: false, + }, + { + name: "should_not_match", + args: args{ + pattern: `^my-app-([0-9]+\.[0-9]+\.[0-9]+)$`, + tmpl: `$1`, + version: "my-app-0.0.1-SNAPSHOT", + }, + want: false, + want1: "", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1, err := MatchPattern(tt.args.pattern, tt.args.tmpl, tt.args.version) + if (err != nil) != tt.wantErr { + t.Errorf("MatchPattern() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("MatchPattern() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("MatchPattern() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestMeetConstraint(t *testing.T) { + type args struct { + constraint string + ver string + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + { + name: "meet_constraint", + args: args{ + constraint: ">=0.0.1", + ver: "0.0.1", + }, + want: true, + wantErr: false, + }, + { + name: "not_meet_constraint", + args: args{ + constraint: ">=0.0.1", + ver: "0.0.0", + }, + want: false, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := MeetConstraint(tt.args.constraint, tt.args.ver) + if (err != nil) != tt.wantErr { + t.Errorf("MeetConstraint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("MeetConstraint() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestContains(t *testing.T) { + type args struct { + l []string + s string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "contains", + args: args{ + l: []string{"a", "b", "c"}, + s: "a", + }, + want: true, + }, + { + name: "not_contains", + args: args{ + l: []string{"a", "b", "c"}, + s: "d", + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Contains(tt.args.l, tt.args.s); got != tt.want { + t.Errorf("Contains() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestContainsInt(t *testing.T) { + type args struct { + l []int + i int + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "contains", + args: args{ + l: []int{1, 2, 3}, + i: 1, + }, + want: true, + }, + { + name: "not_contains", + args: args{ + l: []int{1, 2, 3}, + i: 4, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ContainsInt(tt.args.l, tt.args.i); got != tt.want { + t.Errorf("ContainsInt() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRemoveDuplicateStr(t *testing.T) { + type args struct { + strSlice []string + } + tests := []struct { + name string + args args + want []string + }{ + { + name: "remove_duplicate", + args: args{ + strSlice: []string{"a", "b", "c", "a", "d", "c"}, + }, + want: []string{"a", "b", "c", "d"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := RemoveDuplicateStr(tt.args.strSlice); !reflect.DeepEqual(got, tt.want) { + t.Errorf("RemoveDuplicateStr() = %v, want %v", got, tt.want) + } + }) + } +}