add duration test

This commit is contained in:
tuunit 2025-02-09 16:44:07 +01:00 committed by Jan Larwig
parent 202de3a05e
commit 11f1b9eacd
No known key found for this signature in database
GPG Key ID: C2172BFA220A037A
3 changed files with 95 additions and 91 deletions

View File

@ -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),
}),
)
})
})

View File

@ -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)
}
}
})
}
}

View File

@ -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