-
Notifications
You must be signed in to change notification settings - Fork 105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add leafref data tree validation. #117
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First review: I didn't yet review any of the test code, since I wanted to get to the end of this before doing so.
@@ -44,3 +46,42 @@ func stringMapSetToSlice(m map[string]interface{}) []string { | |||
} | |||
return out | |||
} | |||
|
|||
// TODO(mostrowski): move below functions into path package. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Today, path functions are in ygot
. Should we create a paths package specifically?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created #119
return false | ||
} | ||
for i := range prefix { | ||
if prefix[i] != path.GetElem()[i].GetName() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason to prefer GetElem()
over just straight Elem
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's just convention for reading vs. writing. In C++ it returns a const value, in Go it populates unset fields with the default.
util/common.go
Outdated
return path | ||
} | ||
return &gpb.Path{ | ||
Origin: path.GetOrigin(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably semantics, but origin
becomes meaningless when it isn't a root-level element. It's specifically used to specify different overlapping trees such that say a server can support /ietf-interfaces/interfaces
and /openconfig-interfaces:interfaces
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm assuming this means I should remove the Origin field?
util/reflect.go
Outdated
|
||
// ForEachField recursively iterates through the fields of value (which may be | ||
// any Go type) and executes iterFunction on each field. | ||
// any Go type) and executes iterFunction on each field. Any nil fields | ||
// (including value) are traversed in the schema tree only. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If value, or any of its descendents, are nil, they are examined in the schema provided
? It might also be worth clarifying here why we need to look at values that are nil in the data tree in the schema tree. I think that this is because we want to ensure that there isn't something there that is a nil leafref, but it's not immediately clear in other scenarios why one wants to walk these entities.
In the case of building the lookup tree, we maybe don't want to define schema nodes that don't exist in the resulting tree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the comment. Do you mean the data nodes that don't exist? (the schema nodes do exist as they are being traversed)
Data nodes are left nil in any part of the tree that exists in the schema only.
util/reflect.go
Outdated
@@ -367,61 +413,368 @@ type NodeInfo struct { | |||
// in, out are passed through from the caller to the iteration and can be used | |||
// to pass state in and out. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any expectation as to what in
and out
are? I assume not, because they are only handled within FieldIteratorFunc
right? It might be worth specifying here that it is safe for in
and out
to be type-asserted since we do not expect that there can ever be a case that they are set by anything else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
return matchNodes, matchSchemas, nil | ||
} | ||
|
||
// pathStructTagKey returns the string label of the struct field sf when it is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only true when compression is enabled -- this code is robust to both cases, but the comment isn't entirely true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If path compression not enabled then I think the element that would've been compressed (e.g. config/name) cannot be part of the key anymore because it's now a container or list. In that case pathToSchema would just return "name" (if that is part of the key). I may be missing your point - let's talk about it live.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, yes this was a nit that I was just pointing out. Feel free to ignore.
util/schema.go
Outdated
} | ||
|
||
// SchemaPaths returns all the paths in the path tag. | ||
func SchemaPaths(f reflect.StructField) ([][]string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have multiple implementations of this function. I wonder whether we can make them common. We should create an issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return nil | ||
} | ||
|
||
// FindFirstNonChoiceOrCase recursively traverses the schema tree and returns a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto, now this is public, we have multiple implementations of this function (the other is in ygen
), let's create an issue/TODO to fix that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Grouped in comment with the same issue since these are related.
util/schema.go
Outdated
if schema == nil { | ||
return nil, nil | ||
} | ||
// TODO(mostrowski): this should only be possible in fakeroot. Add an |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is already an annotation in the schema for this, see here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
ytypes/util_schema.go
Outdated
@@ -228,6 +216,21 @@ func childSchema(schema *yang.Entry, f reflect.StructField) (*yang.Entry, error) | |||
return nil, nil | |||
} | |||
|
|||
// findFirstNonChoiceOrCase recursively traverses the schema tree and populates |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you have two implementations of this even in this code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@ostromart (not sure whether you meant to change to @maoghub :-)) -- is this ready for me to review the test code? Did we say we'd fix the coverage % in a subsequent CL? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ostromart Reviewed the test code here -- few suggestions and questions.
Key : "keyval1" [key (leaf)] | ||
LeafField : 42 [leaf-field (leaf)] | ||
` | ||
if got != want { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using pretty.Compare
or similar to generate a diff here? Seems like it might be easier to spot what the diff is in the case that the tests fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For small protos I find dumping out the whole thing makes the diffs easier to spot. I agree though once it's more than a dozen lines or so, pretty's the way to go.
util/reflect_test.go
Outdated
@@ -771,31 +849,64 @@ func TestForEachField(t *testing.T) { | |||
} | |||
|
|||
func TestUpdateFieldUsingForEachField(t *testing.T) { | |||
containerSchema := &yang.Entry{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this schema be made general for the file such that each test function doesn't need to define the same schema and the associated types that represent its generated code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. I've put it above the tests to keep some locality, otherwise the tests become very hard to understand.
util/reflect_test.go
Outdated
StructKeyList map[string]*ListElemStruct1 `path:"config/simple-key-list"` | ||
} | ||
|
||
func (*InnerContainerType1) IsYANGGoStruct() {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think conventionally these should follow the definition of the type - rather than be in a block like this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.desc, func(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For my education -- it seems like here you could run the anonymous function here directly within the test - with the only difference that it wouldn't run in its own goroutine. Is there another motivating factor for using t.Run
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The short answer is so we can run subtests by name.
The long answer is in this video (worth a look if you have time): https://www.youtube.com/watch?v=8hQG7QlcLBk&index=12&list=PL2ntRZ1ySWBdD9bru6IR-_WXUgJqvrtx9
util/reflect_test.go
Outdated
StructKeyList map[KeyStruct2]*ListElemStruct2 `path:"struct-key-list"` | ||
} | ||
|
||
func (*InnerContainerType2) IsYANGGoStruct() {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment re: ordering of these functions with their receiver type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Kind: yang.LeafEntry, | ||
Type: &yang.YangType{ | ||
Kind: yang.Yleafref, | ||
Path: "../../config/inner/leaf-field", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider also testing absolute paths here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Kind: yang.Yleafref, | ||
Path: "../../list[key = current()/../../key]/int32", | ||
}, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding test cases for:
leaf-list
containingleafref
.typedef
that defines aleafref
.- (for Y1.1)
union
that defines aleafref
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created #124
ytypes/leafref_test.go
Outdated
wantErr: `pointed-to value with path ../../list[key = current()/../../key]/int32 from field LeafRefToList value 43 (int32 ptr) schema /int32-ref-to-list is empty set`, | ||
}, | ||
} | ||
for _, tt := range tests { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Linebreak here (nit).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
ytypes/leafref_test.go
Outdated
}, | ||
{ | ||
desc: "path", | ||
in: "b[key = ../a/b/c]", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unquoted keys aren't valid XPATH - so wouldn't ever be seen here, IMHO, this should return an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
}, | ||
{ | ||
"config": { | ||
"name": "METRO-AR" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please anonymise these policy names a bit :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Apologies for the delay. LGTM. Let's fix the coverage so we get our green logo back! |
Previously, leafref validation simply validated that the referring node conformed to the schema at the leafref path, without attempting to compare with the referred-to value.
This change adds a data tree verification at the root node that compares the actual leafref values.
To accomplish this, a mirror data tree is built from the existing gostruct tree with additional information at each node to allow traversing back up to the parent, which is not possible with gostructs alone and which is required to follow relative leafref paths.
Then, for each leafref encountered in the traversal, the new tree is passed in which allows traversal back up to the root of the leafref. From this point, a new function traverses down the data tree and returns all data nodes that match the specified key (which selects the entire list when empty).
Currently, while the node selection components supports partial keys, the leafref path parsing does not. Therefore, leafref paths like "current()/../a[k1 = ../path1 and k2 = ../path2]" will lead to a parse error.