Repository: openconfig/ygot
Pull request #862 (2023-06-07 02:21:59)

Add support for ending iteration of subtrees by new `ForEachDataField2`
Added `ForEachDataField2` and `FieldIteratorFunc2` to support returning new enum type indicating iteration behaviour.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/60571610/badge)](https://coveralls.io/builds/60571610)

coverage: 89.709% (-0.04%) from 89.749% when pulling **2fad30430187b4d6ca2c13c4cd4719d02bb705a7 on orderedmap-diff** into **85d7eaf6f4d1af08ae3fa5b70ed35707c02f87ca on master**.


Patch:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 49cb4d5..fb778c7 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -113,7 +113,9 @@ jobs:
           done
 
       - name: Make sure exampleoc doesn't error or panic
-        run: go generate ./...
+        run: |
+          go install golang.org/x/tools/cmd/stringer@latest
+          go generate ./...
         working-directory: go/src/github.com/openconfig/ygot
 
   static_analysis:
diff --git a/util/iterationaction_string.go b/util/iterationaction_string.go
new file mode 100644
index 0000000..aee0931
--- /dev/null
+++ b/util/iterationaction_string.go
@@ -0,0 +1,24 @@
+// Code generated by "stringer -type=IterationAction"; DO NOT EDIT.
+
+package util
+
+import "strconv"
+
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[ContinueIteration-0]
+	_ = x[DoNotIterateDescendants-1]
+}
+
+const _IterationAction_name = "ContinueIterationDoNotIterateDescendants"
+
+var _IterationAction_index = [...]uint8{0, 17, 40}
+
+func (i IterationAction) String() string {
+	if i >= IterationAction(len(_IterationAction_index)-1) {
+		return "IterationAction(" + strconv.FormatInt(int64(i), 10) + ")"
+	}
+	return _IterationAction_name[_IterationAction_index[i]:_IterationAction_index[i+1]]
+}
diff --git a/util/reflect.go b/util/reflect.go
index a13925f..216f8d8 100644
--- a/util/reflect.go
+++ b/util/reflect.go
@@ -615,6 +615,25 @@ func (node *PathQueryNodeMemo) GetRoot() *PathQueryNodeMemo {
 // It returns a slice of errors encountered while processing the field.
 type FieldIteratorFunc func(ni *NodeInfo, in, out interface{}) Errors
 
+// IterationAction is an enumeration representing different iteration actions.
+//
+//go:generate stringer -type=IterationAction
+type IterationAction uint
+
+const (
+	// ContinueIteration means to continue the preorder traversal.
+	ContinueIteration = IterationAction(iota)
+	// DoNotIterateDescendants means to continue traversal but skip the
+	// descendant elements of this subtree node.
+	DoNotIterateDescendants
+)
+
+// FieldIteratorFunc2 is an iteration function for arbitrary field traversals.
+// in, out are passed through from the caller to the iteration vistior function
+// and can be used to pass state in and out. They are not otherwise touched.
+// It returns what next iteration action to take as well as an error.
+type FieldIteratorFunc2 func(ni *NodeInfo, in, out any) (IterationAction, Errors)
+
 // ForEachField recursively iterates through the fields of value (which may be
 // any Go type) and executes iterFunction on each field. Any nil fields
 // (including value) are traversed in the schema tree only. This is done to
@@ -793,20 +812,46 @@ func forEachFieldInternal(ni *NodeInfo, in, out interface{}, iterFunction FieldI
 	return errs
 }
 
+// iterFuncToIterFunc2 converts a FieldIteratorFunc to FieldIteratorFunc2.
+func iterFuncToIterFunc2(iterFunction FieldIteratorFunc) FieldIteratorFunc2 {
+	return func(ni *NodeInfo, in, out interface{}) (IterationAction, Errors) {
+		return ContinueIteration, iterFunction(ni, in, out)
+	}
+}
+
 // ForEachDataField iterates the value supplied and calls the iterFunction for
 // each data tree node found in the supplied value. No schema information is required
 // to perform the iteration. The in and out arguments are passed to the iterFunction
 // without inspection by this function, and can be used by the caller to store
 // input and output during the iteration through the data tree.
+//
+// Deprecated: Use ForEachDataField2 instead.
 func ForEachDataField(value, in, out interface{}, iterFunction FieldIteratorFunc) Errors {
 	if IsValueNil(value) {
 		return nil
 	}
 
+	return forEachDataFieldInternal(&NodeInfo{FieldValue: reflect.ValueOf(value)}, in, out, iterFuncToIterFunc2(iterFunction))
+}
+
+// ForEachDataField2 is an improved ForEachDataField that allows iteration over
+// the data tree in the supplied value with custom iteration behaviour at each
+// iteration step.
+//
+// ForEachDataField2 calls iterFunction for each data tree node found in the
+// supplied value. No schema information is required to perform the iteration.
+// The in and out arguments are passed to the iterFunction without inspection
+// by this function, and can be used by the caller to store input and output
+// during the iteration through the data tree.
+func ForEachDataField2(value, in, out any, iterFunction FieldIteratorFunc2) Errors {
+	if IsValueNil(value) {
+		return nil
+	}
+
 	return forEachDataFieldInternal(&NodeInfo{FieldValue: reflect.ValueOf(value)}, in, out, iterFunction)
 }
 
-func forEachDataFieldInternal(ni *NodeInfo, in, out interface{}, iterFunction FieldIteratorFunc) Errors {
+func forEachDataFieldInternal(ni *NodeInfo, in, out any, iterFunction FieldIteratorFunc2) Errors {
 	if IsValueNil(ni) {
 		return nil
 	}
@@ -819,7 +864,16 @@ func forEachDataFieldInternal(ni *NodeInfo, in, out interface{}, iterFunction Fi
 
 	var errs Errors
 	// Run the iterator function for this field.
-	errs = AppendErrs(errs, iterFunction(ni, in, out))
+	iterAction, err := iterFunction(ni, in, out)
+	errs = AppendErrs(errs, err)
+	switch iterAction {
+	case DoNotIterateDescendants:
+		return errs
+	case ContinueIteration:
+	default:
+		errs = AppendErr(errs, fmt.Errorf("unhandled IterationAction type: %v", iterAction))
+		return errs
+	}
 
 	v := ni.FieldValue
 	t := v.Type()
diff --git a/ygot/diff.go b/ygot/diff.go
index c16099a..ce578c7 100644
--- a/ygot/diff.go
+++ b/ygot/diff.go
@@ -21,6 +21,8 @@ import (
 	"strings"
 
 	"github.com/kylelemons/godebug/pretty"
+	"github.com/openconfig/gnmi/errlist"
+	"github.com/openconfig/ygot/internal/yreflect"
 	"github.com/openconfig/ygot/util"
 	"google.golang.org/protobuf/proto"
 
@@ -232,11 +234,15 @@ func toStringPathMap(pathMap map[*pathSpec]interface{}) (map[string]*pathInfo, e
 // walk of the struct - using the out argument to store the set of changed leaves.
 // A specific Annotation is used to store the absolute path of the entity during
 // the walk.
-func findSetLeaves(s GoStruct, opts ...DiffOpt) (map[*pathSpec]interface{}, error) {
+//
+// - orderedMapAsLeaf=true specifies that ordered maps (GoOrderedList
+// interface) will be treated as a leaf and will be returned as-is instead of
+// being walked and its leaves populated.
+func findSetLeaves(s GoStruct, orderedMapAsLeaf bool, opts ...DiffOpt) (map[*pathSpec]interface{}, error) {
 	pathOpt := hasDiffPathOpt(opts)
 	processedPaths := map[string]bool{}
 
-	findSetIterFunc := func(ni *util.NodeInfo, in, out interface{}) (errs util.Errors) {
+	findSetIterFunc := func(ni *util.NodeInfo, in, out interface{}) (action util.IterationAction, errs util.Errors) {
 		if reflect.DeepEqual(ni.StructField, reflect.StructField{}) {
 			return
 		}
@@ -272,15 +278,17 @@ func findSetLeaves(s GoStruct, opts ...DiffOpt) (map[*pathSpec]interface{}, erro
 
 		vp, err := nodeValuePath(ni, sp)
 		if err != nil {
-			return util.NewErrs(err)
+			errs = util.NewErrs(err)
+			return
 		}
 
-		//prevent duplicate key
+		// Avoid processing twice if there is duplicate path.
 		keys := make([]string, len(vp.gNMIPaths))
 		for i, paths := range vp.gNMIPaths {
 			s, err := PathToString(paths)
 			if err != nil {
-				return util.NewErrs(err)
+				errs = util.NewErrs(err)
+				return
 			}
 			keys[i] = s
 		}
@@ -293,12 +301,23 @@ func findSetLeaves(s GoStruct, opts ...DiffOpt) (map[*pathSpec]interface{}, erro
 
 		ni.Annotation = []interface{}{vp}
 
+		ival := ni.FieldValue.Interface()
+
+		orderedMap, isOrderedMap := ival.(GoOrderedList)
+
 		// Ignore non-data, or default data values.
-		if util.IsNilOrInvalidValue(ni.FieldValue) || util.IsValueNilOrDefault(ni.FieldValue.Interface()) || util.IsValueStructPtr(ni.FieldValue) || util.IsValueMap(ni.FieldValue) {
+		if util.IsNilOrInvalidValue(ni.FieldValue) || util.IsValueNilOrDefault(ni.FieldValue.Interface()) || util.IsValueMap(ni.FieldValue) {
+			return
+		}
+		// Ignore structs unless it is an ordered map and we're
+		// treating it as a leaf (since it is assumed to be
+		// telemetry-atomic in order to preserve ordering of entries).
+		if (!isOrderedMap || !orderedMapAsLeaf) && util.IsValueStructPtr(ni.FieldValue) {
+			return
+		}
+		if isOrderedMap && orderedMap.Len() == 0 {
 			return
 		}
-
-		ival := ni.FieldValue.Interface()
 
 		// If this is an enumerated value in the output structs, then check whether
 		// it is set. Only include values that are set to a non-zero value.
@@ -317,11 +336,17 @@ func findSetLeaves(s GoStruct, opts ...DiffOpt) (map[*pathSpec]interface{}, erro
 		outs := out.(map[*pathSpec]interface{})
 		outs[vp] = ival
 
+		if isOrderedMap && orderedMapAsLeaf {
+			// We treat the ordered map as a leaf, so don't
+			// traverse any descendant elements.
+			action = util.DoNotIterateDescendants
+		}
+
 		return
 	}
 
 	out := map[*pathSpec]interface{}{}
-	if errs := util.ForEachDataField(s, nil, out, findSetIterFunc); errs != nil {
+	if errs := util.ForEachDataField2(s, nil, out, findSetIterFunc); errs != nil {
 		return nil, fmt.Errorf("error from ForEachDataField iteration: %v", errs)
 	}
 
@@ -434,6 +459,11 @@ func (*DiffPathOpt) IsDiffOpt() {}
 //     field value.
 //   - The paths within the Delete field of the notification indicate that the
 //     field was not present in the modified struct, but was set in the original.
+//   - NOTE: For `ordered-by user` nodes, which are represented by ordered maps
+//     in the ygot-generated code, the output Notification cannot be directly
+//     unmarshalling into original to arrive at modified since updates are
+//     granular. For generating atomic:true Notifications, use
+//     ygot.DiffWithAtomic instead.
 //
 // Annotation fields that are contained within the supplied original or modified
 // GoStruct are skipped.
@@ -447,17 +477,156 @@ func (*DiffPathOpt) IsDiffOpt() {}
 // to the fields specified if a GoStruct that does not represent the root of
 // a YANG schema tree is not supplied as original and modified.
 func Diff(original, modified GoStruct, opts ...DiffOpt) (*gnmipb.Notification, error) {
+	notifs, err := diff(original, modified, false, opts...)
+	switch len(notifs) {
+	case 0:
+		return &gnmipb.Notification{}, err
+	case 1:
+		return notifs[0], err
+	default:
+		return nil, fmt.Errorf("internal error: Diff expected a single Notification but got multiple")
+	}
+}
+
+// DiffWithAtomic takes an original and modified GoStruct, which must be of the same
+// type and returns a slice of gNMI Notifications that represents the diff
+// between them in a way that can be used to update a local gNMI.Subscribe
+// server with the Notifications to be served. When
+// ytypes.UnmarshalNotifications is called, it can also provide the same
+// GoStruct back.
+//
+// The last message in the slice (if it exists) contains a non-atomic
+// Notification representing updates/deletes to non-atomic nodes and deletes to
+// atomic nodes. All messages prior to the last one are atomic Notifications
+// representing updates to atomic nodes.
+//
+// NOTE: Currently only YANG `ordered-by user lists` are supported as atomic
+// nodes. Further, they're always treated as so since there is no way of
+// representing order using TypedValue scalar types.
+//
+// The original struct is considered as the "from" data, with the
+// modified struct the "to" such that:
+//
+//   - The contents of the Update field of the notification indicate that the
+//     field in modified was either not present in original, or had a different
+//     field value.
+//   - The paths within the Delete field of the notification indicate that the
+//     field was not present in the modified struct, but was set in the original.
+//
+// Annotation fields that are contained within the supplied original or modified
+// GoStruct are skipped.
+//
+// A set of options for diff's behaviour, as specified by the supplied DiffOpts
+// can be used to modify the behaviour of the Diff function per the individual
+// option's specification.
+//
+// The returned gNMI Notification cannot be put on the wire unmodified, since
+// it does not specify a timestamp - and may not contain the absolute paths
+// to the fields specified if a GoStruct that does not represent the root of
+// a YANG schema tree is not supplied as original and modified.
+func DiffWithAtomic(original, modified GoStruct, opts ...DiffOpt) ([]*gnmipb.Notification, error) {
+	return diff(original, modified, true, opts...)
+}
+
+// orderedMapLeaves returns an ordered list of path-value pairs representing
+// the leaves belonging to the input ordered map.
+//
+//   - parent is the gNMI path representing the absolute path to the ordered
+//     list. This must not be empty.
+//   - The second return value is the prefix path that must be used in the
+//     atomic Notification representing the ordered list.
+func orderedMapLeaves(orderedMap GoOrderedList, parent *gnmiPath) ([]*pathval, *gnmiPath, error) {
+	var errs errlist.List
+	var atomicLeaves []*pathval
+
+	if err := yreflect.RangeOrderedMap(orderedMap, func(k reflect.Value, v reflect.Value) bool {
+		childPath, err := mapValuePath(k, v, parent)
+		if err != nil {
+			errs.Add(err)
+			return true
+		}
+
+		goStruct, ok := v.Interface().(GoStruct)
+		if !ok {
+			errs.Add(fmt.Errorf("%v: was not a valid GoStruct", parent))
+			return true
+		}
+		errs.Add(findUpdatedLeaves(&atomicLeaves, goStruct, childPath))
+		return true
+	}); err != nil {
+		errs.Add(err)
+		return nil, nil, errs.Err()
+	}
+
+	// TODO(wenbli): Make this more robust by potentially introducing another struct
+	// tag to indicate which element in the compressed path should the prefix cutoff
+	// be based on the placement of the atomic extension, although for ordered-maps
+	// it should always be at the container level. Need more discussion on this.
+	// The current use case is for BGP policy statements:
+	// https://github.com/openconfig/public/pull/867
+	// The reason for this subtreePath hack is that the atomic annotation
+	// is at the container surrounding the ordered lists.
+	subtreePath := parent.Copy()
+	if err := subtreePath.Pop(); err != nil {
+		errs.Add(err)
+	}
+
+	return atomicLeaves, subtreePath, errs.Err()
+}
+
+// orderedMapNotif returns an atomic Notification for the given ordered map.
+//
+//   - If empty, then nil is returned.
+//   - parent is the gNMI path representing the absolute path to the ordered
+//     list. This must not be empty.
+func orderedMapNotif(orderedMap GoOrderedList, parent *gnmiPath, ts int64) (*gnmipb.Notification, error) {
+	atomicLeaves, subtreePath, err := orderedMapLeaves(orderedMap, parent)
+	if err != nil {
+		return nil, err
+	}
+	if len(atomicLeaves) == 0 {
+		return nil, nil
+	}
+
+	return createAtomicNotif(atomicLeaves, ts, subtreePath)
+}
+
+func createAtomicNotif(atomicLeaves []*pathval, ts int64, subtreePfx *gnmiPath) (*gnmipb.Notification, error) {
+	no := &gnmipb.Notification{
+		Timestamp: ts,
+		Atomic:    true,
+	}
+	p, err := subtreePfx.ToProto()
+	if err != nil {
+		return nil, err
+	}
+	no.Prefix = p
 
+	for _, pv := range atomicLeaves {
+		if err := addToNotification(pv.path, pv.val, no, subtreePfx); err != nil {
+			return nil, err
+		}
+	}
+	return no, nil
+}
+
+// diff produces a slice of notifications given two GoStructs.
+//
+// See documentation for Diff and DiffWithAtomic for more information.
+//
+//   - withAtomic indicates that atomic notifications should be generated
+//     (currently this is only supported for `ordered-by user` lists)
+func diff(original, modified GoStruct, withAtomic bool, opts ...DiffOpt) ([]*gnmipb.Notification, error) {
 	if reflect.TypeOf(original) != reflect.TypeOf(modified) {
 		return nil, fmt.Errorf("cannot diff structs of different types, original: %T, modified: %T", original, modified)
 	}
 
-	origLeaves, err := findSetLeaves(original, opts...)
+	origLeaves, err := findSetLeaves(original, withAtomic, opts...)
 	if err != nil {
 		return nil, fmt.Errorf("could not extract set leaves from original struct: %v", err)
 	}
 
-	modLeaves, err := findSetLeaves(modified, opts...)
+	modLeaves, err := findSetLeaves(modified, withAtomic, opts...)
 	if err != nil {
 		return nil, fmt.Errorf("could not extract set leaves from modified struct: %v", err)
 	}
@@ -471,34 +640,61 @@ func Diff(original, modified GoStruct, opts ...DiffOpt) (*gnmipb.Notification, e
 		return nil, fmt.Errorf("could not convert leaf path map to string path map: %v", err)
 	}
 
+	var atomicNotifs []*gnmipb.Notification
 	n := &gnmipb.Notification{}
+	processUpdate := func(path string, modVal *pathInfo) error {
+		if orderedMap, isOrderedMap := modVal.val.(GoOrderedList); isOrderedMap {
+			notif, err := orderedMapNotif(orderedMap, newPathElemGNMIPath(modVal.path.GetElem()), 0)
+			if err != nil {
+				return err
+			}
+			atomicNotifs = append(atomicNotifs, notif)
+		} else {
+			// The contents of the value should indicate that value a has changed
+			// to value b.
+			if err := appendUpdate(n, path, modVal); err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+
 	for origPath, origVal := range origLeavesStr {
 		if modVal, ok := modLeavesStr[origPath]; ok {
 			if !reflect.DeepEqual(origVal.val, modVal.val) {
-				// The contents of the value should indicate that value a has changed
-				// to value b.
-				if err := appendUpdate(n, origPath, modVal); err != nil {
+				if err := processUpdate(origPath, modVal); err != nil {
 					return nil, err
 				}
 			}
 		} else if !ok {
+			if orderedMap, isOrderedMap := origVal.val.(GoOrderedList); isOrderedMap {
+				pathLen := len(origVal.path.Elem)
+				if pathLen == 0 {
+					return nil, fmt.Errorf("deletion path on ordered list is empty, this is unexpected: %T", orderedMap)
+				}
+				origVal.path.Elem = origVal.path.Elem[:pathLen-1]
+			}
 			// This leaf was set in the original struct, but not in the modified
 			// struct, therefore it has been deleted.
 			n.Delete = append(n.Delete, origVal.path)
 		}
 	}
-	if hasIgnoreAdditions(opts) != nil {
-		return n, nil
-	}
-	// Check that all paths that are in the modified struct have been examined, if
-	// not they are updates.
-	for modPath, modVal := range modLeavesStr {
-		if _, ok := origLeavesStr[modPath]; !ok {
-			if err := appendUpdate(n, modPath, modVal); err != nil {
-				return nil, err
+
+	if hasIgnoreAdditions(opts) == nil {
+		// Check that all paths that are in the modified struct have been examined, if
+		// not they are updates.
+		for modPath, modVal := range modLeavesStr {
+			if _, ok := origLeavesStr[modPath]; !ok {
+				if err := processUpdate(modPath, modVal); err != nil {
+					return nil, err
+				}
 			}
 		}
 	}
 
-	return n, nil
+	if len(n.Delete)+len(n.Update) == 0 {
+		return atomicNotifs, nil
+	}
+
+	return append([]*gnmipb.Notification{n}, atomicNotifs...), nil
 }
diff --git a/ygot/render.go b/ygot/render.go
index 88ef697..43abf4c 100644
--- a/ygot/render.go
+++ b/ygot/render.go
@@ -194,6 +194,25 @@ func (g *gnmiPath) AppendName(n string) error {
 	return nil
 }
 
+// Pop removes the last element from the gnmiPath.
+// If the supplied path is empty, then an error is returned.
+func (g *gnmiPath) Pop() error {
+	if !g.isValid() {
+		return fmt.Errorf("cannot pop from invalid path")
+	}
+
+	if g.Len() == 0 {
+		return fmt.Errorf("cannot pop from empty path")
+	}
+
+	if g.isStringSlicePath() {
+		g.stringSlicePath = g.stringSlicePath[:len(g.stringSlicePath)-1]
+	} else {
+		g.pathElemPath = g.pathElemPath[:g.Len()-1]
+	}
+	return nil
+}
+
 // LastPathElem returns the last PathElem element in the gnmiPath.
 func (g *gnmiPath) LastPathElem() (*gnmipb.PathElem, error) {
 	return g.PathElemAt(g.Len() - 1)
@@ -342,9 +361,8 @@ type GNMINotificationsConfig struct {
 // provided determines the path format utilised, and the prefix to be included
 // in the message if relevant. If there are any `ordered-by user` lists within
 // the input struct, then they will be treated as "telemetry-atomic", and put
-// into separate notifications at the beginning of the returned list where each
-// notification represents the entirety of the data of the ordered list, and
-// serialized in order of the list elements.
+// into separate atomic notifications after the initial notification containing
+// the non-atomic updates.
 //
 // Note: Within the generated notifications there could be data sharing for
 // space and compute optimization. Make a deep copy if one plans to modify the
@@ -401,46 +419,14 @@ func findUpdatedOrderedListLeaves(leaves any, s GoOrderedList, parent *gnmiPath)
 		return fmt.Errorf("internal ygot error: leaves is not an expected type: %T", leaves)
 	}
 
-	var atomicLeaves []*pathval
-	if err := yreflect.RangeOrderedMap(s, func(k reflect.Value, v reflect.Value) bool {
-		childPath, err := mapValuePath(k, v, parent)
-		if err != nil {
-			errs.Add(err)
-			return true
-		}
-
-		goStruct, ok := v.Interface().(GoStruct)
-		if !ok {
-			errs.Add(fmt.Errorf("%v: was not a valid GoStruct", parent))
-			return true
-		}
-		errs.Add(findUpdatedLeaves(&atomicLeaves, goStruct, childPath))
-		return true
-	}); err != nil {
+	atomicLeaves, subtreePath, err := orderedMapLeaves(s, parent)
+	if err != nil {
 		errs.Add(err)
 		return errs.Err()
 	}
 
 	if len(atomicLeaves) > 0 {
-		// TODO(wenbli): Make this more robust by potentially introducing another struct
-		// tag to indicate which element in the compressed path should the prefix cutoff
-		// be based on the placement of the atomic extension, although for ordered-maps
-		// it should always be at the container level. Need more discussion on this.
-		// The current use case is for BGP policy statements:
-		// https://github.com/openconfig/public/pull/867
-		// The reason for this parentPath hack is that compressed lists are represented
-		// using two elements, and only the first element should be in the prefix in the
-		// generated Notification rather than the whole set of elements since the atomic
-		// is at the container level.
-		parentPath := parent.Copy()
-		if parentPath.Len() > 1 {
-			if parentPath.isPathElemPath() {
-				parentPath.pathElemPath = parentPath.pathElemPath[:parentPath.Len()-1]
-			} else if parentPath.isStringSlicePath() {
-				parentPath.stringSlicePath = parentPath.stringSlicePath[:parentPath.Len()-1]
-			}
-		}
-		leavesMap[&path{parentPath}] = atomicLeaves
+		leavesMap[&path{subtreePath}] = atomicLeaves
 	}
 	return errs.Err()
 }
@@ -759,6 +745,31 @@ func sliceToScalarArray(v []any) (*gnmipb.ScalarArray, error) {
 	return arr, nil
 }
 
+// addToNotification adds the given path value pair to the given notification,
+// stripping the given prefix.
+func addToNotification(pk *path, value any, n *gnmipb.Notification, pfx *gnmiPath) error {
+	path, err := pk.p.StripPrefix(pfx)
+	if err != nil {
+		return err
+	}
+
+	ppath, err := path.ToProto()
+	if err != nil {
+		return err
+	}
+
+	val, err := EncodeTypedValue(value, gnmipb.Encoding_JSON)
+	if err != nil {
+		return err
+	}
+
+	n.Update = append(n.Update, &gnmipb.Update{
+		Path: ppath,
+		Val:  val,
+	})
+	return nil
+}
+
 // leavesToNotifications takes an input map of leaves, and outputs a slice of
 // notifications that corresponds to the leaf update, the supplied timestamp is
 // used in the set of notifications. If an error is encountered it is returned.
@@ -768,29 +779,6 @@ func sliceToScalarArray(v []any) (*gnmipb.ScalarArray, error) {
 // fragmentation of Updates across Notification messages in a future
 // implementation. We return a slice to keep the API stable.
 func leavesToNotifications(leaves map[*path]any, ts int64, pfx *gnmiPath) ([]*gnmipb.Notification, error) {
-	addToNotification := func(pk *path, value any, n *gnmipb.Notification, pfx *gnmiPath) error {
-		path, err := pk.p.StripPrefix(pfx)
-		if err != nil {
-			return err
-		}
-
-		ppath, err := path.ToProto()
-		if err != nil {
-			return err
-		}
-
-		val, err := EncodeTypedValue(value, gnmipb.Encoding_JSON)
-		if err != nil {
-			return err
-		}
-
-		n.Update = append(n.Update, &gnmipb.Update{
-			Path: ppath,
-			Val:  val,
-		})
-		return nil
-	}
-
 	var notifs []*gnmipb.Notification
 
 	// Non-"telemetry-atomic" values.
@@ -807,7 +795,7 @@ func leavesToNotifications(leaves map[*path]any, ts int64, pfx *gnmiPath) ([]*gn
 	for pk, v := range leaves {
 		// This is for handling "telemetry-atomic" subtrees that have
 		// leaves bundled and possibly in a certain order.
-		if pvs, ok := v.([]*pathval); ok && len(pvs) > 0 {
+		if pvs, ok := v.([]*pathval); ok {
 			// Check that the subtree path matches the prefix, and
 			// then provide the subtree path itself as the prefix.
 			subtreePfx := pk.p
@@ -815,28 +803,19 @@ func leavesToNotifications(leaves map[*path]any, ts int64, pfx *gnmiPath) ([]*gn
 				return nil, err
 			}
 
-			no := &gnmipb.Notification{
-				Timestamp: ts,
-				Atomic:    true,
-			}
-			p, err := subtreePfx.ToProto()
+			notif, err := createAtomicNotif(pvs, ts, subtreePfx)
 			if err != nil {
 				return nil, err
 			}
-			no.Prefix = p
-
-			for _, pv := range pvs {
-				if err := addToNotification(pv.path, pv.val, no, subtreePfx); err != nil {
-					return nil, err
-				}
+			if notif != nil {
+				notifs = append(notifs, notif)
 			}
-			notifs = append(notifs, no)
 		} else if err := addToNotification(pk, v, n, pfx); err != nil {
 			return nil, err
 		}
 	}
 	if len(n.Update) > 0 {
-		notifs = append(notifs, n)
+		notifs = append([]*gnmipb.Notification{n}, notifs...)
 	}
 
 	return notifs, nil


Test patch:
diff --git a/integration_tests/schemaops/ctestschema/ctestschema.go b/integration_tests/schemaops/ctestschema/ctestschema.go
index 4aedf8e..8560a77 100644
--- a/integration_tests/schemaops/ctestschema/ctestschema.go
+++ b/integration_tests/schemaops/ctestschema/ctestschema.go
@@ -137,6 +137,8 @@ type Device struct {
 	ΛOrderedList           []ygot.Annotation                 `path:"ordered-lists/@ordered-list" ygotAnnotation:"true"`
 	OrderedMultikeyedList  *OrderedMultikeyedList_OrderedMap `path:"ordered-multikeyed-lists/ordered-multikeyed-list" module:"ctestschema/ctestschema"`
 	ΛOrderedMultikeyedList []ygot.Annotation                 `path:"ordered-multikeyed-lists/@ordered-multikeyed-list" ygotAnnotation:"true"`
+	OtherData              *OtherData                        `path:"other-data" module:"ctestschema"`
+	ΛOtherData             []ygot.Annotation                 `path:"@other-data" ygotAnnotation:"true"`
 	UnorderedList          map[string]*UnorderedList         `path:"unordered-lists/unordered-list" module:"ctestschema/ctestschema"`
 	ΛUnorderedList         []ygot.Annotation                 `path:"unordered-lists/@unordered-list" ygotAnnotation:"true"`
 }
@@ -273,6 +275,26 @@ func (t *Device) AppendUnorderedList(v *UnorderedList) error {
 	return nil
 }
 
+// GetOrCreateOtherData retrieves the value of the OtherData field
+// or returns the existing field if it already exists.
+func (t *Device) GetOrCreateOtherData() *OtherData {
+	if t.OtherData != nil {
+		return t.OtherData
+	}
+	t.OtherData = &OtherData{}
+	return t.OtherData
+}
+
+// GetOtherData returns the value of the OtherData struct pointer
+// from Device. If the receiver or the field OtherData is nil, nil
+// is returned such that the Get* methods can be safely chained.
+func (t *Device) GetOtherData() *OtherData {
+	if t != nil && t.OtherData != nil {
+		return t.OtherData
+	}
+	return nil
+}
+
 // AppendNewOrderedList creates a new entry in the OrderedList
 // ordered map of the Device struct. The keys of the list are
 // populated from the input arguments.
@@ -1107,6 +1129,57 @@ func (*OrderedMultikeyedList) ΛBelongingModule() string {
 	return "ctestschema"
 }
 
+// OtherData represents the /ctestschema/other-data YANG schema element.
+type OtherData struct {
+	ΛMetadata []ygot.Annotation `path:"@" ygotAnnotation:"true"`
+	Motd      *string           `path:"config/motd" module:"ctestschema/ctestschema" shadow-path:"state/motd" shadow-module:"ctestschema/ctestschema"`
+	ΛMotd     []ygot.Annotation `path:"config/@motd" ygotAnnotation:"true"`
+}
+
+// IsYANGGoStruct ensures that OtherData implements the yang.GoStruct
+// interface. This allows functions that need to handle this struct to
+// identify it as being generated by ygen.
+func (*OtherData) IsYANGGoStruct() {}
+
+// GetMotd retrieves the value of the leaf Motd from the OtherData
+// struct. If the field is unset but has a default value in the YANG schema,
+// then the default value will be returned.
+// Caution should be exercised whilst using this method since when without a
+// default value, it will return the Go zero value if the field is explicitly
+// unset. If the caller explicitly does not care if Motd is set, it can
+// safely use t.GetMotd() to retrieve the value. In the case that the
+// caller has different actions based on whether the leaf is set or unset, it
+// should use 'if t.Motd == nil' before retrieving the leaf's value.
+func (t *OtherData) GetMotd() string {
+	if t == nil || t.Motd == nil {
+		return ""
+	}
+	return *t.Motd
+}
+
+// Validate validates s against the YANG schema corresponding to its type.
+func (t *OtherData) ΛValidate(opts ...ygot.ValidationOption) error {
+	if err := ytypes.Validate(SchemaTree["OtherData"], t, opts...); err != nil {
+		return err
+	}
+	return nil
+}
+
+// Validate validates s against the YANG schema corresponding to its type.
+func (t *OtherData) Validate(opts ...ygot.ValidationOption) error {
+	return t.ΛValidate(opts...)
+}
+
+// ΛEnumTypeMap returns a map, keyed by YANG schema path, of the enumerated types
+// that are included in the generated code.
+func (t *OtherData) ΛEnumTypeMap() map[string][]reflect.Type { return ΛEnumTypes }
+
+// ΛBelongingModule returns the name of the module that defines the namespace
+// of OtherData.
+func (*OtherData) ΛBelongingModule() string {
+	return "ctestschema"
+}
+
 // UnorderedList represents the /ctestschema/unordered-lists/unordered-list YANG schema element.
 type UnorderedList struct {
 	ΛMetadata []ygot.Annotation `path:"@" ygotAnnotation:"true"`
@@ -1195,107 +1268,114 @@ var (
 	// contents of a goyang yang.Entry struct, which defines the schema for the
 	// fields within the struct.
 	ySchema = []byte{
-		0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x5d, 0x4f, 0x6f, 0xdb, 0xc6,
-		0x13, 0xbd, 0xfb, 0x53, 0x10, 0x3c, 0x5b, 0xb1, 0xa5, 0x9f, 0xff, 0xfd, 0x74, 0x73, 0xe3, 0x06,
-		0x05, 0x52, 0xb7, 0x41, 0xd2, 0xf4, 0x52, 0x04, 0x05, 0x43, 0xaf, 0x15, 0xc2, 0x32, 0x69, 0x90,
-		0x4b, 0xd7, 0x42, 0xa1, 0xef, 0x5e, 0x50, 0xa4, 0x18, 0x91, 0x12, 0xa9, 0x99, 0xd9, 0x25, 0xcd,
-		0xd8, 0x0f, 0xc8, 0x21, 0x96, 0x76, 0xb5, 0xdc, 0x99, 0x79, 0xb3, 0x33, 0x6f, 0x77, 0x96, 0xff,
-		0x1e, 0x38, 0x8e, 0xe3, 0xb8, 0xbf, 0x79, 0xf7, 0xca, 0x9d, 0x3a, 0xee, 0x8d, 0x7a, 0x0c, 0x7c,
-		0xe5, 0x1e, 0xe6, 0x9f, 0xbe, 0x0f, 0xc2, 0x1b, 0x77, 0xea, 0x8c, 0x8b, 0x3f, 0xdf, 0x46, 0xe1,
-		0x6d, 0x30, 0x73, 0xa7, 0xce, 0x71, 0xf1, 0xc1, 0x55, 0x10, 0xbb, 0x53, 0x27, 0xff, 0x89, 0xd5,
-		0x07, 0x51, 0x7c, 0xa3, 0x62, 0x75, 0x33, 0x9a, 0x07, 0x89, 0x4e, 0x2a, 0x5f, 0x55, 0x46, 0xa9,
-		0x36, 0x3b, 0xac, 0x36, 0xaa, 0x0e, 0x5a, 0x7e, 0x5c, 0x1f, 0xbc, 0xfc, 0xe2, 0x43, 0xac, 0x6e,
-		0x83, 0xa7, 0xad, 0xc1, 0x2a, 0x03, 0xfa, 0x5b, 0xc3, 0xac, 0xbe, 0xfe, 0x14, 0xa5, 0xb1, 0xaf,
-		0x76, 0x76, 0xcd, 0x1f, 0x45, 0x2d, 0xfe, 0x89, 0xe2, 0xec, 0x69, 0xdc, 0x87, 0x7c, 0x94, 0xc3,
-		0xdd, 0x0d, 0x7f, 0xf1, 0x92, 0xcb, 0x78, 0x96, 0xde, 0xab, 0x50, 0xbb, 0x53, 0x47, 0xc7, 0xa9,
-		0x6a, 0x68, 0xb8, 0xd1, 0x6a, 0xf5, 0x50, 0x5b, 0xad, 0x96, 0x95, 0x4f, 0x96, 0xb5, 0xb9, 0xd6,
-		0x05, 0xbe, 0x53, 0xf0, 0xcd, 0xf3, 0xd9, 0x25, 0xff, 0xa6, 0x29, 0xed, 0x56, 0xc3, 0x5e, 0x75,
-		0x50, 0xd4, 0x42, 0x54, 0x0f, 0x55, 0x4d, 0x6c, 0x75, 0xb1, 0xd5, 0x46, 0x57, 0xdf, 0x6e, 0x35,
-		0x36, 0xa8, 0x73, 0xaf, 0x5a, 0xcb, 0x06, 0xfe, 0x5a, 0xda, 0x7b, 0x24, 0x50, 0x0a, 0x34, 0x6f,
-		0xbf, 0x67, 0x36, 0xed, 0x2a, 0x26, 0xab, 0x9a, 0xa3, 0x72, 0xa6, 0xea, 0xb9, 0x26, 0x20, 0x36,
-		0x05, 0xb1, 0x49, 0xf0, 0x4d, 0xa3, 0xdd, 0x44, 0xf6, 0x98, 0x0a, 0xd9, 0x64, 0xca, 0x86, 0x77,
-		0x6a, 0x41, 0x17, 0xdb, 0x5a, 0x2b, 0x59, 0x27, 0xe2, 0xbc, 0x0b, 0x23, 0x3a, 0x26, 0x36, 0xa7,
-		0x1a, 0x93, 0xc4, 0xa8, 0x84, 0xc6, 0x25, 0x35, 0x32, 0x63, 0x63, 0x33, 0x36, 0x3a, 0xb9, 0xf1,
-		0xd1, 0x8c, 0x90, 0x68, 0x8c, 0xe5, 0x63, 0xfc, 0xb1, 0x78, 0x50, 0x32, 0x4d, 0x25, 0x3a, 0x0e,
-		0xc2, 0x19, 0x47, 0x59, 0x6b, 0xe7, 0x75, 0x61, 0x75, 0x06, 0x97, 0x61, 0x18, 0x69, 0x4f, 0x07,
-		0x51, 0xc8, 0x9b, 0xc7, 0x62, 0x16, 0xe9, 0x51, 0xe4, 0x8f, 0xfc, 0xe8, 0xfe, 0x21, 0x56, 0x49,
-		0x92, 0x2d, 0xae, 0xca, 0xbb, 0xcd, 0x7e, 0x84, 0x28, 0xe2, 0x03, 0x0b, 0x53, 0x70, 0x1f, 0xbd,
-		0x79, 0xaa, 0xf8, 0x70, 0xcf, 0xbb, 0x01, 0xf0, 0x00, 0x3c, 0x00, 0x3f, 0x20, 0xc0, 0x1b, 0x05,
-		0x08, 0xcc, 0x89, 0xb9, 0x89, 0xff, 0x4d, 0xdd, 0x7b, 0x0f, 0x9e, 0xfe, 0x96, 0xa9, 0xe7, 0xc8,
-		0xd7, 0x2a, 0xd1, 0xf9, 0x67, 0x47, 0x95, 0x74, 0xad, 0xf2, 0xd7, 0x51, 0x11, 0x68, 0x1e, 0xc8,
-		0xe6, 0xd1, 0x32, 0x07, 0x52, 0xdc, 0xc2, 0x88, 0x57, 0x88, 0x6e, 0x0b, 0xc1, 0xee, 0x10, 0x83,
-		0x5d, 0xb2, 0x9b, 0x29, 0x25, 0x9d, 0x61, 0x31, 0x56, 0xb7, 0x14, 0x69, 0xaf, 0xfd, 0xca, 0x39,
-		0xa1, 0xed, 0x87, 0x02, 0x1e, 0x6f, 0xde, 0x14, 0x96, 0x7f, 0x94, 0x59, 0x5e, 0x07, 0xd6, 0xdf,
-		0x4e, 0xa4, 0x90, 0x12, 0xfb, 0x04, 0xe9, 0x1f, 0xd2, 0x3f, 0x1a, 0x31, 0x64, 0x42, 0x14, 0x09,
-		0xcd, 0x0a, 0xf1, 0xe1, 0x6b, 0x88, 0x0f, 0xa9, 0x66, 0xca, 0x25, 0xba, 0xcc, 0x88, 0x2f, 0x43,
-		0x93, 0x15, 0x9b, 0xae, 0x89, 0x09, 0x1b, 0x9a, 0xb2, 0xa9, 0x49, 0x5b, 0x33, 0x6d, 0x6b, 0x26,
-		0x6e, 0x6e, 0xea, 0x3c, 0x93, 0x67, 0x9a, 0xbe, 0x18, 0x02, 0x22, 0xe2, 0xce, 0x02, 0x91, 0x67,
-		0x98, 0xe7, 0x5b, 0x03, 0x87, 0x0d, 0x90, 0x58, 0x02, 0x8b, 0x2d, 0xd0, 0x58, 0x07, 0x8f, 0x75,
-		0x10, 0xd9, 0x03, 0x93, 0x0c, 0x54, 0x42, 0x70, 0xc9, 0x79, 0x08, 0x7b, 0xbc, 0x84, 0x21, 0x4f,
-		0x61, 0x4b, 0x02, 0x42, 0x1e, 0xc3, 0x3e, 0xaf, 0x61, 0x6e, 0x08, 0x02, 0x11, 0x30, 0x89, 0x4f,
-		0x2b, 0x44, 0x28, 0x1c, 0x26, 0x1c, 0x26, 0x1c, 0x26, 0x1c, 0xe6, 0x00, 0x1c, 0x66, 0xa7, 0x01,
-		0xac, 0xa1, 0xa0, 0x84, 0xc4, 0xb2, 0x21, 0xe7, 0x2c, 0x17, 0x11, 0x43, 0x3c, 0xa2, 0x10, 0xdd,
-		0x20, 0x34, 0x17, 0xae, 0x30, 0xc8, 0x53, 0x91, 0xa7, 0x76, 0xb8, 0x22, 0x08, 0x38, 0xf7, 0xc6,
-		0x25, 0xe0, 0x5c, 0xd0, 0x57, 0xc0, 0xc9, 0xf7, 0xe3, 0x1d, 0x12, 0xed, 0x69, 0x25, 0xf7, 0x0f,
-		0x79, 0xf7, 0x9e, 0x99, 0xac, 0x09, 0x3c, 0x04, 0x3c, 0x04, 0x98, 0x2c, 0x24, 0x66, 0x48, 0xcc,
-		0x90, 0x98, 0x19, 0x26, 0x66, 0xbd, 0x10, 0x3f, 0x0f, 0x5e, 0xac, 0x42, 0x3d, 0xb2, 0xe2, 0x62,
-		0x36, 0x7e, 0x0b, 0x9e, 0x06, 0x9e, 0x06, 0x9e, 0xa6, 0xf3, 0xc0, 0xdf, 0x46, 0x02, 0xb0, 0x2b,
-		0x11, 0x28, 0xff, 0x49, 0x33, 0x82, 0x7e, 0x9d, 0x18, 0xd8, 0x6b, 0xb8, 0x2e, 0xb8, 0x2e, 0x04,
-		0x49, 0xcf, 0x9f, 0xe3, 0x0d, 0x8e, 0xec, 0xcd, 0x59, 0x90, 0xae, 0xd8, 0x1c, 0xab, 0x87, 0x9d,
-		0xde, 0xaf, 0x62, 0x50, 0x4e, 0xed, 0xd4, 0xaf, 0x41, 0xa2, 0x2f, 0xb5, 0x66, 0x9e, 0x91, 0xba,
-		0x0e, 0xc2, 0x9f, 0xe7, 0x2a, 0x83, 0x6d, 0xc2, 0xf3, 0xb7, 0xee, 0xb5, 0xf7, 0xb4, 0xd1, 0x73,
-		0x7c, 0x71, 0x72, 0x72, 0x76, 0x7e, 0x72, 0x72, 0x7c, 0xfe, 0xbf, 0xf3, 0xe3, 0xff, 0x9f, 0x9e,
-		0x8e, 0xcf, 0xc6, 0xa7, 0x8c, 0x1f, 0xfb, 0x3d, 0x57, 0xd3, 0x4f, 0x06, 0x14, 0x78, 0x9a, 0xa8,
-		0x98, 0xcb, 0x70, 0x19, 0x38, 0xe7, 0x4d, 0x87, 0xbc, 0x36, 0xb2, 0xaf, 0x92, 0x58, 0xdf, 0x8a,
-		0x23, 0xae, 0x38, 0xdf, 0x95, 0x24, 0x06, 0xc0, 0x59, 0x96, 0x4a, 0xfd, 0x9c, 0x3d, 0x50, 0x3e,
-		0x35, 0xab, 0x20, 0x51, 0x4f, 0x3a, 0xf6, 0x46, 0x69, 0x98, 0x68, 0xef, 0xeb, 0x9c, 0x59, 0x3b,
-		0xb2, 0xa1, 0xb3, 0xa9, 0xf3, 0x17, 0x4b, 0x5a, 0x06, 0xf4, 0xa4, 0xc0, 0x48, 0x6d, 0xf3, 0x93,
-		0x46, 0xc6, 0xda, 0x1d, 0x47, 0xc9, 0x37, 0x5a, 0xc1, 0x02, 0x46, 0x6e, 0xfd, 0x65, 0x10, 0x75,
-		0x41, 0xb6, 0x17, 0x40, 0x97, 0xb5, 0xe1, 0x10, 0xa7, 0xbe, 0x0e, 0x0b, 0xbb, 0x2d, 0xa0, 0x9c,
-		0xad, 0x31, 0x7f, 0x6f, 0xfc, 0xdf, 0x7d, 0x45, 0xd5, 0x49, 0xd5, 0x72, 0x88, 0x0e, 0xca, 0x34,
-		0x68, 0x5b, 0x3c, 0xac, 0x2d, 0x1d, 0x76, 0x59, 0xc6, 0x04, 0x65, 0x19, 0x16, 0x13, 0x1c, 0x54,
-		0xe5, 0x77, 0x95, 0xad, 0xa3, 0x08, 0x03, 0x45, 0xba, 0xfc, 0x55, 0x86, 0xe0, 0x85, 0x04, 0x5b,
-		0x00, 0x72, 0xca, 0x1f, 0xc8, 0x02, 0xb2, 0x2c, 0x20, 0x8b, 0x4f, 0x99, 0x4b, 0x28, 0xf2, 0x6d,
-		0x4a, 0xbc, 0x12, 0x1e, 0x91, 0x59, 0x71, 0x3b, 0x38, 0x8d, 0xa3, 0x91, 0xf0, 0x7e, 0x8a, 0xb2,
-		0x27, 0x30, 0x0a, 0x8c, 0x62, 0xf5, 0xab, 0x8c, 0x89, 0x2b, 0x5f, 0x80, 0x27, 0xe0, 0xe9, 0xe5,
-		0x70, 0x16, 0x84, 0x0d, 0x8f, 0xa5, 0xa5, 0x6b, 0x07, 0xf7, 0x6e, 0x58, 0xd0, 0x36, 0x28, 0xe8,
-		0x1b, 0x12, 0xb5, 0x0d, 0x88, 0x96, 0xdd, 0x06, 0xc6, 0xee, 0x02, 0x87, 0xa8, 0xe5, 0xc0, 0x5f,
-		0x4c, 0xc0, 0x8a, 0xa0, 0xce, 0x24, 0x56, 0x65, 0x74, 0x15, 0x99, 0xdd, 0x6f, 0xb2, 0x18, 0x06,
-		0x7b, 0xcf, 0x61, 0xeb, 0x19, 0xcc, 0x14, 0x91, 0x8d, 0x37, 0xa1, 0xa6, 0xd8, 0x6c, 0xbb, 0x1d,
-		0x7a, 0x8a, 0xc6, 0xa6, 0x2f, 0x85, 0x66, 0xf1, 0x85, 0xa5, 0x67, 0xa2, 0xab, 0x93, 0xb9, 0x38,
-		0xb7, 0x95, 0x50, 0x6d, 0xa0, 0xb0, 0x77, 0x4b, 0x66, 0xc9, 0xbc, 0x3f, 0x77, 0xcf, 0xbc, 0xc8,
-		0xf3, 0xa9, 0x3e, 0xcd, 0xf7, 0x31, 0x37, 0xc6, 0x2b, 0xcd, 0xe8, 0x3e, 0x9d, 0xeb, 0xe0, 0x4e,
-		0x2d, 0xe8, 0xd7, 0x22, 0x6f, 0xf5, 0xc0, 0x0d, 0xc9, 0x64, 0x0d, 0xef, 0xbd, 0x21, 0xb9, 0x26,
-		0x5c, 0xfa, 0x65, 0xc9, 0xf5, 0x8e, 0xb8, 0x37, 0x19, 0xf7, 0x26, 0x5b, 0x49, 0x83, 0xb0, 0x43,
-		0x33, 0xd0, 0x1d, 0x9a, 0xb1, 0x68, 0x8b, 0x66, 0x8c, 0xac, 0x1a, 0x59, 0xf5, 0x0b, 0xca, 0xaa,
-		0x5f, 0xc7, 0xcd, 0xc9, 0x77, 0x6a, 0x31, 0x11, 0xc1, 0x7d, 0x02, 0xb8, 0x03, 0xee, 0xfd, 0xc1,
-		0x3d, 0x0d, 0x42, 0x7d, 0x76, 0x22, 0x80, 0xfb, 0x05, 0xa3, 0xcb, 0x47, 0x2f, 0x9c, 0xa9, 0x3e,
-		0xce, 0xd8, 0x5d, 0x07, 0xf2, 0xdb, 0x37, 0xdc, 0x3f, 0x0b, 0xe6, 0x5b, 0x5a, 0x71, 0xf0, 0x2e,
-		0xf6, 0xfc, 0xcc, 0x5f, 0x5d, 0x05, 0xb3, 0x80, 0x7b, 0x94, 0xb6, 0xaa, 0x1b, 0x35, 0xf3, 0x74,
-		0xf0, 0x98, 0x3d, 0xcb, 0xad, 0x37, 0x4f, 0x54, 0x2f, 0x35, 0x23, 0xd7, 0xde, 0x93, 0xb9, 0xe8,
-		0xcc, 0x8e, 0x00, 0x0f, 0x55, 0x9a, 0x2f, 0xfa, 0x80, 0x21, 0xde, 0x34, 0x80, 0x15, 0x13, 0x2b,
-		0x26, 0x02, 0xe4, 0x17, 0xfd, 0xa6, 0x81, 0x3a, 0x03, 0xda, 0xf4, 0x45, 0xb7, 0xef, 0x1f, 0x18,
-		0xb3, 0x5e, 0x40, 0x30, 0xc6, 0x1b, 0x08, 0xf0, 0x06, 0x82, 0xe7, 0x79, 0x03, 0xc1, 0xb8, 0x23,
-		0x00, 0x4c, 0x58, 0x00, 0x98, 0x00, 0x00, 0x00, 0xc0, 0xf3, 0x00, 0x60, 0x82, 0xe2, 0x0e, 0x40,
-		0x00, 0x5b, 0x07, 0xc8, 0x8c, 0x90, 0x19, 0x0d, 0x29, 0x33, 0x02, 0x33, 0x0f, 0x34, 0x81, 0x99,
-		0x07, 0x33, 0x0f, 0x66, 0x1e, 0xcc, 0xfc, 0x00, 0x98, 0x79, 0x10, 0xdf, 0x58, 0x90, 0x10, 0xde,
-		0xf5, 0x19, 0xde, 0xfd, 0xb0, 0xbc, 0xf2, 0xb3, 0x54, 0x61, 0x8c, 0x9d, 0x96, 0xa8, 0xb5, 0xe3,
-		0x5a, 0x0c, 0xde, 0x7a, 0x83, 0xf2, 0x0c, 0x94, 0x67, 0x48, 0x95, 0xec, 0xa0, 0x3c, 0x83, 0x1a,
-		0xc1, 0xf4, 0x50, 0x9e, 0x41, 0xf5, 0x88, 0xec, 0xa2, 0x8d, 0xeb, 0xb2, 0xff, 0xa0, 0xca, 0x37,
-		0xb6, 0x6a, 0x2b, 0x08, 0x95, 0x1c, 0x69, 0xd8, 0xfe, 0x3a, 0xe6, 0xef, 0xd6, 0x1f, 0xb6, 0xbd,
-		0x80, 0x19, 0x75, 0x1b, 0x2d, 0xaa, 0x6d, 0xac, 0xdb, 0xa8, 0xca, 0x74, 0x7f, 0xb9, 0x46, 0xad,
-		0x3d, 0xaa, 0x34, 0x50, 0xa5, 0x61, 0x25, 0x79, 0xc2, 0x56, 0x0b, 0xee, 0xd1, 0x42, 0x26, 0x8e,
-		0x4c, 0x1c, 0x47, 0xd0, 0x70, 0xe6, 0x14, 0x80, 0x07, 0xe0, 0x01, 0xf8, 0x17, 0xc2, 0x0d, 0xd6,
-		0x92, 0xb6, 0xda, 0xdf, 0xdd, 0x9e, 0x30, 0x65, 0x9d, 0xaf, 0xc3, 0xf1, 0x3a, 0x1c, 0xaf, 0x7b,
-		0x9e, 0xe3, 0x75, 0x38, 0x5d, 0x07, 0x04, 0x20, 0xe5, 0x43, 0x04, 0x88, 0x08, 0x70, 0x38, 0x11,
-		0x20, 0x32, 0x2a, 0xe0, 0x09, 0x78, 0xea, 0x13, 0x4f, 0x3f, 0x48, 0xc2, 0xf2, 0x8a, 0x2e, 0x90,
-		0x34, 0x38, 0xb4, 0x10, 0xa6, 0xf3, 0x39, 0x63, 0x2b, 0xbe, 0xf9, 0xbc, 0x5b, 0x87, 0x7b, 0xb4,
-		0xed, 0x8a, 0xa6, 0xef, 0xc8, 0x7e, 0x5e, 0xf7, 0x1b, 0xc4, 0x4e, 0x6c, 0x7d, 0x93, 0xb4, 0x69,
-		0x03, 0xf6, 0x60, 0x63, 0xdc, 0xa6, 0xf1, 0xdc, 0x20, 0x79, 0x5b, 0x52, 0x14, 0x9f, 0x56, 0xbf,
-		0xbf, 0xe5, 0x1f, 0xdd, 0x20, 0x79, 0xe7, 0xdd, 0xa9, 0x8f, 0x51, 0xb4, 0xed, 0x3b, 0xeb, 0xcf,
-		0xe9, 0x6e, 0x7e, 0x55, 0x91, 0xe1, 0x95, 0x7a, 0x0c, 0xfc, 0x02, 0x56, 0xcb, 0x83, 0xe5, 0x7f,
-		0x00, 0x00, 0x00, 0xff, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff, 0x7d, 0xb3, 0x2a, 0x10, 0x37, 0xa9,
-		0x00, 0x00,
+		0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x5d, 0xd1, 0x6f, 0xdb, 0xb6,
+		0x13, 0x7e, 0xcf, 0x5f, 0x21, 0xe8, 0xb9, 0x6e, 0x62, 0xff, 0xd2, 0x24, 0x3f, 0xbf, 0x65, 0xcd,
+		0x8a, 0x01, 0x5d, 0xb6, 0xa2, 0x5d, 0xf7, 0x32, 0x14, 0x83, 0x6a, 0x33, 0x8e, 0x10, 0x5b, 0x0a,
+		0x24, 0x2a, 0x8b, 0x31, 0xf8, 0x7f, 0x1f, 0x64, 0xd9, 0xaa, 0x25, 0x4b, 0xf2, 0xdd, 0x91, 0x52,
+		0x94, 0xe4, 0x03, 0xfa, 0xd0, 0xd8, 0xa4, 0x29, 0xf2, 0xee, 0x3b, 0xde, 0x7d, 0xc7, 0xa3, 0xfe,
+		0x3d, 0x72, 0x1c, 0xc7, 0x71, 0x7f, 0xf3, 0x16, 0xca, 0x1d, 0x3b, 0xee, 0x54, 0x3d, 0xf8, 0x13,
+		0xe5, 0xbe, 0xc9, 0x3e, 0xfd, 0xe8, 0x07, 0x53, 0x77, 0xec, 0x0c, 0x37, 0x7f, 0xbe, 0x0f, 0x83,
+		0x1b, 0x7f, 0xe6, 0x8e, 0x9d, 0x93, 0xcd, 0x07, 0x57, 0x7e, 0xe4, 0x8e, 0x9d, 0xec, 0x27, 0xd6,
+		0x1f, 0x84, 0xd1, 0x54, 0x45, 0x6a, 0x3a, 0x98, 0xfb, 0xb1, 0x8e, 0x0b, 0x5f, 0x15, 0x46, 0x29,
+		0x36, 0x7b, 0x53, 0x6c, 0x54, 0x1c, 0x34, 0xff, 0xb8, 0x3c, 0x78, 0xfe, 0xc5, 0xa7, 0x48, 0xdd,
+		0xf8, 0x8f, 0x7b, 0x83, 0x15, 0x06, 0x9c, 0xec, 0x0d, 0xb3, 0xfe, 0xfa, 0x4b, 0x98, 0x44, 0x13,
+		0x55, 0xd9, 0x35, 0x7b, 0x14, 0xb5, 0xfc, 0x27, 0x8c, 0xd2, 0xa7, 0x71, 0xef, 0xb3, 0x51, 0xde,
+		0x54, 0x37, 0xfc, 0xc5, 0x8b, 0x2f, 0xa3, 0x59, 0xb2, 0x50, 0x81, 0x76, 0xc7, 0x8e, 0x8e, 0x12,
+		0x55, 0xd3, 0x70, 0xa7, 0xd5, 0xfa, 0xa1, 0xf6, 0x5a, 0xad, 0x0a, 0x9f, 0xac, 0x4a, 0x73, 0x2d,
+		0x2f, 0x78, 0xe5, 0xc2, 0xd7, 0xcf, 0xa7, 0x6a, 0xfd, 0xeb, 0xa6, 0x54, 0x2d, 0x86, 0x83, 0xe2,
+		0xa0, 0x88, 0x85, 0x28, 0x1e, 0xaa, 0x98, 0xd8, 0xe2, 0x62, 0x8b, 0x8d, 0x2e, 0xbe, 0x6a, 0x31,
+		0xd6, 0x88, 0xf3, 0xa0, 0x58, 0xf3, 0x06, 0x93, 0xed, 0x6a, 0x1f, 0x58, 0x81, 0x7c, 0x41, 0xb3,
+		0xf6, 0x07, 0x66, 0xd3, 0x2c, 0x62, 0xb2, 0xa8, 0x39, 0x22, 0x67, 0x8a, 0x9e, 0xab, 0x02, 0x62,
+		0x55, 0x10, 0xab, 0x04, 0x5f, 0x35, 0x9a, 0x55, 0xe4, 0x80, 0xaa, 0x90, 0x55, 0x26, 0x6f, 0x78,
+		0xa7, 0x96, 0xf4, 0x65, 0xdb, 0x4a, 0x25, 0xed, 0x44, 0x9c, 0xf7, 0x46, 0x89, 0x4e, 0x88, 0xcd,
+		0xa9, 0xca, 0x24, 0x51, 0x2a, 0xa1, 0x72, 0x49, 0x95, 0xcc, 0x58, 0xd9, 0x8c, 0x95, 0x4e, 0xae,
+		0x7c, 0x34, 0x25, 0x24, 0x2a, 0x63, 0xfe, 0x18, 0x7f, 0x2c, 0xef, 0x95, 0x4c, 0x52, 0xb1, 0x8e,
+		0xfc, 0x60, 0xc6, 0x11, 0xd6, 0xd6, 0x78, 0x5d, 0x58, 0x9d, 0xc1, 0x65, 0x10, 0x84, 0xda, 0xd3,
+		0x7e, 0x18, 0xf0, 0xe6, 0xb1, 0x9c, 0x85, 0x7a, 0x10, 0x4e, 0x06, 0x93, 0x70, 0x71, 0x1f, 0xa9,
+		0x38, 0x4e, 0x37, 0x57, 0xe5, 0xdd, 0xa4, 0x3f, 0x42, 0x5c, 0xe2, 0x23, 0x0b, 0x53, 0x70, 0x1f,
+		0xbc, 0x79, 0xa2, 0xf8, 0x70, 0xcf, 0xba, 0x01, 0xf0, 0x00, 0x3c, 0x00, 0xdf, 0x23, 0xc0, 0x1b,
+		0x39, 0x08, 0xcc, 0x89, 0xb9, 0xf1, 0xe4, 0x56, 0x2d, 0xbc, 0x7b, 0x4f, 0xdf, 0xa6, 0xe2, 0x39,
+		0x9e, 0x68, 0x15, 0xeb, 0xec, 0xb3, 0xe3, 0x42, 0xb8, 0x56, 0xf8, 0xeb, 0x78, 0xe3, 0x68, 0x1e,
+		0xc9, 0xe6, 0xd1, 0x30, 0x07, 0x92, 0xdf, 0xc2, 0xf0, 0x57, 0x88, 0x66, 0x0b, 0xce, 0x6e, 0x1f,
+		0x9d, 0x5d, 0xb2, 0x99, 0xc9, 0x57, 0x3a, 0xc5, 0x62, 0xa4, 0x6e, 0x28, 0xab, 0xbd, 0xb5, 0x2b,
+		0xe7, 0x84, 0xb6, 0x9f, 0x36, 0xf0, 0x78, 0xfb, 0x76, 0xa3, 0xf9, 0xc7, 0xa9, 0xe6, 0xb5, 0xa0,
+		0xfd, 0xcd, 0x44, 0x0a, 0x29, 0xb0, 0x8f, 0x11, 0xfe, 0x21, 0xfc, 0xa3, 0x11, 0x43, 0x26, 0x44,
+		0x91, 0x50, 0xad, 0xe0, 0x1f, 0xbe, 0x06, 0xff, 0x90, 0xaa, 0xa6, 0x5c, 0xa2, 0xcb, 0x8c, 0xf8,
+		0x32, 0x54, 0x59, 0xb1, 0xea, 0x9a, 0xa8, 0xb0, 0xa1, 0x2a, 0x9b, 0xaa, 0xb4, 0x35, 0xd5, 0xb6,
+		0xa6, 0xe2, 0xe6, 0xaa, 0xce, 0x53, 0x79, 0xa6, 0xea, 0x8b, 0x21, 0x20, 0x22, 0xee, 0x2c, 0x10,
+		0x79, 0x86, 0x71, 0xbe, 0x35, 0x70, 0xd8, 0x00, 0x89, 0x25, 0xb0, 0xd8, 0x02, 0x8d, 0x75, 0xf0,
+		0x58, 0x07, 0x91, 0x3d, 0x30, 0xc9, 0x40, 0x25, 0x04, 0x97, 0x9c, 0x87, 0xb0, 0xc7, 0x4b, 0x18,
+		0xf2, 0x14, 0xb6, 0x56, 0x40, 0xc8, 0x63, 0xd8, 0xe7, 0x35, 0xcc, 0x15, 0x41, 0xb0, 0x04, 0x4c,
+		0xe2, 0xd3, 0x0a, 0x11, 0x0a, 0x83, 0x09, 0x83, 0x09, 0x83, 0x09, 0x83, 0xd9, 0x03, 0x83, 0xd9,
+		0xaa, 0x03, 0x6b, 0xb8, 0x50, 0x42, 0x62, 0xd9, 0x90, 0x73, 0x96, 0x2f, 0x11, 0x63, 0x79, 0x44,
+		0x2e, 0xba, 0x81, 0x6b, 0x2e, 0xdc, 0x61, 0x10, 0xa7, 0x22, 0x4e, 0x6d, 0x71, 0x47, 0x10, 0x70,
+		0xee, 0xb5, 0x5b, 0xc0, 0xb9, 0xa0, 0xaf, 0x80, 0x93, 0xef, 0xc6, 0x3a, 0xc4, 0xda, 0xd3, 0x4a,
+		0x6e, 0x1f, 0xb2, 0xee, 0x1d, 0x33, 0x59, 0x23, 0x58, 0x08, 0x58, 0x08, 0x30, 0x59, 0x08, 0xcc,
+		0x10, 0x98, 0x21, 0x30, 0x33, 0x0c, 0xcc, 0x3a, 0x21, 0x7e, 0xee, 0xbd, 0x48, 0x05, 0x7a, 0x60,
+		0xc5, 0xc4, 0xec, 0xfc, 0x16, 0x2c, 0x0d, 0x2c, 0x0d, 0x2c, 0x4d, 0xeb, 0x8e, 0xbf, 0x8d, 0x00,
+		0xa0, 0x2a, 0x10, 0xc8, 0xff, 0x49, 0x23, 0x82, 0x6e, 0x8d, 0x18, 0xd8, 0x6b, 0x98, 0x2e, 0x98,
+		0x2e, 0x38, 0x49, 0x4f, 0x1f, 0xe3, 0xf5, 0x8e, 0xec, 0xcd, 0x58, 0x90, 0xb6, 0xd8, 0x1c, 0xab,
+		0x87, 0x9d, 0x3e, 0xae, 0x7d, 0x50, 0x4e, 0xed, 0xd4, 0xaf, 0x7e, 0xac, 0x2f, 0xb5, 0x66, 0x9e,
+		0x91, 0xba, 0xf6, 0x83, 0x9f, 0xe7, 0x2a, 0x85, 0x6d, 0xcc, 0xb3, 0xb7, 0xee, 0xb5, 0xf7, 0xb8,
+		0xd3, 0x73, 0x78, 0x71, 0x7a, 0x7a, 0x76, 0x7e, 0x7a, 0x7a, 0x72, 0xfe, 0xbf, 0xf3, 0x93, 0xff,
+		0xbf, 0x7b, 0x37, 0x3c, 0x1b, 0xbe, 0x63, 0xfc, 0xd8, 0xef, 0x99, 0x98, 0x7e, 0x32, 0xa0, 0xc0,
+		0x93, 0x58, 0x45, 0x5c, 0x86, 0xcb, 0xc0, 0x38, 0xef, 0x1a, 0xe4, 0xad, 0x92, 0x7d, 0x97, 0xf8,
+		0xfa, 0x56, 0x0c, 0x71, 0xc1, 0xf8, 0xae, 0x57, 0xa2, 0x07, 0x9c, 0x65, 0x2e, 0xd4, 0xaf, 0xe9,
+		0x03, 0x65, 0x53, 0xb3, 0x0a, 0x12, 0xf5, 0xa8, 0x23, 0x6f, 0x90, 0x04, 0xb1, 0xf6, 0xbe, 0xcf,
+		0x99, 0xb5, 0x23, 0x3b, 0x32, 0x1b, 0x3b, 0x7f, 0xb1, 0x56, 0xcb, 0x80, 0x9e, 0x14, 0x28, 0xa9,
+		0x6d, 0x7e, 0xd2, 0x48, 0x59, 0xdb, 0xe3, 0x28, 0xf9, 0x4a, 0x2b, 0xd8, 0xc0, 0xc8, 0xad, 0xbf,
+		0xf5, 0xa2, 0x2e, 0xc8, 0xf6, 0x06, 0xe8, 0xb2, 0x12, 0x0e, 0x51, 0x32, 0xd1, 0xc1, 0x46, 0x6f,
+		0x37, 0x50, 0x4e, 0xf7, 0x98, 0xbf, 0x77, 0xfe, 0xef, 0xbe, 0xa2, 0xea, 0xa4, 0x62, 0x39, 0x44,
+		0x0b, 0x65, 0x1a, 0xb4, 0x14, 0x0f, 0x2b, 0xa5, 0xc3, 0x2e, 0xcb, 0x18, 0xa1, 0x2c, 0xc3, 0x62,
+		0x80, 0x83, 0xaa, 0xfc, 0xb6, 0xa2, 0x75, 0x14, 0x61, 0xa0, 0x48, 0x97, 0xbf, 0xcb, 0x10, 0xac,
+		0x90, 0x20, 0x05, 0x20, 0xa7, 0xfc, 0x81, 0x2c, 0x20, 0xcb, 0x02, 0xb2, 0xf8, 0x94, 0xb9, 0x84,
+		0x22, 0xdf, 0xa7, 0xc4, 0x0b, 0xee, 0x11, 0x99, 0x15, 0xb7, 0x83, 0xd3, 0x28, 0x1c, 0x08, 0xef,
+		0xa7, 0xc8, 0x7b, 0x02, 0xa3, 0xc0, 0x28, 0x76, 0xbf, 0xc2, 0x98, 0xb8, 0xf2, 0x05, 0x78, 0x02,
+		0x9e, 0x5e, 0x0e, 0x67, 0x41, 0x48, 0x78, 0xac, 0x2c, 0x5d, 0x3b, 0x78, 0x30, 0x61, 0x41, 0x4b,
+		0x50, 0xd0, 0x13, 0x12, 0xa5, 0x04, 0x44, 0x43, 0xb6, 0x81, 0x91, 0x5d, 0xe0, 0x10, 0xb5, 0x1c,
+		0xf8, 0x8b, 0x09, 0x58, 0x11, 0xd4, 0x99, 0xc4, 0xaa, 0x8c, 0xae, 0x22, 0xb3, 0xfb, 0x75, 0x1a,
+		0xc3, 0x60, 0xef, 0x39, 0x6c, 0x3d, 0x83, 0x99, 0x22, 0xb2, 0xf1, 0x26, 0xd4, 0x14, 0x9b, 0x6d,
+		0xb7, 0x43, 0x4f, 0xd1, 0xd8, 0xf4, 0x95, 0x50, 0x2d, 0xbe, 0xb1, 0xe4, 0x4c, 0x34, 0x75, 0x32,
+		0x13, 0xe7, 0x36, 0x12, 0xaa, 0x35, 0x14, 0x76, 0xf5, 0xca, 0xac, 0x98, 0xf7, 0xe7, 0x1e, 0x98,
+		0x17, 0x79, 0x3e, 0xc5, 0xa7, 0xf9, 0x31, 0xe6, 0xce, 0x78, 0xb9, 0x1a, 0x2d, 0x92, 0xb9, 0xf6,
+		0xef, 0xd4, 0x92, 0x7e, 0x2d, 0xf2, 0x5e, 0x0f, 0xdc, 0x90, 0x4c, 0x96, 0xf0, 0xc1, 0x1b, 0x92,
+		0x4b, 0x8b, 0x4b, 0xbf, 0x2c, 0xb9, 0xdc, 0x11, 0xf7, 0x26, 0xe3, 0xde, 0x64, 0x2b, 0x61, 0x10,
+		0x32, 0x34, 0x3d, 0xcd, 0xd0, 0x0c, 0x45, 0x29, 0x9a, 0x21, 0xa2, 0x6a, 0x44, 0xd5, 0x2f, 0x28,
+		0xaa, 0x7e, 0x1d, 0x37, 0x27, 0xdf, 0xa9, 0xe5, 0x48, 0x04, 0xf7, 0x11, 0xe0, 0x0e, 0xb8, 0x77,
+		0x07, 0xf7, 0xc4, 0x0f, 0xf4, 0xd9, 0xa9, 0x00, 0xee, 0x17, 0x8c, 0x2e, 0x9f, 0xbd, 0x60, 0xa6,
+		0xba, 0x38, 0x63, 0x77, 0xed, 0xcb, 0x6f, 0xdf, 0x70, 0xff, 0xdc, 0x30, 0xdf, 0xd2, 0x8a, 0x83,
+		0x0f, 0x91, 0x37, 0x49, 0xed, 0xd5, 0x95, 0x3f, 0xf3, 0xb9, 0x47, 0x69, 0x8b, 0xb2, 0x51, 0x33,
+		0x4f, 0xfb, 0x0f, 0xe9, 0xb3, 0xdc, 0x78, 0xf3, 0x58, 0x75, 0x52, 0x33, 0x72, 0xed, 0x3d, 0x9a,
+		0x2f, 0x9d, 0xd9, 0x11, 0xe0, 0xbe, 0xae, 0xe6, 0x8b, 0x3e, 0x60, 0x88, 0x37, 0x0d, 0x60, 0xc7,
+		0xc4, 0x8e, 0x09, 0x07, 0xf9, 0x45, 0xbf, 0x69, 0xa0, 0xcc, 0x80, 0xd6, 0x7d, 0xd1, 0xee, 0xfb,
+		0x07, 0x86, 0xac, 0x17, 0x10, 0x0c, 0xf1, 0x06, 0x02, 0xbc, 0x81, 0xe0, 0x69, 0xde, 0x40, 0x30,
+		0x6c, 0x09, 0x00, 0x23, 0x16, 0x00, 0x46, 0x00, 0x00, 0x00, 0xf0, 0x34, 0x00, 0x18, 0xa1, 0xb8,
+		0x03, 0x10, 0x40, 0xea, 0x00, 0x91, 0x11, 0x22, 0xa3, 0x3e, 0x45, 0x46, 0x60, 0xe6, 0x81, 0x26,
+		0x30, 0xf3, 0x60, 0xe6, 0xc1, 0xcc, 0x83, 0x99, 0xef, 0x01, 0x33, 0x0f, 0xe2, 0x1b, 0x1b, 0x12,
+		0xdc, 0xbb, 0x2e, 0xdd, 0xbb, 0x67, 0xcb, 0x2b, 0x3f, 0x49, 0x15, 0xc6, 0xd0, 0x69, 0xf0, 0x5a,
+		0x5b, 0xae, 0xc5, 0xe0, 0xed, 0x37, 0x28, 0xcf, 0x40, 0x79, 0x86, 0x54, 0xc8, 0x0e, 0xca, 0x33,
+		0xa8, 0x1e, 0x4c, 0x07, 0xe5, 0x19, 0x54, 0x8b, 0xc8, 0x2e, 0xda, 0xb8, 0xce, 0xfb, 0xf7, 0xaa,
+		0x7c, 0x63, 0xaf, 0xb6, 0x82, 0x52, 0xc9, 0xa1, 0x6f, 0x55, 0x34, 0x98, 0x7a, 0xda, 0x6b, 0xa8,
+		0xdd, 0xf8, 0xd1, 0x06, 0xd5, 0x1a, 0x64, 0x81, 0xd6, 0x56, 0x6b, 0x1c, 0x38, 0xb8, 0x4f, 0x3b,
+		0xb0, 0x8f, 0x5a, 0x0c, 0x23, 0xbf, 0xd6, 0x72, 0x2d, 0xc6, 0x22, 0xd4, 0x53, 0xba, 0xb7, 0xb0,
+		0x6e, 0x8d, 0x7c, 0x22, 0xf2, 0x89, 0xfc, 0x70, 0x86, 0x18, 0xc6, 0xd8, 0x0e, 0x3f, 0xc4, 0xe7,
+		0x74, 0x6c, 0x05, 0x12, 0x36, 0xbc, 0x82, 0x7c, 0x17, 0x6b, 0x3c, 0x59, 0x53, 0xb1, 0x6f, 0x57,
+		0xec, 0x46, 0xcd, 0xf9, 0x53, 0x52, 0xde, 0x94, 0x6c, 0xc1, 0x47, 0xb0, 0xe0, 0xb0, 0xe0, 0xb0,
+		0xe0, 0xaf, 0xcc, 0x82, 0xf7, 0xd2, 0x70, 0x36, 0x50, 0x47, 0x5d, 0xc6, 0x3b, 0x75, 0xf1, 0x48,
+		0x55, 0xc4, 0x96, 0xb6, 0xbd, 0x4a, 0x9b, 0x12, 0x82, 0xa1, 0x24, 0x28, 0x56, 0xc2, 0xd7, 0x46,
+		0x44, 0xe5, 0x86, 0x08, 0x8b, 0xcc, 0xc3, 0xa2, 0xe2, 0x9a, 0x1e, 0xde, 0x5b, 0x4b, 0xed, 0x11,
+		0x26, 0xa1, 0x64, 0x1d, 0x1b, 0xed, 0x73, 0xdb, 0x68, 0x71, 0xa9, 0xb0, 0x35, 0xe5, 0x92, 0x2a,
+		0x99, 0xb1, 0xb2, 0x19, 0x2b, 0x9d, 0x5c, 0xf9, 0xe8, 0xe9, 0x3a, 0x07, 0xf5, 0x38, 0x84, 0x7e,
+		0x28, 0xc0, 0x03, 0xe0, 0x01, 0x78, 0x00, 0xfe, 0x45, 0x16, 0xe0, 0x95, 0x82, 0xb6, 0xd2, 0xdf,
+		0xed, 0x96, 0xdb, 0xb1, 0x8a, 0x8d, 0xc0, 0x2c, 0x81, 0x59, 0x7a, 0x9a, 0x5a, 0x23, 0x94, 0x1a,
+		0x01, 0x01, 0x08, 0xf9, 0xe0, 0x01, 0xc2, 0x03, 0xec, 0x8f, 0x07, 0x88, 0x88, 0x0a, 0x78, 0x02,
+		0x9e, 0xba, 0xc4, 0xd3, 0x33, 0x09, 0x58, 0x5e, 0xd1, 0x6d, 0xfa, 0x06, 0x27, 0xb8, 0x83, 0x64,
+		0x3e, 0x67, 0x9c, 0x4b, 0xae, 0x2f, 0xfe, 0x69, 0x31, 0xc3, 0xda, 0x2c, 0x68, 0xfa, 0xf1, 0xd4,
+		0xaf, 0xdb, 0x7e, 0xbd, 0x38, 0x96, 0x5a, 0x4e, 0x92, 0xd6, 0x25, 0x60, 0x8f, 0x76, 0xc6, 0xad,
+		0x1b, 0xcf, 0xf5, 0xe3, 0xf7, 0x39, 0x45, 0xf1, 0x65, 0xfd, 0xfb, 0x7b, 0xf6, 0xd1, 0xf5, 0xe3,
+		0x0f, 0xde, 0x9d, 0xfa, 0x1c, 0x86, 0xfb, 0xb6, 0xb3, 0xfc, 0x9c, 0xee, 0xee, 0x57, 0x85, 0x35,
+		0xbc, 0x52, 0x0f, 0xfe, 0x64, 0x03, 0xab, 0xd5, 0xd1, 0xea, 0x3f, 0x00, 0x00, 0x00, 0xff, 0xff,
+		0x01, 0x00, 0x00, 0xff, 0xff, 0xa7, 0x48, 0x2e, 0x06, 0x44, 0xb6, 0x00, 0x00,
 	}
 )
 
diff --git a/integration_tests/schemaops/yang/ctestschema.yang b/integration_tests/schemaops/yang/ctestschema.yang
index 44fa929..9eba64f 100644
--- a/integration_tests/schemaops/yang/ctestschema.yang
+++ b/integration_tests/schemaops/yang/ctestschema.yang
@@ -6,6 +6,21 @@ module ctestschema {
     "ctestschema contains elements that are compatible for compressed (and
     therefore also uncompressed) generation options.";
 
+  container other-data {
+    container config {
+      uses other-data-config;
+    }
+
+    container state {
+      config false;
+      uses other-data-config;
+    }
+  }
+
+  grouping other-data-config {
+    leaf motd { type string; }
+  }
+
   container ordered-lists {
     list ordered-list {
       key "key";
diff --git a/util/reflect_exported_test.go b/util/reflect_exported_test.go
index 0588d14..56aee01 100644
--- a/util/reflect_exported_test.go
+++ b/util/reflect_exported_test.go
@@ -293,7 +293,7 @@ func TestForEachFieldOrderedMap(t *testing.T) {
 	}
 }
 
-func TestForEachDataField(t *testing.T) {
+func TestForEachDataFieldOrderedMap(t *testing.T) {
 	tests := []struct {
 		desc       string
 		inParent   any
diff --git a/util/reflect_test.go b/util/reflect_test.go
index 4bc8cf2..9137bb7 100644
--- a/util/reflect_test.go
+++ b/util/reflect_test.go
@@ -960,6 +960,32 @@ var (
 		return
 	}
 
+	PrintMapKeysSchemaAnnotationFuncSkipBasicStruct2 = func(ni *NodeInfo, in, out interface{}) (action IterationAction, errs Errors) {
+		if IsNilOrInvalidValue(ni.FieldKey) {
+			return
+		}
+		if key := ni.FieldKey.Interface(); key == "basicStruct2" {
+			action = DoNotIterateDescendants
+			return
+		}
+		outs := out.(*string)
+		s := "nil"
+		if !IsNilOrInvalidValue(ni.FieldValue) {
+			s = pretty.Sprint(ni.FieldValue.Interface())
+		}
+
+		fn, err := SchemaPaths(ni.StructField)
+		if err != nil {
+			errs = append(errs, err)
+		}
+		if l := len(fn); l != 1 {
+			errs = append(errs, fmt.Errorf("invalid schema path length %d for %v", l, ni.StructField.Name))
+		}
+
+		*outs += fmt.Sprintf("%s/%s : \n%s\n, ", ValueStr(ni.FieldKey.Interface()), fn[0][0], ValueStr(s))
+		return
+	}
+
 	basicStruct1 = BasicStruct{
 		Int32Field:     int32(42),
 		StringField:    "forty two",
@@ -1194,9 +1220,11 @@ func TestForEachDataField(t *testing.T) {
 		parentStruct interface{}
 		in           interface{}
 		out          interface{}
-		iterFunc     FieldIteratorFunc
-		wantOut      string
-		wantErr      string
+		// Only one of the below iterFunc must be specified.
+		iterFunc  FieldIteratorFunc
+		iterFunc2 FieldIteratorFunc2
+		wantOut   string
+		wantErr   string
 	}{
 		{
 			desc:         "nil",
@@ -1266,6 +1294,18 @@ func TestForEachDataField(t *testing.T) {
  StringField:    "forty three",
  Int32PtrField:  4343,
  StringPtrField: "forty three ptr"} (string)
+, `,
+		},
+		{
+			desc:         "map keys with no struct schema, skipping basicStruct2",
+			in:           nil,
+			parentStruct: &StructOfMapOfStructs{BasicStructMapField: map[string]BasicStruct{"basicStruct1": basicStruct1}, BasicStructPtrMapField: map[string]*BasicStruct{"basicStruct2": &basicStruct2}},
+			iterFunc2:    PrintMapKeysSchemaAnnotationFuncSkipBasicStruct2,
+			wantOut: `basicStruct1 (string)/basic-struct : 
+{Int32Field:     42,
+ StringField:    "forty two",
+ Int32PtrField:  4242,
+ StringPtrField: "forty two ptr"} (string)
 , `,
 		},
 		{
@@ -1281,20 +1321,34 @@ func TestForEachDataField(t *testing.T) {
 	}
 
 	for _, tt := range tests {
-		outStr := ""
-		var errs Errors = ForEachDataField(tt.parentStruct, tt.in, &outStr, tt.iterFunc)
-		if got, want := errs.String(), tt.wantErr; got != want {
-			diff, _ := testutil.GenerateUnifiedDiff(want, got)
-			t.Errorf("%s: ForEachDataField(%v, %#v, ...): \n%s", tt.desc, tt.parentStruct, tt.in, diff)
-		}
-		testErrLog(t, tt.desc, errs)
-		if len(errs) > 0 {
-			continue
-		}
-		if got, want := outStr, tt.wantOut; got != want {
-			diff, _ := testutil.GenerateUnifiedDiff(want, got)
-			t.Errorf("%s: ForEachDataField(%v, %#v, ...): \n%s", tt.desc, tt.parentStruct, tt.in, diff)
-		}
+		t.Run(tt.desc, func(t *testing.T) {
+			outStr := ""
+			test := func(funcName string, errs Errors) {
+				if got, want := errs.String(), tt.wantErr; got != want {
+					diff, _ := testutil.GenerateUnifiedDiff(want, got)
+					t.Errorf("%s: %s(%v, %#v, ...): \n%s", tt.desc, funcName, tt.parentStruct, tt.in, diff)
+				}
+				testErrLog(t, tt.desc, errs)
+				if len(errs) > 0 {
+					return
+				}
+				if got, want := outStr, tt.wantOut; got != want {
+					diff, _ := testutil.GenerateUnifiedDiff(want, got)
+					t.Errorf("%s: %s(%v, %#v, ...): \n%s", tt.desc, funcName, tt.parentStruct, tt.in, diff)
+				}
+				outStr = ""
+			}
+
+			switch {
+			case tt.iterFunc != nil && tt.iterFunc2 != nil:
+				t.Fatalf("Only one of iterFunc and iterFunc2 must be specified")
+			case tt.iterFunc2 != nil:
+				test("ForEachDataField2", ForEachDataField2(tt.parentStruct, tt.in, &outStr, tt.iterFunc2))
+			default:
+				test("ForEachDataField", ForEachDataField(tt.parentStruct, tt.in, &outStr, tt.iterFunc))
+				test("ForEachDataField2", ForEachDataField2(tt.parentStruct, tt.in, &outStr, iterFuncToIterFunc2(tt.iterFunc)))
+			}
+		})
 	}
 }
 
diff --git a/ygot/diff_exported_test.go b/ygot/diff_exported_test.go
new file mode 100644
index 0000000..186649b
--- /dev/null
+++ b/ygot/diff_exported_test.go
@@ -0,0 +1,658 @@
+// Copyright 2023 Google 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 ygot_test
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"github.com/openconfig/gnmi/errdiff"
+	"github.com/openconfig/ygot/integration_tests/schemaops/ctestschema"
+	"github.com/openconfig/ygot/internal/ytestutil"
+	"github.com/openconfig/ygot/testutil"
+	"github.com/openconfig/ygot/ygot"
+	"github.com/openconfig/ygot/ytypes"
+	"google.golang.org/protobuf/testing/protocmp"
+
+	gnmipb "github.com/openconfig/gnmi/proto/gnmi"
+)
+
+// mustPath returns a string as a gNMI path, causing a panic if the string
+// is invalid.
+func mustPath(s string) *gnmipb.Path {
+	p, err := ygot.StringToStructuredPath(s)
+	if err != nil {
+		panic(err)
+	}
+	return p
+}
+
+func TestDiffOrderedMap(t *testing.T) {
+	// TODO: Test for uncompressed structs.
+	tests := []struct {
+		name          string
+		inOrig, inMod *ctestschema.Device
+		inOpts        []ygot.DiffOpt
+		// skipTestUnmarshal determines whether the unmarshal test is skipped.
+		skipTestUnmarshal bool
+		// want is the expected output for Diff.
+		want *gnmipb.Notification
+		// wantAtomic and wantAtomic are the expected output for DiffWithAtomic.
+		wantAtomic          []*gnmipb.Notification
+		wantNonAtomic       *gnmipb.Notification
+		wantErrSubstrAtomic string
+	}{{
+		name: "no change",
+		inOrig: &ctestschema.Device{
+			OrderedList: ctestschema.GetOrderedMap(t),
+		},
+		inMod: &ctestschema.Device{
+			OrderedList: ctestschema.GetOrderedMap(t),
+		},
+		want:       &gnmipb.Notification{},
+		wantAtomic: nil,
+	}, {
+		name:   "empty",
+		inOrig: &ctestschema.Device{},
+		inMod:  &ctestschema.Device{},
+	}, {
+		name: "empty-and-nil",
+		inOrig: &ctestschema.Device{
+			OrderedList: &ctestschema.OrderedList_OrderedMap{},
+		},
+		inMod:             &ctestschema.Device{},
+		skipTestUnmarshal: true,
+	}, {
+		name:   "nested-ordered-map",
+		inOrig: &ctestschema.Device{},
+		inMod: &ctestschema.Device{
+			OrderedList: ctestschema.GetNestedOrderedMap(t),
+		},
+		want: &gnmipb.Notification{
+			Update: []*gnmipb.Update{{
+				Path: mustPath(`/ordered-lists/ordered-list[key=foo]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=foo]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=foo]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo-val"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=bar]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=bar]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=bar]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar-val"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=foo]/ordered-lists/ordered-list[key=foo]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=foo]/ordered-lists/ordered-list[key=foo]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=foo]/ordered-lists/ordered-list[key=foo]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo-val"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=foo]/ordered-lists/ordered-list[key=bar]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=foo]/ordered-lists/ordered-list[key=bar]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=foo]/ordered-lists/ordered-list[key=bar]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar-val"}},
+			}},
+		},
+		wantErrSubstrAtomic: "detected nested `ordered-by user` list, this is not supported",
+	}, {
+		name:   "empty-original-two-ordered-maps",
+		inOrig: &ctestschema.Device{},
+		inMod: &ctestschema.Device{
+			OrderedList:           ctestschema.GetOrderedMap(t),
+			OrderedMultikeyedList: ctestschema.GetOrderedMapMultikeyed(t),
+		},
+		want: &gnmipb.Notification{
+			Update: []*gnmipb.Update{{
+				Path: mustPath(`/ordered-lists/ordered-list[key=foo]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=foo]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=foo]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo-val"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=bar]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=bar]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=bar]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar-val"}},
+			}, {
+				Path: mustPath(`/ordered-multikeyed-lists/ordered-multikeyed-list[key1=foo][key2=42]/config/key1`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`/ordered-multikeyed-lists/ordered-multikeyed-list[key1=foo][key2=42]/key1`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`/ordered-multikeyed-lists/ordered-multikeyed-list[key1=foo][key2=42]/config/key2`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_UintVal{UintVal: 42}},
+			}, {
+				Path: mustPath(`/ordered-multikeyed-lists/ordered-multikeyed-list[key1=foo][key2=42]/key2`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_UintVal{UintVal: 42}},
+			}, {
+				Path: mustPath(`/ordered-multikeyed-lists/ordered-multikeyed-list[key1=foo][key2=42]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo-val"}},
+			}, {
+				Path: mustPath(`/ordered-multikeyed-lists/ordered-multikeyed-list[key1=bar][key2=42]/config/key1`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`/ordered-multikeyed-lists/ordered-multikeyed-list[key1=bar][key2=42]/key1`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`/ordered-multikeyed-lists/ordered-multikeyed-list[key1=bar][key2=42]/config/key2`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_UintVal{UintVal: 42}},
+			}, {
+				Path: mustPath(`/ordered-multikeyed-lists/ordered-multikeyed-list[key1=bar][key2=42]/key2`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_UintVal{UintVal: 42}},
+			}, {
+				Path: mustPath(`/ordered-multikeyed-lists/ordered-multikeyed-list[key1=bar][key2=42]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar-val"}},
+			}, {
+				Path: mustPath(`/ordered-multikeyed-lists/ordered-multikeyed-list[key1=baz][key2=84]/config/key1`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "baz"}},
+			}, {
+				Path: mustPath(`/ordered-multikeyed-lists/ordered-multikeyed-list[key1=baz][key2=84]/key1`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "baz"}},
+			}, {
+				Path: mustPath(`/ordered-multikeyed-lists/ordered-multikeyed-list[key1=baz][key2=84]/config/key2`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_UintVal{UintVal: 84}},
+			}, {
+				Path: mustPath(`/ordered-multikeyed-lists/ordered-multikeyed-list[key1=baz][key2=84]/key2`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_UintVal{UintVal: 84}},
+			}, {
+				Path: mustPath(`/ordered-multikeyed-lists/ordered-multikeyed-list[key1=baz][key2=84]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "baz-val"}},
+			}},
+		},
+		wantAtomic: []*gnmipb.Notification{{
+			Prefix: mustPath(`ordered-lists`),
+			Atomic: true,
+			Update: []*gnmipb.Update{{
+				Path: mustPath(`ordered-list[key=foo]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`ordered-list[key=foo]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`ordered-list[key=foo]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo-val"}},
+			}, {
+				Path: mustPath(`ordered-list[key=bar]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`ordered-list[key=bar]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`ordered-list[key=bar]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar-val"}},
+			}},
+		}, {
+			Prefix: mustPath(`ordered-multikeyed-lists`),
+			Atomic: true,
+			Update: []*gnmipb.Update{{
+				Path: mustPath(`ordered-multikeyed-list[key1=foo][key2=42]/config/key1`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`ordered-multikeyed-list[key1=foo][key2=42]/key1`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`ordered-multikeyed-list[key1=foo][key2=42]/config/key2`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_UintVal{UintVal: 42}},
+			}, {
+				Path: mustPath(`ordered-multikeyed-list[key1=foo][key2=42]/key2`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_UintVal{UintVal: 42}},
+			}, {
+				Path: mustPath(`ordered-multikeyed-list[key1=foo][key2=42]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo-val"}},
+			}, {
+				Path: mustPath(`ordered-multikeyed-list[key1=bar][key2=42]/config/key1`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`ordered-multikeyed-list[key1=bar][key2=42]/key1`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`ordered-multikeyed-list[key1=bar][key2=42]/config/key2`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_UintVal{UintVal: 42}},
+			}, {
+				Path: mustPath(`ordered-multikeyed-list[key1=bar][key2=42]/key2`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_UintVal{UintVal: 42}},
+			}, {
+				Path: mustPath(`ordered-multikeyed-list[key1=bar][key2=42]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar-val"}},
+			}, {
+				Path: mustPath(`ordered-multikeyed-list[key1=baz][key2=84]/config/key1`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "baz"}},
+			}, {
+				Path: mustPath(`ordered-multikeyed-list[key1=baz][key2=84]/key1`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "baz"}},
+			}, {
+				Path: mustPath(`ordered-multikeyed-list[key1=baz][key2=84]/config/key2`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_UintVal{UintVal: 84}},
+			}, {
+				Path: mustPath(`ordered-multikeyed-list[key1=baz][key2=84]/key2`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_UintVal{UintVal: 84}},
+			}, {
+				Path: mustPath(`ordered-multikeyed-list[key1=baz][key2=84]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "baz-val"}},
+			}},
+		}},
+	}, {
+		name: "empty-modified",
+		inOrig: &ctestschema.Device{
+			OrderedList: ctestschema.GetOrderedMap2(t),
+		},
+		inMod: &ctestschema.Device{},
+		want: &gnmipb.Notification{
+			Delete: []*gnmipb.Path{
+				mustPath(`/ordered-lists/ordered-list[key=wee]/config/key`),
+				mustPath(`/ordered-lists/ordered-list[key=wee]/key`),
+				mustPath(`/ordered-lists/ordered-list[key=wee]/config/value`),
+				mustPath(`/ordered-lists/ordered-list[key=woo]/config/key`),
+				mustPath(`/ordered-lists/ordered-list[key=woo]/key`),
+				mustPath(`/ordered-lists/ordered-list[key=woo]/config/value`),
+			},
+		},
+		wantNonAtomic: &gnmipb.Notification{
+			Delete: []*gnmipb.Path{
+				mustPath(`/ordered-lists`),
+			},
+		},
+	}, {
+		name: "empty-modified-with-other-data-in-orig",
+		inOrig: &ctestschema.Device{
+			OrderedList: ctestschema.GetOrderedMap2(t),
+			OtherData: &ctestschema.OtherData{
+				Motd: ygot.String("venus-is-hazy-today"),
+			},
+		},
+		inMod: &ctestschema.Device{},
+		want: &gnmipb.Notification{
+			Delete: []*gnmipb.Path{
+				mustPath(`/other-data/config/motd`),
+				mustPath(`/ordered-lists/ordered-list[key=wee]/config/key`),
+				mustPath(`/ordered-lists/ordered-list[key=wee]/key`),
+				mustPath(`/ordered-lists/ordered-list[key=wee]/config/value`),
+				mustPath(`/ordered-lists/ordered-list[key=woo]/config/key`),
+				mustPath(`/ordered-lists/ordered-list[key=woo]/key`),
+				mustPath(`/ordered-lists/ordered-list[key=woo]/config/value`),
+			},
+		},
+		wantNonAtomic: &gnmipb.Notification{
+			Delete: []*gnmipb.Path{
+				mustPath(`/other-data/config/motd`),
+				mustPath(`/ordered-lists`),
+			},
+		},
+	}, {
+		name: "empty-modified-with-other-data",
+		inOrig: &ctestschema.Device{
+			OrderedList: ctestschema.GetOrderedMap2(t),
+		},
+		inMod: &ctestschema.Device{
+			OtherData: &ctestschema.OtherData{
+				Motd: ygot.String("venus-is-hazy-today"),
+			},
+		},
+		want: &gnmipb.Notification{
+			Update: []*gnmipb.Update{{
+				Path: mustPath(`/other-data/config/motd`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "venus-is-hazy-today"}},
+			}},
+			Delete: []*gnmipb.Path{
+				mustPath(`/ordered-lists/ordered-list[key=wee]/config/key`),
+				mustPath(`/ordered-lists/ordered-list[key=wee]/key`),
+				mustPath(`/ordered-lists/ordered-list[key=wee]/config/value`),
+				mustPath(`/ordered-lists/ordered-list[key=woo]/config/key`),
+				mustPath(`/ordered-lists/ordered-list[key=woo]/key`),
+				mustPath(`/ordered-lists/ordered-list[key=woo]/config/value`),
+			},
+		},
+		wantNonAtomic: &gnmipb.Notification{
+			Update: []*gnmipb.Update{{
+				Path: mustPath(`/other-data/config/motd`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "venus-is-hazy-today"}},
+			}},
+			Delete: []*gnmipb.Path{
+				mustPath(`/ordered-lists`),
+			},
+		},
+	}, {
+		name: "disjoint-ordered-lists",
+		inOrig: &ctestschema.Device{
+			OrderedList: ctestschema.GetOrderedMap2(t),
+		},
+		inMod: &ctestschema.Device{
+			OrderedList: ctestschema.GetOrderedMap(t),
+		},
+		want: &gnmipb.Notification{
+			Update: []*gnmipb.Update{{
+				Path: mustPath(`/ordered-lists/ordered-list[key=foo]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=foo]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=foo]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo-val"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=bar]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=bar]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=bar]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar-val"}},
+			}},
+			Delete: []*gnmipb.Path{
+				mustPath(`/ordered-lists/ordered-list[key=wee]/config/key`),
+				mustPath(`/ordered-lists/ordered-list[key=wee]/key`),
+				mustPath(`/ordered-lists/ordered-list[key=wee]/config/value`),
+				mustPath(`/ordered-lists/ordered-list[key=woo]/config/key`),
+				mustPath(`/ordered-lists/ordered-list[key=woo]/key`),
+				mustPath(`/ordered-lists/ordered-list[key=woo]/config/value`),
+			},
+		},
+		wantAtomic: []*gnmipb.Notification{{
+			Prefix: mustPath(`ordered-lists`),
+			Atomic: true,
+			Update: []*gnmipb.Update{{
+				Path: mustPath(`ordered-list[key=foo]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`ordered-list[key=foo]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`ordered-list[key=foo]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo-val"}},
+			}, {
+				Path: mustPath(`ordered-list[key=bar]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`ordered-list[key=bar]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`ordered-list[key=bar]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar-val"}},
+			}},
+		}},
+	}, {
+		name: "disjoint-ordered-lists-with-ignore-additions",
+		inOrig: &ctestschema.Device{
+			OrderedList: ctestschema.GetOrderedMap2(t),
+		},
+		inMod: &ctestschema.Device{
+			OrderedList:           ctestschema.GetOrderedMap(t),
+			OrderedMultikeyedList: ctestschema.GetOrderedMapMultikeyed(t),
+		},
+		inOpts: []ygot.DiffOpt{&ygot.IgnoreAdditions{}},
+		want: &gnmipb.Notification{
+			Delete: []*gnmipb.Path{
+				mustPath(`/ordered-lists/ordered-list[key=wee]/config/key`),
+				mustPath(`/ordered-lists/ordered-list[key=wee]/key`),
+				mustPath(`/ordered-lists/ordered-list[key=wee]/config/value`),
+				mustPath(`/ordered-lists/ordered-list[key=woo]/config/key`),
+				mustPath(`/ordered-lists/ordered-list[key=woo]/key`),
+				mustPath(`/ordered-lists/ordered-list[key=woo]/config/value`),
+			},
+		},
+		wantAtomic: []*gnmipb.Notification{{
+			Prefix: mustPath(`ordered-lists`),
+			Atomic: true,
+			Update: []*gnmipb.Update{{
+				Path: mustPath(`ordered-list[key=foo]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`ordered-list[key=foo]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`ordered-list[key=foo]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo-val"}},
+			}, {
+				Path: mustPath(`ordered-list[key=bar]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`ordered-list[key=bar]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`ordered-list[key=bar]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar-val"}},
+			}},
+		}},
+		skipTestUnmarshal: true,
+	}, {
+		name: "modified-is-subset-of-original",
+		inOrig: &ctestschema.Device{
+			OrderedList: ctestschema.GetOrderedMap2(t),
+		},
+		inMod: &ctestschema.Device{
+			OrderedList: func() *ctestschema.OrderedList_OrderedMap {
+				om := ctestschema.GetOrderedMap2(t)
+				om.Delete("wee")
+				return om
+			}(),
+		},
+		want: &gnmipb.Notification{
+			Delete: []*gnmipb.Path{
+				mustPath(`/ordered-lists/ordered-list[key=wee]/config/key`),
+				mustPath(`/ordered-lists/ordered-list[key=wee]/key`),
+				mustPath(`/ordered-lists/ordered-list[key=wee]/config/value`),
+			},
+		},
+		wantAtomic: []*gnmipb.Notification{{
+			Prefix: mustPath(`ordered-lists`),
+			Atomic: true,
+			Update: []*gnmipb.Update{{
+				Path: mustPath(`ordered-list[key=woo]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "woo"}},
+			}, {
+				Path: mustPath(`ordered-list[key=woo]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "woo"}},
+			}, {
+				Path: mustPath(`ordered-list[key=woo]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "woo-val"}},
+			}},
+		}},
+	}, {
+		name: "modified-is-superset-of-original",
+		inOrig: &ctestschema.Device{
+			OrderedList: func() *ctestschema.OrderedList_OrderedMap {
+				om := ctestschema.GetOrderedMap2(t)
+				om.Delete("wee")
+				return om
+			}(),
+		},
+		inMod: &ctestschema.Device{
+			OrderedList: ctestschema.GetOrderedMap2(t),
+		},
+		want: &gnmipb.Notification{
+			Update: []*gnmipb.Update{{
+				Path: mustPath(`/ordered-lists/ordered-list[key=wee]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "wee"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=wee]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "wee"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=wee]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "wee-val"}},
+			}},
+		},
+		wantAtomic: []*gnmipb.Notification{{
+			Prefix: mustPath(`ordered-lists`),
+			Atomic: true,
+			Update: []*gnmipb.Update{{
+				Path: mustPath(`ordered-list[key=wee]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "wee"}},
+			}, {
+				Path: mustPath(`ordered-list[key=wee]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "wee"}},
+			}, {
+				Path: mustPath(`ordered-list[key=wee]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "wee-val"}},
+			}, {
+				Path: mustPath(`ordered-list[key=woo]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "woo"}},
+			}, {
+				Path: mustPath(`ordered-list[key=woo]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "woo"}},
+			}, {
+				Path: mustPath(`ordered-list[key=woo]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "woo-val"}},
+			}},
+		}},
+	}, {
+		name: "modified-overlaps-original",
+		inOrig: &ctestschema.Device{
+			OrderedList: func() *ctestschema.OrderedList_OrderedMap {
+				om := ctestschema.GetOrderedMapLonger(t)
+				om.Delete("baz")
+				return om
+			}(),
+		},
+		inMod: &ctestschema.Device{
+			OrderedList: func() *ctestschema.OrderedList_OrderedMap {
+				om := ctestschema.GetOrderedMapLonger(t)
+				om.Delete("bar")
+				return om
+			}(),
+		},
+		want: &gnmipb.Notification{
+			Update: []*gnmipb.Update{{
+				Path: mustPath(`/ordered-lists/ordered-list[key=baz]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "baz"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=baz]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "baz"}},
+			}, {
+				Path: mustPath(`/ordered-lists/ordered-list[key=baz]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "baz-val"}},
+			}},
+			Delete: []*gnmipb.Path{
+				mustPath(`/ordered-lists/ordered-list[key=bar]/config/key`),
+				mustPath(`/ordered-lists/ordered-list[key=bar]/key`),
+				mustPath(`/ordered-lists/ordered-list[key=bar]/config/value`),
+			},
+		},
+		wantAtomic: []*gnmipb.Notification{{
+			Prefix: mustPath(`ordered-lists`),
+			Atomic: true,
+			Update: []*gnmipb.Update{{
+				Path: mustPath(`ordered-list[key=foo]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`ordered-list[key=foo]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`ordered-list[key=foo]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo-val"}},
+			}, {
+				Path: mustPath(`ordered-list[key=baz]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "baz"}},
+			}, {
+				Path: mustPath(`ordered-list[key=baz]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "baz"}},
+			}, {
+				Path: mustPath(`ordered-list[key=baz]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "baz-val"}},
+			}},
+		}},
+	}}
+
+	for _, tt := range tests {
+		t.Run("Diff/"+tt.name, func(t *testing.T) {
+			got, err := ygot.Diff(tt.inOrig, tt.inMod, tt.inOpts...)
+			if err != nil {
+				t.Errorf("Diff: got unexpected error: %v", err)
+				return
+			}
+
+			if err != nil {
+				return
+			}
+			// To re-use the NotificationSetEqual helper, we put the want and got into
+			// a slice.
+			if !testutil.NotificationSetEqual([]*gnmipb.Notification{got}, []*gnmipb.Notification{tt.want}) {
+				diff := cmp.Diff(got, tt.want, protocmp.Transform())
+				t.Errorf("Diff: did not get expected Notification, diff(-got,+want):\n%s", diff)
+			}
+		})
+
+		t.Run("DiffWithAtomic/"+tt.name, func(t *testing.T) {
+			got, err := ygot.DiffWithAtomic(tt.inOrig, tt.inMod, tt.inOpts...)
+			if diff := errdiff.Substring(err, tt.wantErrSubstrAtomic); diff != "" {
+				t.Errorf("DiffWithAtomic: did not get expected error status, got: %s, want: %s", err, tt.wantErrSubstrAtomic)
+				return
+			}
+
+			if tt.wantErrSubstrAtomic != "" {
+				return
+			}
+
+			var gotNonAtomic *gnmipb.Notification
+			gotAtomic := got
+			if tt.wantNonAtomic != nil && len(got) > 0 {
+				gotNonAtomic = got[len(got)-1]
+				gotAtomic = got[:len(got)-1]
+				if len(gotAtomic) == 0 {
+					gotAtomic = nil
+				}
+			}
+
+			if diff := cmp.Diff(gotAtomic, tt.wantAtomic, cmpopts.SortSlices(testutil.NotificationLess), protocmp.Transform()); diff != "" {
+				t.Errorf("telemetry-atomic values of DiffWithAtomic: did not get expected Notification, diff(-got,+want):%s\n", diff)
+			}
+			// Avoid test flakiness by ignoring the update ordering. Required because
+			// there is no order to the map of fields that are returned by the struct
+			// output.
+			if diff := cmp.Diff(gotNonAtomic, tt.wantNonAtomic, testutil.NotificationComparer()); diff != "" {
+				t.Errorf("non-telemetry-atomic values of DiffWithAtomic: did not get expected Notification, diff(-got,+want):%s\n", diff)
+			}
+
+			if tt.skipTestUnmarshal {
+				return
+			}
+			// Test that unmarshalling into original gets back to modified.
+			schema, err := ctestschema.Schema()
+			if err != nil {
+				t.Fatal(err)
+			}
+			schema.Root = tt.inOrig
+			if err := ytypes.UnmarshalNotifications(schema, got); err != nil {
+				t.Fatal(err)
+			}
+			if diff := cmp.Diff(tt.inOrig, tt.inMod, ytestutil.OrderedMapCmpOptions...); diff != "" {
+				t.Errorf("Unmarshal diff into orig (-got, +want):\n%s", diff)
+			}
+		})
+	}
+}
diff --git a/ygot/diff_test.go b/ygot/diff_test.go
index 4b9eeca..70c7b96 100644
--- a/ygot/diff_test.go
+++ b/ygot/diff_test.go
@@ -627,7 +627,7 @@ func TestFindSetLeaves(t *testing.T) {
 	}}
 
 	for _, tt := range tests {
-		got, err := findSetLeaves(tt.inStruct, tt.inOpts...)
+		got, err := findSetLeaves(tt.inStruct, false, tt.inOpts...)
 		if err != nil && (err.Error() != tt.wantErr) {
 			t.Errorf("%s: findSetLeaves(%v): did not get expected error: %v", tt.desc, tt.inStruct, err)
 			continue
@@ -1556,21 +1556,40 @@ func TestDiff(t *testing.T) {
 	}}
 
 	for _, tt := range tests {
-		got, err := Diff(tt.inOrig, tt.inMod, tt.inOpts...)
-		if diff := errdiff.Substring(err, tt.wantErrSubStr); diff != "" {
-			t.Errorf("%s: Diff(%s, %s): did not get expected error status, got: %s, want: %s", tt.desc, pretty.Sprint(tt.inOrig), pretty.Sprint(tt.inMod), err, tt.wantErrSubStr)
-			continue
-		}
+		testDiffSingleNotif := func(t *testing.T, funcName string, got *gnmipb.Notification, err error) {
+			if diff := errdiff.Substring(err, tt.wantErrSubStr); diff != "" {
+				t.Errorf("%s: %s(%s, %s): did not get expected error status, got: %s, want: %s", tt.desc, funcName, pretty.Sprint(tt.inOrig), pretty.Sprint(tt.inMod), err, tt.wantErrSubStr)
+				return
+			}
 
-		if tt.wantErrSubStr != "" {
-			continue
-		}
-		// To re-use the NotificationSetEqual helper, we put the want and got into
-		// a slice.
-		if !testutil.NotificationSetEqual([]*gnmipb.Notification{tt.want}, []*gnmipb.Notification{got}) {
-			diff := cmp.Diff(got, tt.want, protocmp.Transform())
-			t.Errorf("%s: Diff(%s, %s): did not get expected Notification, diff(-got,+want):\n%s", tt.desc, pretty.Sprint(tt.inOrig), pretty.Sprint(tt.inMod), diff)
+			if tt.wantErrSubStr != "" {
+				return
+			}
+			// To re-use the NotificationSetEqual helper, we put the want and got into
+			// a slice.
+			if !testutil.NotificationSetEqual([]*gnmipb.Notification{tt.want}, []*gnmipb.Notification{got}) {
+				diff := cmp.Diff(got, tt.want, protocmp.Transform())
+				t.Errorf("%s: Diff(%s, %s): did not get expected Notification, diff(-got,+want):\n%s", tt.desc, pretty.Sprint(tt.inOrig), pretty.Sprint(tt.inMod), diff)
+			}
 		}
+
+		t.Run(tt.desc+"Diff", func(t *testing.T) {
+			got, err := Diff(tt.inOrig, tt.inMod, tt.inOpts...)
+			testDiffSingleNotif(t, "Diff", got, err)
+		})
+		t.Run(tt.desc+"DiffWithAtomic", func(t *testing.T) {
+			var got *gnmipb.Notification
+			gots, err := DiffWithAtomic(tt.inOrig, tt.inMod, tt.inOpts...)
+			switch len(gots) {
+			case 0:
+				got = nil
+			case 1:
+				got = gots[0]
+			default:
+				t.Fatalf("Got multiple Notifications for DiffWithAtomic: %d, this is not expected.", len(gots))
+			}
+			testDiffSingleNotif(t, "DiffWithAtomic", got, err)
+		})
 	}
 }
 
diff --git a/ygot/render_test.go b/ygot/render_test.go
index 314cb53..39f75f9 100644
--- a/ygot/render_test.go
+++ b/ygot/render_test.go
@@ -1054,15 +1054,13 @@ func TestTogNMINotifications(t *testing.T) {
 	}
 
 	tests := []struct {
-		name        string
-		inTimestamp int64
-		inStruct    GoStruct
-		inConfig    GNMINotificationsConfig
-		// wantAtomicCutoff represents the index one-past the one at
-		// which atomic notifications end.
-		wantAtomicCutoff uint
-		want             []*gnmipb.Notification
-		wantErr          bool
+		name           string
+		inTimestamp    int64
+		inStruct       GoStruct
+		inConfig       GNMINotificationsConfig
+		wantAtomicMsgs int
+		want           []*gnmipb.Notification
+		wantErr        bool
 	}{{
 		name:        "simple single leaf example",
 		inTimestamp: 42,
@@ -1385,8 +1383,18 @@ func TestTogNMINotifications(t *testing.T) {
 			UsePathElem:    true,
 			PathElemPrefix: mustPathElem("heart/of/gold"),
 		},
-		wantAtomicCutoff: 2,
+		wantAtomicMsgs: 2,
 		want: []*gnmipb.Notification{{
+			Timestamp: 42,
+			Prefix:    mustPath("heart/of/gold"),
+			Update: []*gnmipb.Update{{
+				Path: mustPath(`child/config/field-one`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "abc -> def"}},
+			}, {
+				Path: mustPath(`child/config/field-two`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_UintVal{UintVal: 42}},
+			}},
+		}, {
 			Timestamp: 42,
 			Atomic:    true,
 			Prefix:    mustPath("heart/of/gold/ordered-lists"),
@@ -1432,15 +1440,40 @@ func TestTogNMINotifications(t *testing.T) {
 				Path: mustPath(`ordered-list[key=woo]/config/value`),
 				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "woo-val"}},
 			}},
-		}, {
+		}},
+	}, {
+		name:        "struct with only an ordered list difference",
+		inTimestamp: 42,
+		inStruct: &mapStructTestOne{
+			OrderedList: getOrderedMap(),
+		},
+		inConfig: GNMINotificationsConfig{
+			UsePathElem:    true,
+			PathElemPrefix: mustPathElem("heart/of/gold"),
+		},
+		wantAtomicMsgs: 1,
+		want: []*gnmipb.Notification{{
 			Timestamp: 42,
-			Prefix:    mustPath("heart/of/gold"),
+			Atomic:    true,
+			Prefix:    mustPath("heart/of/gold/ordered-lists"),
 			Update: []*gnmipb.Update{{
-				Path: mustPath(`child/config/field-one`),
-				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "abc -> def"}},
+				Path: mustPath(`ordered-list[key=foo]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
 			}, {
-				Path: mustPath(`child/config/field-two`),
-				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_UintVal{UintVal: 42}},
+				Path: mustPath(`ordered-list[key=foo]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo"}},
+			}, {
+				Path: mustPath(`ordered-list[key=foo]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "foo-val"}},
+			}, {
+				Path: mustPath(`ordered-list[key=bar]/config/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`ordered-list[key=bar]/key`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar"}},
+			}, {
+				Path: mustPath(`ordered-list[key=bar]/config/value`),
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar-val"}},
 			}},
 		}},
 	}, {
@@ -1456,8 +1489,18 @@ func TestTogNMINotifications(t *testing.T) {
 		inConfig: GNMINotificationsConfig{
 			StringSlicePrefix: []string{"heart", "of", "gold"},
 		},
-		wantAtomicCutoff: 1,
+		wantAtomicMsgs: 1,
 		want: []*gnmipb.Notification{{
+			Timestamp: 42,
+			Prefix:    &gnmipb.Path{Element: []string{"heart", "of", "gold"}},
+			Update: []*gnmipb.Update{{
+				Path: &gnmipb.Path{Element: []string{"child", "config", "field-one"}},
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "abc -> def"}},
+			}, {
+				Path: &gnmipb.Path{Element: []string{"child", "config", "field-two"}},
+				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_UintVal{UintVal: 42}},
+			}},
+		}, {
 			Timestamp: 42,
 			Atomic:    true,
 			Prefix:    &gnmipb.Path{Element: []string{"heart", "of", "gold", "ordered-lists"}},
@@ -1480,16 +1523,6 @@ func TestTogNMINotifications(t *testing.T) {
 				Path: &gnmipb.Path{Element: []string{"ordered-list", "bar", "config", "value"}},
 				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "bar-val"}},
 			}},
-		}, {
-			Timestamp: 42,
-			Prefix:    &gnmipb.Path{Element: []string{"heart", "of", "gold"}},
-			Update: []*gnmipb.Update{{
-				Path: &gnmipb.Path{Element: []string{"child", "config", "field-one"}},
-				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_StringVal{StringVal: "abc -> def"}},
-			}, {
-				Path: &gnmipb.Path{Element: []string{"child", "config", "field-two"}},
-				Val:  &gnmipb.TypedValue{Value: &gnmipb.TypedValue_UintVal{UintVal: 42}},
-			}},
 		}},
 	}, {
 		name:        "struct with nested ordered list",
@@ -1505,8 +1538,8 @@ func TestTogNMINotifications(t *testing.T) {
 			UsePathElem:    true,
 			PathElemPrefix: mustPathElem("heart/of/gold"),
 		},
-		wantAtomicCutoff: 1,
-		wantErr:          true,
+		wantAtomicMsgs: 1,
+		wantErr:        true,
 	}, {
 		name:        "struct with list",
 		inTimestamp: 42,
@@ -1708,24 +1741,31 @@ func TestTogNMINotifications(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			got, err := TogNMINotifications(tt.inStruct, tt.inTimestamp, tt.inConfig)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("%s: TogNMINotifications(%v, %v, %v): got unexpected error: %v", tt.name, tt.inStruct, tt.inTimestamp, tt.inConfig, err)
+			}
 			if err != nil {
-				if !tt.wantErr {
-					t.Errorf("%s: TogNMINotifications(%v, %v, %v): got unexpected error: %v", tt.name, tt.inStruct, tt.inTimestamp, tt.inConfig, err)
-				}
 				return
 			}
 
-			if diff := cmp.Diff(got[:tt.wantAtomicCutoff], tt.want[:tt.wantAtomicCutoff], cmpopts.SortSlices(testutil.NotificationLess), protocmp.Transform()); diff != "" {
-				t.Errorf("%s: telemetry-atomic values of TogNMINotifications(%v, %v): did not get expected Notification, diff(-got,+want):%s\n", tt.name, tt.inStruct, tt.inTimestamp, diff)
+			if gotLen, wantLen := len(got), len(tt.want); gotLen != wantLen {
+				t.Errorf("gotLen: %d, wantLen: %d", gotLen, wantLen)
+				if diff := cmp.Diff(got, tt.want, cmpopts.SortSlices(testutil.NotificationLess), testutil.NotificationComparer()); diff != "" {
+					t.Errorf("%s: telemetry-atomic values of TogNMINotifications(%v, %v): did not get expected Notification, diff(-got,+want):%s\n", tt.name, tt.inStruct, tt.inTimestamp, diff)
+				}
+				return
 			}
 
 			// Avoid test flakiness by ignoring the update ordering. Required because
 			// there is no order to the map of fields that are returned by the struct
 			// output.
-
-			if diff := cmp.Diff(got[tt.wantAtomicCutoff:], tt.want[tt.wantAtomicCutoff:], testutil.NotificationComparer(), cmpopts.SortSlices(testutil.NotificationLess)); diff != "" {
+			if diff := cmp.Diff(got[:len(got)-tt.wantAtomicMsgs], tt.want[:len(got)-tt.wantAtomicMsgs], cmpopts.SortSlices(testutil.NotificationLess), testutil.NotificationComparer()); diff != "" {
 				t.Errorf("%s: non-telemetry-atomic values of TogNMINotifications(%v, %v): did not get expected Notification, diff(-got,+want):%s\n", tt.name, tt.inStruct, tt.inTimestamp, diff)
 			}
+
+			if diff := cmp.Diff(got[len(got)-tt.wantAtomicMsgs:], tt.want[len(got)-tt.wantAtomicMsgs:], cmpopts.SortSlices(testutil.NotificationLess), protocmp.Transform()); diff != "" {
+				t.Errorf("%s: telemetry-atomic values of TogNMINotifications(%v, %v): did not get expected Notification, diff(-got,+want):%s\n", tt.name, tt.inStruct, tt.inTimestamp, diff)
+			}
 		})
 	}
 }