From 11f1b9eacd64427c8937c3540aa50e4690ec82b2 Mon Sep 17 00:00:00 2001 From: tuunit Date: Sun, 9 Feb 2025 16:44:07 +0100 Subject: [PATCH] add duration test --- pkg/apis/options/common_test.go | 87 ------------------------------- pkg/apis/options/duration_test.go | 82 +++++++++++++++++++++++++++++ pkg/apis/options/load.go | 17 ++++-- 3 files changed, 95 insertions(+), 91 deletions(-) delete mode 100644 pkg/apis/options/common_test.go create mode 100644 pkg/apis/options/duration_test.go diff --git a/pkg/apis/options/common_test.go b/pkg/apis/options/common_test.go deleted file mode 100644 index db33a58b..00000000 --- a/pkg/apis/options/common_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package options - -import ( - "encoding/json" - "errors" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Common", func() { - Context("Duration", func() { - type marshalJSONTableInput struct { - duration Duration - expectedJSON string - } - - DescribeTable("MarshalJSON", - func(in marshalJSONTableInput) { - data, err := in.duration.MarshalJSON() - Expect(err).ToNot(HaveOccurred()) - Expect(string(data)).To(Equal(in.expectedJSON)) - - var d Duration - Expect(json.Unmarshal(data, &d)).To(Succeed()) - Expect(d).To(Equal(in.duration)) - }, - Entry("30 seconds", marshalJSONTableInput{ - duration: Duration(30 * time.Second), - expectedJSON: "\"30s\"", - }), - Entry("1 minute", marshalJSONTableInput{ - duration: Duration(1 * time.Minute), - expectedJSON: "\"1m0s\"", - }), - Entry("1 hour 15 minutes", marshalJSONTableInput{ - duration: Duration(75 * time.Minute), - expectedJSON: "\"1h15m0s\"", - }), - Entry("A zero Duration", marshalJSONTableInput{ - duration: Duration(0), - expectedJSON: "\"0s\"", - }), - ) - - type unmarshalJSONTableInput struct { - json string - expectedErr error - expectedDuration Duration - } - - DescribeTable("UnmarshalJSON", - func(in unmarshalJSONTableInput) { - // A duration must be initialised pointer before UnmarshalJSON will work. - zero := Duration(0) - d := &zero - - err := d.UnmarshalJSON([]byte(in.json)) - if in.expectedErr != nil { - Expect(err).To(MatchError(in.expectedErr.Error())) - } else { - Expect(err).ToNot(HaveOccurred()) - } - Expect(d).ToNot(BeNil()) - Expect(*d).To(Equal(in.expectedDuration)) - }, - Entry("1m", unmarshalJSONTableInput{ - json: "\"1m\"", - expectedDuration: Duration(1 * time.Minute), - }), - Entry("30s", unmarshalJSONTableInput{ - json: "\"30s\"", - expectedDuration: Duration(30 * time.Second), - }), - Entry("1h15m", unmarshalJSONTableInput{ - json: "\"1h15m\"", - expectedDuration: Duration(75 * time.Minute), - }), - Entry("am", unmarshalJSONTableInput{ - json: "\"am\"", - expectedErr: errors.New("time: invalid duration \"am\""), - expectedDuration: Duration(0), - }), - ) - }) -}) diff --git a/pkg/apis/options/duration_test.go b/pkg/apis/options/duration_test.go new file mode 100644 index 00000000..fc1e77e3 --- /dev/null +++ b/pkg/apis/options/duration_test.go @@ -0,0 +1,82 @@ +package options + +import ( + "testing" + "time" +) + +func TestDecode(t *testing.T) { + type result struct { + Duration time.Duration `json:"duration"` + } + + tests := []struct { + name string + input map[string]interface{} + out result + expected time.Duration + expectedErr bool + }{ + { + name: "Valid String Duration with single unit", + input: map[string]interface{}{"duration": "3s"}, + out: result{}, + expected: 3 * time.Second, + expectedErr: false, + }, + { + name: "Valid String Duration with multiple units", + input: map[string]interface{}{"duration": "1h20m30s"}, + out: result{}, + expected: 1*time.Hour + 20*time.Minute + 30*time.Second, + expectedErr: false, + }, + { + name: "Valid Float Duration", + input: map[string]interface{}{"duration": 2.5}, + out: result{}, + expected: 2500 * time.Millisecond, + expectedErr: false, + }, + { + name: "Valid Int64 Duration", + input: map[string]interface{}{"duration": int64(5000000000)}, + out: result{}, + expected: 5 * time.Second, + expectedErr: false, + }, + { + name: "Invalid String", + input: map[string]interface{}{"duration": "invalid"}, + out: result{}, + expected: 0, + expectedErr: true, + }, + { + name: "Unsupported Type", + input: map[string]interface{}{"duration": true}, + out: result{}, + expected: 0, + expectedErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var result struct { + Duration time.Duration `json:"duration"` + } + + err := Decode(tt.input, &result) + if (err != nil) != tt.expectedErr { + t.Errorf("expected error: %v, got: %v", tt.expectedErr, err) + } + + if !tt.expectedErr { + if result.Duration != tt.expected { + t.Errorf("expected: %v, got: %v", tt.expected, result.Duration) + } + } + }) + } +} diff --git a/pkg/apis/options/load.go b/pkg/apis/options/load.go index 878199ed..2905af5f 100644 --- a/pkg/apis/options/load.go +++ b/pkg/apis/options/load.go @@ -69,16 +69,25 @@ func LoadYAML(configFileName string, opts interface{}) error { return fmt.Errorf("error unmarshalling config: %w", err) } - return Decode(intermediate, opts) -} - -func Decode(input interface{}, result interface{}) error { // Using mapstructure to decode arbitrary yaml structure into options and // merge with existing values instead of overwriting everything. This is especially // important as we have a lot of default values for boolean which are supposed to be // true by default. Normally by just parsing through yaml all booleans that aren't // referenced in the config file would be parsed as false and we cannot identify after // the fact if they have been explicitly set to false or have not been referenced. + return Decode(intermediate, opts) +} + +// Decode processes an input map and decodes it into a given struct while preserving default values. +// It ensures proper conversion of duration values from strings, floats, and int64 into time.Duration. +// +// Parameters: +// - input: A map[string]interface{} representing the input data. +// - result: A pointer to a struct where the decoded values will be stored. +// +// Returns: +// - An error if decoding fails or if there are unmapped keys. +func Decode(input interface{}, result interface{}) error { decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ DecodeHook: mapstructure.ComposeDecodeHookFunc(toDurationHookFunc()), Metadata: nil, // Don't track any metadata