From 6397aedadf4f083881f465bd51397c1d064fa239 Mon Sep 17 00:00:00 2001 From: Bo Liu Date: Fri, 20 Dec 2024 11:33:05 +0800 Subject: [PATCH] refactor(utils): add hasher pkg and add more uts for toml (#5992) Signed-off-by: liubo02 --- pkg/controllers/pd/tasks/cm.go | 3 +- pkg/controllers/tidb/tasks/cm.go | 3 +- pkg/controllers/tiflash/tasks/cm.go | 3 +- pkg/controllers/tikv/tasks/cm.go | 3 +- pkg/utils/hasher/hasher.go | 41 +++ pkg/utils/hasher/hasher_test.go | 142 +++++++ pkg/utils/toml/toml.go | 42 +-- pkg/utils/toml/toml_test.go | 549 +++++++++++++++++++++------- 8 files changed, 620 insertions(+), 166 deletions(-) create mode 100644 pkg/utils/hasher/hasher.go create mode 100644 pkg/utils/hasher/hasher_test.go diff --git a/pkg/controllers/pd/tasks/cm.go b/pkg/controllers/pd/tasks/cm.go index 11798b6f65..0845bd4cd1 100644 --- a/pkg/controllers/pd/tasks/cm.go +++ b/pkg/controllers/pd/tasks/cm.go @@ -22,6 +22,7 @@ import ( "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/client" pdcfg "github.com/pingcap/tidb-operator/pkg/configs/pd" + "github.com/pingcap/tidb-operator/pkg/utils/hasher" maputil "github.com/pingcap/tidb-operator/pkg/utils/map" "github.com/pingcap/tidb-operator/pkg/utils/task/v3" "github.com/pingcap/tidb-operator/pkg/utils/toml" @@ -46,7 +47,7 @@ func TaskConfigMap(ctx *ReconcileContext, _ logr.Logger, c client.Client) task.T return task.Fail().With("pd config cannot be encoded: %v", err) } - hash, err := toml.GenerateHash(ctx.PD.Spec.Config) + hash, err := hasher.GenerateHash(ctx.PD.Spec.Config) if err != nil { return task.Fail().With("failed to generate hash for `pd.spec.config`: %v", err) } diff --git a/pkg/controllers/tidb/tasks/cm.go b/pkg/controllers/tidb/tasks/cm.go index 6f881c78a7..498fee52b5 100644 --- a/pkg/controllers/tidb/tasks/cm.go +++ b/pkg/controllers/tidb/tasks/cm.go @@ -22,6 +22,7 @@ import ( "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/client" tidbcfg "github.com/pingcap/tidb-operator/pkg/configs/tidb" + "github.com/pingcap/tidb-operator/pkg/utils/hasher" maputil "github.com/pingcap/tidb-operator/pkg/utils/map" "github.com/pingcap/tidb-operator/pkg/utils/task/v2" "github.com/pingcap/tidb-operator/pkg/utils/toml" @@ -61,7 +62,7 @@ func (t *TaskConfigMap) Sync(ctx task.Context[ReconcileContext]) task.Result { return task.Fail().With("tidb config cannot be encoded: %w", err) } - rtx.ConfigHash, err = toml.GenerateHash(rtx.TiDB.Spec.Config) + rtx.ConfigHash, err = hasher.GenerateHash(rtx.TiDB.Spec.Config) if err != nil { return task.Fail().With("failed to generate hash for `tidb.spec.config`: %w", err) } diff --git a/pkg/controllers/tiflash/tasks/cm.go b/pkg/controllers/tiflash/tasks/cm.go index 4919f484f5..d0c3e89dd2 100644 --- a/pkg/controllers/tiflash/tasks/cm.go +++ b/pkg/controllers/tiflash/tasks/cm.go @@ -22,6 +22,7 @@ import ( "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/client" tiflashcfg "github.com/pingcap/tidb-operator/pkg/configs/tiflash" + "github.com/pingcap/tidb-operator/pkg/utils/hasher" maputil "github.com/pingcap/tidb-operator/pkg/utils/map" "github.com/pingcap/tidb-operator/pkg/utils/task/v2" "github.com/pingcap/tidb-operator/pkg/utils/toml" @@ -72,7 +73,7 @@ func (t *TaskConfigMap) Sync(ctx task.Context[ReconcileContext]) task.Result { return task.Fail().With("tiflash proxy config cannot be encoded: %w", err) } - rtx.ConfigHash, err = toml.GenerateHash(rtx.TiFlash.Spec.Config) + rtx.ConfigHash, err = hasher.GenerateHash(rtx.TiFlash.Spec.Config) if err != nil { return task.Fail().With("failed to generate hash for `tiflash.spec.config`: %w", err) } diff --git a/pkg/controllers/tikv/tasks/cm.go b/pkg/controllers/tikv/tasks/cm.go index b4dc0417c6..23b0540e65 100644 --- a/pkg/controllers/tikv/tasks/cm.go +++ b/pkg/controllers/tikv/tasks/cm.go @@ -22,6 +22,7 @@ import ( "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/client" tikvcfg "github.com/pingcap/tidb-operator/pkg/configs/tikv" + "github.com/pingcap/tidb-operator/pkg/utils/hasher" maputil "github.com/pingcap/tidb-operator/pkg/utils/map" "github.com/pingcap/tidb-operator/pkg/utils/task/v2" "github.com/pingcap/tidb-operator/pkg/utils/toml" @@ -60,7 +61,7 @@ func (t *TaskConfigMap) Sync(ctx task.Context[ReconcileContext]) task.Result { return task.Fail().With("tikv config cannot be encoded: %w", err) } - rtx.ConfigHash, err = toml.GenerateHash(rtx.TiKV.Spec.Config) + rtx.ConfigHash, err = hasher.GenerateHash(rtx.TiKV.Spec.Config) if err != nil { return task.Fail().With("failed to generate hash for `tikv.spec.config`: %w", err) } diff --git a/pkg/utils/hasher/hasher.go b/pkg/utils/hasher/hasher.go new file mode 100644 index 0000000000..71e5cc08ad --- /dev/null +++ b/pkg/utils/hasher/hasher.go @@ -0,0 +1,41 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hasher + +import ( + "bytes" + "fmt" + "hash/fnv" + + "github.com/pelletier/go-toml/v2" + "k8s.io/apimachinery/pkg/util/rand" + + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" + hashutil "github.com/pingcap/tidb-operator/third_party/kubernetes/pkg/util/hash" +) + +// GenerateHash takes a TOML string as input, unmarshals it into a map, +// and generates a hash of the resulting configuration. The hash is then +// encoded into a safe string format and returned. +// If the order of keys in the TOML string is different, the hash will be the same. +func GenerateHash(tomlStr v1alpha1.ConfigFile) (string, error) { + var config map[string]any + if err := toml.NewDecoder(bytes.NewReader([]byte(tomlStr))).Decode(&config); err != nil { + return "", fmt.Errorf("failed to unmarshal toml string %s: %w", tomlStr, err) + } + hasher := fnv.New32a() + hashutil.DeepHashObject(hasher, config) + return rand.SafeEncodeString(fmt.Sprint(hasher.Sum32())), nil +} diff --git a/pkg/utils/hasher/hasher_test.go b/pkg/utils/hasher/hasher_test.go new file mode 100644 index 0000000000..f54c3ca2be --- /dev/null +++ b/pkg/utils/hasher/hasher_test.go @@ -0,0 +1,142 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hasher + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" +) + +func TestGenerateHash(t *testing.T) { + tests := []struct { + name string + tomlStr v1alpha1.ConfigFile + semanticallyEquivalentStr v1alpha1.ConfigFile + wantHash string + wantError bool + }{ + { + name: "Valid TOML string", + tomlStr: v1alpha1.ConfigFile(`foo = 'bar' +[log] +k1 = 'v1' +k2 = 'v2'`), + semanticallyEquivalentStr: v1alpha1.ConfigFile(`foo = 'bar' +[log] +k2 = 'v2' +k1 = 'v1'`), + wantHash: "5dbbcf4574", + wantError: false, + }, + { + name: "Different config value", + tomlStr: v1alpha1.ConfigFile(`foo = 'foo' +[log] +k2 = 'v2' +k1 = 'v1'`), + wantHash: "f5bc46cb9", + wantError: false, + }, + { + name: "multiple sections with blank line", + tomlStr: v1alpha1.ConfigFile(`[a] +k1 = 'v1' +[b] +k2 = 'v2'`), + semanticallyEquivalentStr: v1alpha1.ConfigFile(`[a] +k1 = 'v1' +[b] + +k2 = 'v2'`), + wantHash: "79598d5977", + wantError: false, + }, + { + name: "Empty TOML string", + tomlStr: v1alpha1.ConfigFile(``), + wantHash: "7d6fc488b7", + wantError: false, + }, + { + name: "Invalid TOML string", + tomlStr: v1alpha1.ConfigFile(`key1 = "value1" + key2 = value2`), // Missing quotes around value2 + wantHash: "", + wantError: true, + }, + { + name: "Nested tables", + tomlStr: v1alpha1.ConfigFile(`[parent] +child1 = "value1" +child2 = "value2" +[parent.child] +grandchild1 = "value3" +grandchild2 = "value4"`), + semanticallyEquivalentStr: v1alpha1.ConfigFile(`[parent] +child2 = "value2" +child1 = "value1" +[parent.child] +grandchild2 = "value4" +grandchild1 = "value3"`), + wantHash: "7bf645ccb4", + wantError: false, + }, + { + name: "Array of tables", + tomlStr: v1alpha1.ConfigFile(`[[products]] +name = "Hammer" +sku = 738594937 + +[[products]] +name = "Nail" +sku = 284758393 + +color = "gray"`), + semanticallyEquivalentStr: v1alpha1.ConfigFile(`[[products]] +sku = 738594937 +name = "Hammer" + +[[products]] +sku = 284758393 +name = "Nail" + +color = "gray"`), + wantHash: "7549cf87f4", + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotHash, err := GenerateHash(tt.tomlStr) + if tt.wantError { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.wantHash, gotHash) + + if string(tt.semanticallyEquivalentStr) != "" { + reorderedHash, err := GenerateHash(tt.semanticallyEquivalentStr) + require.NoError(t, err) + assert.Equal(t, tt.wantHash, reorderedHash) + } + } + }) + } +} diff --git a/pkg/utils/toml/toml.go b/pkg/utils/toml/toml.go index 06f68fead8..93cc852a77 100644 --- a/pkg/utils/toml/toml.go +++ b/pkg/utils/toml/toml.go @@ -17,15 +17,11 @@ package toml import ( "bytes" "fmt" - "hash/fnv" "reflect" + "strings" "github.com/mitchellh/mapstructure" "github.com/pelletier/go-toml/v2" - "k8s.io/apimachinery/pkg/util/rand" - - "github.com/pingcap/tidb-operator/apis/core/v1alpha1" - hashutil "github.com/pingcap/tidb-operator/third_party/kubernetes/pkg/util/hash" ) type Decoder[T any, PT *T] interface { @@ -57,6 +53,7 @@ func (c *codec[T, PT]) Decode(data []byte, obj PT) error { Result: obj, }) if err != nil { + // unreachable return err } if err := decoder.Decode(raw); err != nil { @@ -69,7 +66,7 @@ func (c *codec[T, PT]) Decode(data []byte, obj PT) error { } func (c *codec[T, PT]) Encode(obj PT) ([]byte, error) { - if err := Overwrite(obj, c.raw); err != nil { + if err := overwrite(obj, c.raw); err != nil { return nil, err } @@ -81,7 +78,7 @@ func (c *codec[T, PT]) Encode(obj PT) ([]byte, error) { return buf.Bytes(), nil } -func Overwrite(obj any, m map[string]any) error { +func overwrite(obj any, m map[string]any) error { structVal := reflect.ValueOf(obj).Elem() fieldTypes := reflect.VisibleFields(structVal.Type()) for _, fieldType := range fieldTypes { @@ -89,17 +86,22 @@ func Overwrite(obj any, m map[string]any) error { continue } - name := fieldType.Tag.Get("toml") - src := structVal.FieldByIndex(fieldType.Index) - - for src.Kind() == reflect.Pointer { - src = src.Elem() + tag := fieldType.Tag.Get("toml") + parts := strings.Split(tag, ",") + name := fieldType.Name + if len(parts) != 0 { + name = parts[0] } + src := structVal.FieldByIndex(fieldType.Index) if src.IsZero() { continue } + for src.Kind() == reflect.Pointer { + src = src.Elem() + } + v, ok := m[name] if !ok { m[name] = src.Interface() @@ -123,7 +125,7 @@ func getField(src reflect.Value, dst any) (any, error) { if !ok { return nil, fmt.Errorf("type mismatched, expected map, actual %T", dst) } - if err := Overwrite(src.Addr().Interface(), vm); err != nil { + if err := overwrite(src.Addr().Interface(), vm); err != nil { return nil, err } @@ -154,17 +156,3 @@ func getField(src reflect.Value, dst any) (any, error) { return src.Interface(), nil } } - -// GenerateHash takes a TOML string as input, unmarshals it into a map, -// and generates a hash of the resulting configuration. The hash is then -// encoded into a safe string format and returned. -// If the order of keys in the TOML string is different, the hash will be the same. -func GenerateHash(tomlStr v1alpha1.ConfigFile) (string, error) { - var config map[string]any - if err := toml.NewDecoder(bytes.NewReader([]byte(tomlStr))).Decode(&config); err != nil { - return "", fmt.Errorf("failed to unmarshal toml string %s: %w", tomlStr, err) - } - hasher := fnv.New32a() - hashutil.DeepHashObject(hasher, config) - return rand.SafeEncodeString(fmt.Sprint(hasher.Sum32())), nil -} diff --git a/pkg/utils/toml/toml_test.go b/pkg/utils/toml/toml_test.go index 90f425b897..e80c95c794 100644 --- a/pkg/utils/toml/toml_test.go +++ b/pkg/utils/toml/toml_test.go @@ -20,35 +20,62 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/pingcap/tidb-operator/apis/core/v1alpha1" ) type TestType struct { - String string `toml:"string"` + String string `toml:"string,omitempty"` + + Struct *TestType `toml:"struct"` + Array []TestType `toml:"array,omitempty"` + + unexported string } +const changedVal = "changed" + func TestCodec(t *testing.T) { cases := []struct { - desc string - input []byte - output []byte - obj TestType - change func(obj *TestType) *TestType + desc string + input []byte + output []byte + obj TestType + change func(obj *TestType) *TestType + hasDecodeErr bool }{ { desc: "empty input", input: []byte(``), output: []byte(` string = 'changed' + +[[array]] +string = 'arr1' + +[[array]] +string = 'arr2' + +[struct] +string = 'struct' `), change: func(obj *TestType) *TestType { - obj.String = "changed" + obj.unexported = "will be skipped" + obj.String = changedVal + obj.Struct = &TestType{ + String: "struct", + } + obj.Array = []TestType{ + { + String: "arr1", + }, + { + String: "arr2", + }, + } return obj }, }, { - desc: "reserve unknown field", + desc: "reserve unknown fields", input: []byte(` unknown = 'xxx' unknown_int = 10 @@ -67,7 +94,7 @@ unknown = 'yyy' unknown = 'yyy' `), output: []byte(` -string = 'mmm' +string = 'changed' unknown = 'xxx' unknown_int = 10 @@ -85,25 +112,389 @@ unknown = 'yyy' unknown = 'yyy' `), change: func(obj *TestType) *TestType { - obj.String = "mmm" + obj.String = changedVal + return obj + }, + }, + { + desc: "change string and reserve unknown fields", + input: []byte(` +string = 'aaa' +unknown = 'xxx' + `), + output: []byte(` +string = 'changed' +unknown = 'xxx' + `), + change: func(obj *TestType) *TestType { + obj.String = changedVal + return obj + }, + }, + { + desc: "add struct", + input: []byte(` +string = 'aaa' +unknown = 'xxx' + `), + output: []byte(` +string = 'aaa' +unknown = 'xxx' + +[struct] +string = 'changed' + `), + change: func(obj *TestType) *TestType { + obj.Struct = &TestType{ + String: changedVal, + } + return obj + }, + }, + { + desc: "change struct and reserve unknown fields", + input: []byte(` +string = 'aaa' +unknown = 'xxx' + +[struct] +string = 'aaa' +unknown = 'xxx' + `), + output: []byte(` +string = 'aaa' +unknown = 'xxx' + +[struct] +string = 'changed' +unknown = 'xxx' + `), + change: func(obj *TestType) *TestType { + obj.Struct = &TestType{ + String: changedVal, + } + return obj + }, + }, + { + desc: "add struct in struct", + input: []byte(` +string = 'aaa' +unknown = 'xxx' + `), + output: []byte(` +string = 'aaa' +unknown = 'xxx' + +[struct] +[struct.struct] +string = 'changed' + `), + change: func(obj *TestType) *TestType { + obj.Struct = &TestType{ + Struct: &TestType{ + String: changedVal, + }, + } + return obj + }, + }, + { + desc: "change struct and add struct in struct", + input: []byte(` +string = 'aaa' +unknown = 'xxx' + +[struct] +string = 'aaa' +unknown = 'xxx' + `), + output: []byte(` +string = 'aaa' +unknown = 'xxx' + +[struct] +string = 'aaa' +unknown = 'xxx' + +[struct.struct] +string = 'changed' + `), + change: func(obj *TestType) *TestType { + obj.Struct = &TestType{ + Struct: &TestType{ + String: changedVal, + }, + } + return obj + }, + }, + { + desc: "change struct in struct and reserve unknown fields", + input: []byte(` +string = 'aaa' +unknown = 'xxx' + +[struct] +[struct.struct] +string = 'aaa' +unknown = 'xxx' + `), + output: []byte(` +string = 'aaa' +unknown = 'xxx' + +[struct] +[struct.struct] +string = 'changed' +unknown = 'xxx' + `), + change: func(obj *TestType) *TestType { + obj.Struct = &TestType{ + Struct: &TestType{ + String: changedVal, + }, + } + return obj + }, + }, + { + desc: "add array in struct", + input: []byte(` +string = 'aaa' +unknown = 'xxx' + `), + output: []byte(` +string = 'aaa' +unknown = 'xxx' + +[struct] +[[struct.array]] +string = 'changed' + `), + change: func(obj *TestType) *TestType { + obj.Struct = &TestType{ + Array: []TestType{ + { + String: changedVal, + }, + }, + } + return obj + }, + }, + { + desc: "change array in struct and reserve unknown fields", + input: []byte(` +string = 'aaa' +unknown = 'xxx' + +[struct] +[[struct.array]] +string = 'aaa' +unknown = 'xxx' + `), + output: []byte(` +string = 'aaa' +unknown = 'xxx' + +[struct] +[[struct.array]] +string = 'changed' +unknown = 'xxx' + `), + change: func(obj *TestType) *TestType { + obj.Struct = &TestType{ + Array: []TestType{ + { + String: changedVal, + }, + }, + } return obj }, }, { - desc: "change existing field", + desc: "add array", input: []byte(` string = 'aaa' unknown = 'xxx' `), output: []byte(` -string = 'yyy' +string = 'aaa' unknown = 'xxx' + +[[array]] +string = 'changed' `), change: func(obj *TestType) *TestType { - obj.String = "yyy" + obj.Array = []TestType{ + { + String: changedVal, + }, + } return obj }, }, + { + desc: "change array and reserve unknown fields", + input: []byte(` +string = 'aaa' +unknown = 'xxx' + +[[array]] +string = 'aaa' +unknown = 'xxx' + `), + output: []byte(` +string = 'aaa' +unknown = 'xxx' + +[[array]] +string = 'changed' +unknown = 'xxx' + `), + change: func(obj *TestType) *TestType { + obj.Array = []TestType{ + { + String: changedVal, + }, + } + return obj + }, + }, + { + desc: "append new item to array", + input: []byte(` +string = 'aaa' +unknown = 'xxx' + +[[array]] +string = 'aaa' +unknown = 'xxx' + `), + output: []byte(` +string = 'aaa' +unknown = 'xxx' + +[[array]] +string = 'aaa' +unknown = 'xxx' + +[[array]] +string = 'changed' + `), + change: func(obj *TestType) *TestType { + obj.Array = append(obj.Array, TestType{ + String: changedVal, + }) + return obj + }, + }, + { + desc: "del existing item in array(useless)", + input: []byte(` +string = 'aaa' +unknown = 'xxx' + +[[array]] +string = 'aaa' +unknown = 'xxx' + +[[array]] +string = 'bbb' +unknown = 'xxx' + `), + output: []byte(` +string = 'aaa' +unknown = 'xxx' + +[[array]] +string = 'aaa' +unknown = 'xxx' + +[[array]] +string = 'bbb' +unknown = 'xxx' + `), + change: func(obj *TestType) *TestType { + obj.Array = obj.Array[:1] + return obj + }, + }, + { + desc: "add struct in array", + input: []byte(` +string = 'aaa' +unknown = 'xxx' + +[[array]] +string = 'aaa' +unknown = 'xxx' + `), + output: []byte(` +string = 'aaa' +unknown = 'xxx' + +[[array]] +string = 'aaa' +unknown = 'xxx' + +[array.struct] +string = 'changed' + `), + change: func(obj *TestType) *TestType { + obj.Array[0].Struct = &TestType{ + String: changedVal, + } + return obj + }, + }, + { + desc: "change struct in array", + input: []byte(` +string = 'aaa' +unknown = 'xxx' + +[[array]] +string = 'aaa' +unknown = 'xxx' + +[array.struct] +string = 'aaa' +unknown = 'xxx' + `), + output: []byte(` +string = 'aaa' +unknown = 'xxx' + +[[array]] +string = 'aaa' +unknown = 'xxx' + +[array.struct] +string = 'changed' +unknown = 'xxx' + `), + change: func(obj *TestType) *TestType { + obj.Array[0].Struct = &TestType{ + String: changedVal, + } + return obj + }, + }, + { + desc: "invalid toml", + input: []byte(`xxx`), + hasDecodeErr: true, + }, + { + desc: "mismatched type", + input: []byte(` +struct = 'xxx' + `), + hasDecodeErr: true, + }, } for i := range cases { @@ -112,129 +503,17 @@ unknown = 'xxx' tt.Parallel() decoder, encoder := Codec[TestType]() - require.NoError(t, decoder.Decode(c.input, &c.obj)) + err := decoder.Decode(c.input, &c.obj) + if !c.hasDecodeErr { + require.NoError(tt, err, c.desc) + } else { + assert.Error(tt, err, c.desc) + return + } c.change(&c.obj) res, err := encoder.Encode(&c.obj) - require.NoError(tt, err) + require.NoError(tt, err, c.desc) assert.Equal(tt, string(bytes.TrimSpace(c.output)), string(bytes.TrimSpace(res))) }) } } - -func TestGenerateHash(t *testing.T) { - tests := []struct { - name string - tomlStr v1alpha1.ConfigFile - semanticallyEquivalentStr v1alpha1.ConfigFile - wantHash string - wantError bool - }{ - { - name: "Valid TOML string", - tomlStr: v1alpha1.ConfigFile(`foo = 'bar' -[log] -k1 = 'v1' -k2 = 'v2'`), - semanticallyEquivalentStr: v1alpha1.ConfigFile(`foo = 'bar' -[log] -k2 = 'v2' -k1 = 'v1'`), - wantHash: "5dbbcf4574", - wantError: false, - }, - { - name: "Different config value", - tomlStr: v1alpha1.ConfigFile(`foo = 'foo' -[log] -k2 = 'v2' -k1 = 'v1'`), - wantHash: "f5bc46cb9", - wantError: false, - }, - { - name: "multiple sections with blank line", - tomlStr: v1alpha1.ConfigFile(`[a] -k1 = 'v1' -[b] -k2 = 'v2'`), - semanticallyEquivalentStr: v1alpha1.ConfigFile(`[a] -k1 = 'v1' -[b] - -k2 = 'v2'`), - wantHash: "79598d5977", - wantError: false, - }, - { - name: "Empty TOML string", - tomlStr: v1alpha1.ConfigFile(``), - wantHash: "7d6fc488b7", - wantError: false, - }, - { - name: "Invalid TOML string", - tomlStr: v1alpha1.ConfigFile(`key1 = "value1" - key2 = value2`), // Missing quotes around value2 - wantHash: "", - wantError: true, - }, - { - name: "Nested tables", - tomlStr: v1alpha1.ConfigFile(`[parent] -child1 = "value1" -child2 = "value2" -[parent.child] -grandchild1 = "value3" -grandchild2 = "value4"`), - semanticallyEquivalentStr: v1alpha1.ConfigFile(`[parent] -child2 = "value2" -child1 = "value1" -[parent.child] -grandchild2 = "value4" -grandchild1 = "value3"`), - wantHash: "7bf645ccb4", - wantError: false, - }, - { - name: "Array of tables", - tomlStr: v1alpha1.ConfigFile(`[[products]] -name = "Hammer" -sku = 738594937 - -[[products]] -name = "Nail" -sku = 284758393 - -color = "gray"`), - semanticallyEquivalentStr: v1alpha1.ConfigFile(`[[products]] -sku = 738594937 -name = "Hammer" - -[[products]] -sku = 284758393 -name = "Nail" - -color = "gray"`), - wantHash: "7549cf87f4", - wantError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotHash, err := GenerateHash(tt.tomlStr) - if tt.wantError { - assert.Error(t, err) - } else { - require.NoError(t, err) - assert.Equal(t, tt.wantHash, gotHash) - - if string(tt.semanticallyEquivalentStr) != "" { - reorderedHash, err := GenerateHash(tt.semanticallyEquivalentStr) - require.NoError(t, err) - assert.Equal(t, tt.wantHash, reorderedHash) - } - } - }) - } -}