Skip to content

Commit 770dbdc

Browse files
wenovusliulk
andauthored
Improve error messages in ygot.Merge. (#815)
* Improve error messages in ygot.Merge. Fail slow: gather error messages about conflicting fields prior to returning to user. Also print the access path to the user so they know which fields contain the conflict. * Import 1.20 errors.Join code to maintain compatibility with 1.18 * Update ygot/join.go Co-authored-by: Likai Liu <[email protected]> * improve style --------- Co-authored-by: Likai Liu <[email protected]>
1 parent 737e8fe commit 770dbdc

File tree

4 files changed

+249
-43
lines changed

4 files changed

+249
-43
lines changed

ygot/join.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2023 Google Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Adapted from errors.Join() introduced in go1.20 for compatibility with older Go versions.
16+
17+
// Copyright 2022 The Go Authors. All rights reserved.
18+
// Use of this source code is governed by a BSD-style
19+
// license that can be found in the LICENSE file.
20+
21+
package ygot
22+
23+
// joinErrors returns an error that wraps the given errors.
24+
// Any nil error values are discarded.
25+
// joinErrors returns nil if errs contains no non-nil values.
26+
// The error formats as the concatenation of the strings obtained
27+
// by calling the Error method of each element of errs, with a newline
28+
// between each string.
29+
func joinErrors(errs ...error) error {
30+
n := 0
31+
for _, err := range errs {
32+
if err != nil {
33+
n++
34+
}
35+
}
36+
if n == 0 {
37+
return nil
38+
}
39+
e := &joinError{
40+
errs: make([]error, 0, n),
41+
}
42+
for _, err := range errs {
43+
if err != nil {
44+
e.errs = append(e.errs, err)
45+
}
46+
}
47+
return e
48+
}
49+
50+
type joinError struct {
51+
errs []error
52+
}
53+
54+
func (e *joinError) Error() string {
55+
var b []byte
56+
for i, err := range e.errs {
57+
if i > 0 {
58+
b = append(b, '\n')
59+
}
60+
b = append(b, err.Error()...)
61+
}
62+
return string(b)
63+
}
64+
65+
func (e *joinError) Unwrap() []error {
66+
return e.errs
67+
}

ygot/join_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2023 Google Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Adapted from https://cs.opensource.google/go/go/+/refs/tags/go1.20.3:src/errors/join.go
16+
17+
// Copyright 2022 The Go Authors. All rights reserved.
18+
// Use of this source code is governed by a BSD-style
19+
// license that can be found in the LICENSE file.
20+
21+
package ygot
22+
23+
import (
24+
"errors"
25+
"reflect"
26+
"testing"
27+
)
28+
29+
func TestJoinReturnsNil(t *testing.T) {
30+
if err := joinErrors(); err != nil {
31+
t.Errorf("joinErrors() = %v, want nil", err)
32+
}
33+
if err := joinErrors(nil); err != nil {
34+
t.Errorf("joinErrors(nil) = %v, want nil", err)
35+
}
36+
if err := joinErrors(nil, nil); err != nil {
37+
t.Errorf("joinErrors(nil, nil) = %v, want nil", err)
38+
}
39+
}
40+
41+
func TestJoin(t *testing.T) {
42+
err1 := errors.New("err1")
43+
err2 := errors.New("err2")
44+
for _, test := range []struct {
45+
errs []error
46+
want []error
47+
}{{
48+
errs: []error{err1},
49+
want: []error{err1},
50+
}, {
51+
errs: []error{err1, err2},
52+
want: []error{err1, err2},
53+
}, {
54+
errs: []error{err1, nil, err2},
55+
want: []error{err1, err2},
56+
}} {
57+
got := joinErrors(test.errs...).(interface{ Unwrap() []error }).Unwrap()
58+
if !reflect.DeepEqual(got, test.want) {
59+
t.Errorf("Join(%v) = %v; want %v", test.errs, got, test.want)
60+
}
61+
if len(got) != cap(got) {
62+
t.Errorf("Join(%v) returns errors with len=%v, cap=%v; want len==cap", test.errs, len(got), cap(got))
63+
}
64+
}
65+
}
66+
67+
func TestJoinErrorMethod(t *testing.T) {
68+
err1 := errors.New("err1")
69+
err2 := errors.New("err2")
70+
for _, test := range []struct {
71+
errs []error
72+
want string
73+
}{{
74+
errs: []error{err1},
75+
want: "err1",
76+
}, {
77+
errs: []error{err1, err2},
78+
want: "err1\nerr2",
79+
}, {
80+
errs: []error{err1, nil, err2},
81+
want: "err1\nerr2",
82+
}} {
83+
got := joinErrors(test.errs...).Error()
84+
if got != test.want {
85+
t.Errorf("Join(%v).Error() = %q; want %q", test.errs, got, test.want)
86+
}
87+
}
88+
}

ygot/struct_validation_map.go

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ func MergeStructInto(dst, src GoStruct, opts ...MergeOpt) error {
604604
return fmt.Errorf("cannot merge structs that are not of matching types, %T != %T", dst, src)
605605
}
606606

607-
return copyStruct(reflect.ValueOf(dst).Elem(), reflect.ValueOf(src).Elem(), opts...)
607+
return copyStruct(reflect.ValueOf(dst).Elem(), reflect.ValueOf(src).Elem(), "", opts...)
608608
}
609609

610610
// DeepCopy returns a deep copy of the supplied GoStruct. A new copy
@@ -626,7 +626,7 @@ func deepCopy(s GoStruct, keepEmptyMaps bool) (GoStruct, error) {
626626
if keepEmptyMaps {
627627
opts = append(opts, &MergeEmptyMaps{})
628628
}
629-
if err := copyStruct(n.Elem(), reflect.ValueOf(s).Elem(), opts...); err != nil {
629+
if err := copyStruct(n.Elem(), reflect.ValueOf(s).Elem(), "", opts...); err != nil {
630630
return nil, fmt.Errorf("cannot DeepCopy struct: %v", err)
631631
}
632632
return n.Interface().(GoStruct), nil
@@ -657,7 +657,13 @@ func mergeEmptyMapsEnabled(opts []MergeOpt) bool {
657657
}
658658

659659
// copyStruct copies the fields of srcVal into the dstVal struct in-place.
660-
func copyStruct(dstVal, srcVal reflect.Value, opts ...MergeOpt) error {
660+
//
661+
// - accessPath is the programmatic access path to the struct. It is used for
662+
// generating a more usable error message. (e.g. Field1.Map2["foo"].Field3)
663+
// When calling at the top level, "" should be used.
664+
//
665+
// It fails-slow: accumulates errors prior to return.
666+
func copyStruct(dstVal, srcVal reflect.Value, accessPath string, opts ...MergeOpt) error {
661667
if srcVal.Type() != dstVal.Type() {
662668
return fmt.Errorf("cannot copy %s to %s", srcVal.Type().Name(), dstVal.Type().Name())
663669
}
@@ -666,27 +672,21 @@ func copyStruct(dstVal, srcVal reflect.Value, opts ...MergeOpt) error {
666672
return fmt.Errorf("cannot handle non-struct types, src: %v, dst: %v", srcVal.Type().Kind(), dstVal.Type().Kind())
667673
}
668674

675+
var errs []error
669676
for i := 0; i < srcVal.NumField(); i++ {
670677
srcField := srcVal.Field(i)
671678
dstField := dstVal.Field(i)
679+
accessPath := accessPath + "." + srcVal.Type().Field(i).Name
672680

673681
switch srcField.Kind() {
674682
case reflect.Ptr:
675-
if err := copyPtrField(dstField, srcField, opts...); err != nil {
676-
return err
677-
}
683+
errs = append(errs, copyPtrField(dstField, srcField, accessPath, opts...))
678684
case reflect.Interface:
679-
if err := copyInterfaceField(dstField, srcField, opts...); err != nil {
680-
return err
681-
}
685+
errs = append(errs, copyInterfaceField(dstField, srcField, accessPath, opts...))
682686
case reflect.Map:
683-
if err := copyMapField(dstField, srcField, opts...); err != nil {
684-
return err
685-
}
687+
errs = append(errs, copyMapField(dstField, srcField, accessPath, opts...))
686688
case reflect.Slice:
687-
if err := copySliceField(dstField, srcField, opts...); err != nil {
688-
return err
689-
}
689+
errs = append(errs, copySliceField(dstField, srcField, accessPath, opts...))
690690
case reflect.Int64:
691691
// In the case of an int64 field, which represents a YANG enumeration
692692
// we should only set the value in the destination if it is not set
@@ -695,7 +695,8 @@ func copyStruct(dstVal, srcVal reflect.Value, opts ...MergeOpt) error {
695695
switch {
696696
case vSrc != 0 && vDst != 0 && vSrc != vDst:
697697
if !fieldOverwriteEnabled(opts) {
698-
return fmt.Errorf("destination and source values were set when merging enum field, dst: %d, src: %d", vSrc, vDst)
698+
errs = append(errs, fmt.Errorf("%s: destination and source values were set when merging enum field, dst: %d, src: %d", accessPath, vSrc, vDst))
699+
break
699700
}
700701
dstField.Set(srcField)
701702
case vSrc != 0 && vDst == 0:
@@ -705,7 +706,7 @@ func copyStruct(dstVal, srcVal reflect.Value, opts ...MergeOpt) error {
705706
dstField.Set(srcField)
706707
}
707708
}
708-
return nil
709+
return joinErrors(errs...)
709710
}
710711

711712
// copyPtrField copies srcField to dstField. srcField and dstField must be
@@ -715,7 +716,7 @@ func copyStruct(dstVal, srcVal reflect.Value, opts ...MergeOpt) error {
715716
// is returned. If the source and destination both have a pointer field, which is
716717
// populated then an error is returned unless the value of the field is
717718
// equal in both structs.
718-
func copyPtrField(dstField, srcField reflect.Value, opts ...MergeOpt) error {
719+
func copyPtrField(dstField, srcField reflect.Value, accessPath string, opts ...MergeOpt) error {
719720

720721
if util.IsNilOrInvalidValue(srcField) {
721722
return nil
@@ -738,7 +739,7 @@ func copyPtrField(dstField, srcField reflect.Value, opts ...MergeOpt) error {
738739
d = dstField
739740
}
740741

741-
if err := copyStruct(d.Elem(), srcField.Elem(), opts...); err != nil {
742+
if err := copyStruct(d.Elem(), srcField.Elem(), accessPath, opts...); err != nil {
742743
return err
743744
}
744745
dstField.Set(d)
@@ -748,7 +749,7 @@ func copyPtrField(dstField, srcField reflect.Value, opts ...MergeOpt) error {
748749
if !util.IsNilOrInvalidValue(dstField) {
749750
s, d := srcField.Elem().Interface(), dstField.Elem().Interface()
750751
if !fieldOverwriteEnabled(opts) && !reflect.DeepEqual(s, d) {
751-
return fmt.Errorf("destination value was set, but was not equal to source value when merging ptr field, src: %v, dst: %v", s, d)
752+
return fmt.Errorf("%s: destination value was set, but was not equal to source value when merging ptr field, src: %v, dst: %v", accessPath, s, d)
752753
}
753754
}
754755

@@ -760,7 +761,7 @@ func copyPtrField(dstField, srcField reflect.Value, opts ...MergeOpt) error {
760761

761762
// copyInterfaceField copies srcField into dstField. Both srcField and dstField
762763
// are reflect.Value structs which contain an interface value.
763-
func copyInterfaceField(dstField, srcField reflect.Value, opts ...MergeOpt) error {
764+
func copyInterfaceField(dstField, srcField reflect.Value, accessPath string, opts ...MergeOpt) error {
764765
if util.IsNilOrInvalidValue(srcField) {
765766
return nil
766767
}
@@ -776,12 +777,12 @@ func copyInterfaceField(dstField, srcField reflect.Value, opts ...MergeOpt) erro
776777
if !util.IsNilOrInvalidValue(dstField) {
777778
dV := dstField.Elem().Elem() // Dereference dst to a struct.
778779
if !fieldOverwriteEnabled(opts) && !reflect.DeepEqual(s.Interface(), dV.Interface()) {
779-
return fmt.Errorf("interface field was set in both src and dst and was not equal, src: %v, dst: %v", s.Interface(), dV.Interface())
780+
return fmt.Errorf("%s: interface field was set in both src and dst and was not equal, src: %v, dst: %v", accessPath, s.Interface(), dV.Interface())
780781
}
781782
}
782783

783784
d := reflect.New(s.Type())
784-
if err := copyStruct(d.Elem(), s, opts...); err != nil {
785+
if err := copyStruct(d.Elem(), s, accessPath, opts...); err != nil {
785786
return err
786787
}
787788
dstField.Set(d)
@@ -790,7 +791,7 @@ func copyInterfaceField(dstField, srcField reflect.Value, opts ...MergeOpt) erro
790791
if !util.IsNilOrInvalidValue(dstField) {
791792
s, d := srcField.Interface(), dstField.Interface()
792793
if !fieldOverwriteEnabled(opts) && !reflect.DeepEqual(s, d) {
793-
return fmt.Errorf("interface field was set in both src and dst and was not equal, src: %v, dst: %v", s, d)
794+
return fmt.Errorf("%s: interface field was set in both src and dst and was not equal, src: %v, dst: %v", accessPath, s, d)
794795
}
795796
}
796797

@@ -805,7 +806,7 @@ func copyInterfaceField(dstField, srcField reflect.Value, opts ...MergeOpt) erro
805806
if !util.IsNilOrInvalidValue(dstField) {
806807
s, d := srcField.Interface(), dstField.Interface()
807808
if !fieldOverwriteEnabled(opts) && !reflect.DeepEqual(s, d) {
808-
return fmt.Errorf("interface field was set in both src and dst and was not equal, src: %v, dst: %v", s, d)
809+
return fmt.Errorf("%s: interface field was set in both src and dst and was not equal, src: %v, dst: %v", accessPath, s, d)
809810
}
810811
}
811812
dstField.Set(srcField)
@@ -819,7 +820,7 @@ func copyInterfaceField(dstField, srcField reflect.Value, opts ...MergeOpt) erro
819820
// are populated, and have non-overlapping keys, they are merged. If the same
820821
// key is populated in srcField and dstField, their contents are merged if they
821822
// do not overlap, otherwise an error is returned.
822-
func copyMapField(dstField, srcField reflect.Value, opts ...MergeOpt) error {
823+
func copyMapField(dstField, srcField reflect.Value, accessPath string, opts ...MergeOpt) error {
823824
if !util.IsValueMap(srcField) {
824825
return fmt.Errorf("received a non-map type in src map field: %v", srcField.Kind())
825826
}
@@ -850,18 +851,20 @@ func copyMapField(dstField, srcField reflect.Value, opts ...MergeOpt) error {
850851
dstKeys[k.Interface()] = true
851852
}
852853

854+
var errs []error
853855
for _, k := range srcField.MapKeys() {
854856
v := srcField.MapIndex(k)
855857
d := reflect.New(v.Elem().Type())
856858
if _, ok := dstKeys[k.Interface()]; ok {
857859
d = dstField.MapIndex(k)
858860
}
859-
if err := copyStruct(d.Elem(), v.Elem(), opts...); err != nil {
860-
return err
861+
if err := copyStruct(d.Elem(), v.Elem(), fmt.Sprintf("%s[%#v]", accessPath, k.Interface()), opts...); err != nil {
862+
errs = append(errs, err)
863+
continue
861864
}
862865
dstField.SetMapIndex(k, d)
863866
}
864-
return nil
867+
return joinErrors(errs...)
865868
}
866869

867870
// mapTypes provides a specification of a map.
@@ -903,7 +906,7 @@ func validateMap(srcField, dstField reflect.Value) (*mapType, error) {
903906
// copySliceField copies srcField into dstField. Both srcField and dstField
904907
// must have a kind of reflect.Slice kind and contain pointers to structs. If
905908
// the slice in dstField is populated an error is returned.
906-
func copySliceField(dstField, srcField reflect.Value, opts ...MergeOpt) error {
909+
func copySliceField(dstField, srcField reflect.Value, accessPath string, opts ...MergeOpt) error {
907910
if dstField.Len() == 0 && srcField.Len() == 0 {
908911
return nil
909912
}
@@ -920,7 +923,7 @@ func copySliceField(dstField, srcField reflect.Value, opts ...MergeOpt) error {
920923

921924
if !unique {
922925
// YANG lists and leaf-lists must be unique.
923-
return fmt.Errorf("source and destination lists must be unique, got src: %v, dst: %v", srcField, dstField)
926+
return fmt.Errorf("%s: source and destination lists must be unique, got src: %v, dst: %v", accessPath, srcField, dstField)
924927
}
925928
}
926929

@@ -932,15 +935,17 @@ func copySliceField(dstField, srcField reflect.Value, opts ...MergeOpt) error {
932935
return nil
933936
}
934937

938+
var errs []error
935939
for i := 0; i < srcField.Len(); i++ {
936940
v := srcField.Index(i)
937941
d := reflect.New(v.Type().Elem())
938-
if err := copyStruct(d.Elem(), v.Elem(), opts...); err != nil {
939-
return err
942+
if err := copyStruct(d.Elem(), v.Elem(), fmt.Sprintf("%s[%v]", accessPath, i), opts...); err != nil {
943+
errs = append(errs, err)
944+
continue
940945
}
941946
dstField.Set(reflect.Append(dstField, v))
942947
}
943-
return nil
948+
return joinErrors(errs...)
944949
}
945950

946951
// uniqueSlices takes two reflect.Values which must represent slices, and determines

0 commit comments

Comments
 (0)