Closed vardius closed 4 years ago
If you want to know if you should look for other node, because current one was created with a middleware only, I believe assigning custom route is not a solution. Route is a route, and should only be a route, if you can't use route as a handler then it is invalid implementation. Everything should be decoupled, and have only one responsibility.
Lets summarise the state before your commit. There are 2 issues:
For 2 it is an easy fix, for 1 I believe we should panic and let user handle this in the handler or with use of subrouter. Maybe we could do something like you did. But with proper implementation. Instead of using Route to flag nodes that are empty middleware nodes, we could just create a proper struct boolean property. Simply a flag. And then in the Tree.Match loop instead of checking if route is not nil, we would check node.ourFlag. This could work, but we need to think what happens when user first adds middleware, we flag that route and then he adds a handler. We need to make sure flag is removed.
I reverted your commit, and pushed fix for mentioned issue number 2. Issue number 2 still needs to be fixed. Might be done following your idea, with correct implementation which would be just adding flag to node as I mentioned earlier.
@mar1n3r0 please review this pull request, let me know your thoughts and I believe we can merge it.
Also if there is anything that you did not know before working on this router, and you had to learn/understand how it works yourself, could you summarise it and maybe we could update docs ? Maybe there still is something that you would like me to explain ? That we could also include in the docs.
➜ gorouter git:(master) ✗ go test -bench=. -cpu=4 -benchmem
test
goos: darwin
goarch: amd64
pkg: github.com/vardius/gorouter/v4
BenchmarkStatic1-4 35997880 27.9 ns/op 0 B/op 0 allocs/op
BenchmarkStatic2-4 33372828 37.7 ns/op 0 B/op 0 allocs/op
BenchmarkStatic3-4 28278296 43.6 ns/op 0 B/op 0 allocs/op
BenchmarkStatic5-4 18393667 57.0 ns/op 0 B/op 0 allocs/op
BenchmarkStatic10-4 13458920 95.3 ns/op 0 B/op 0 allocs/op
BenchmarkStatic20-4 7444556 158 ns/op 0 B/op 0 allocs/op
BenchmarkWildcard1-4 5017465 225 ns/op 496 B/op 5 allocs/op
BenchmarkWildcard2-4 4917728 300 ns/op 528 B/op 5 allocs/op
BenchmarkWildcard3-4 4460277 279 ns/op 560 B/op 5 allocs/op
BenchmarkWildcard5-4 4117964 361 ns/op 624 B/op 5 allocs/op
BenchmarkWildcard10-4 2757624 405 ns/op 784 B/op 5 allocs/op
BenchmarkWildcard20-4 1877768 562 ns/op 1104 B/op 5 allocs/op
BenchmarkRegexp1-4 3372494 353 ns/op 500 B/op 5 allocs/op
BenchmarkRegexp2-4 2474242 501 ns/op 532 B/op 5 allocs/op
BenchmarkRegexp3-4 1929957 663 ns/op 564 B/op 5 allocs/op
BenchmarkRegexp5-4 1332510 933 ns/op 629 B/op 5 allocs/op
BenchmarkRegexp10-4 617763 2248 ns/op 791 B/op 5 allocs/op
BenchmarkRegexp20-4 366548 3350 ns/op 1113 B/op 5 allocs/op
BenchmarkFastHTTPStatic1-4 29598078 35.3 ns/op 2 B/op 1 allocs/op
BenchmarkFastHTTPStatic2-4 27048343 45.7 ns/op 4 B/op 1 allocs/op
BenchmarkFastHTTPStatic3-4 20608680 76.4 ns/op 8 B/op 1 allocs/op
BenchmarkFastHTTPStatic5-4 15883792 72.0 ns/op 16 B/op 1 allocs/op
BenchmarkFastHTTPStatic10-4 11401267 124 ns/op 32 B/op 1 allocs/op
BenchmarkFastHTTPStatic20-4 5862892 217 ns/op 48 B/op 1 allocs/op
BenchmarkFastHTTPWildcard1-4 11145861 98.8 ns/op 66 B/op 3 allocs/op
BenchmarkFastHTTPWildcard2-4 9085635 178 ns/op 100 B/op 3 allocs/op
BenchmarkFastHTTPWildcard3-4 6847353 172 ns/op 136 B/op 3 allocs/op
BenchmarkFastHTTPWildcard5-4 6322854 185 ns/op 208 B/op 3 allocs/op
BenchmarkFastHTTPWildcard10-4 4685559 308 ns/op 384 B/op 3 allocs/op
BenchmarkFastHTTPWildcard20-4 2442094 786 ns/op 720 B/op 3 allocs/op
BenchmarkFastHTTPRegexp1-4 4234552 254 ns/op 69 B/op 3 allocs/op
BenchmarkFastHTTPRegexp2-4 2538032 486 ns/op 113 B/op 3 allocs/op
BenchmarkFastHTTPRegexp3-4 1806182 589 ns/op 145 B/op 3 allocs/op
BenchmarkFastHTTPRegexp5-4 1000000 1078 ns/op 226 B/op 3 allocs/op
BenchmarkFastHTTPRegexp10-4 740905 2075 ns/op 419 B/op 3 allocs/op
BenchmarkFastHTTPRegexp20-4 335192 3066 ns/op 791 B/op 3 allocs/op
PASS
ok github.com/vardius/gorouter/v4 56.036s
➜ gorouter git:(hotfix/middleware-by-path) go test -bench=. -cpu=4 -benchmem
test
goos: darwin
goarch: amd64
pkg: github.com/vardius/gorouter/v4
BenchmarkStatic1-4 33909061 34.3 ns/op 0 B/op 0 allocs/op
BenchmarkStatic2-4 25284765 47.5 ns/op 0 B/op 0 allocs/op
BenchmarkStatic3-4 21225134 54.6 ns/op 0 B/op 0 allocs/op
BenchmarkStatic5-4 14245224 72.2 ns/op 0 B/op 0 allocs/op
BenchmarkStatic10-4 9515647 127 ns/op 0 B/op 0 allocs/op
BenchmarkStatic20-4 5496370 218 ns/op 0 B/op 0 allocs/op
BenchmarkWildcard1-4 4730401 227 ns/op 496 B/op 5 allocs/op
BenchmarkWildcard2-4 4525063 261 ns/op 528 B/op 5 allocs/op
BenchmarkWildcard3-4 4346812 283 ns/op 560 B/op 5 allocs/op
BenchmarkWildcard5-4 3658440 318 ns/op 624 B/op 5 allocs/op
BenchmarkWildcard10-4 2758533 411 ns/op 784 B/op 5 allocs/op
BenchmarkWildcard20-4 1871103 639 ns/op 1104 B/op 5 allocs/op
BenchmarkRegexp1-4 3286542 362 ns/op 501 B/op 5 allocs/op
BenchmarkRegexp2-4 2416214 508 ns/op 533 B/op 5 allocs/op
BenchmarkRegexp3-4 1846933 687 ns/op 565 B/op 5 allocs/op
BenchmarkRegexp5-4 1342232 911 ns/op 630 B/op 5 allocs/op
BenchmarkRegexp10-4 673621 1559 ns/op 790 B/op 5 allocs/op
BenchmarkRegexp20-4 377835 2923 ns/op 1113 B/op 5 allocs/op
BenchmarkFastHTTPStatic1-4 25817391 47.7 ns/op 2 B/op 1 allocs/op
BenchmarkFastHTTPStatic2-4 21749991 55.9 ns/op 4 B/op 1 allocs/op
BenchmarkFastHTTPStatic3-4 17201960 69.5 ns/op 8 B/op 1 allocs/op
BenchmarkFastHTTPStatic5-4 13123863 88.9 ns/op 16 B/op 1 allocs/op
BenchmarkFastHTTPStatic10-4 8274832 147 ns/op 32 B/op 1 allocs/op
BenchmarkFastHTTPStatic20-4 4877264 249 ns/op 48 B/op 1 allocs/op
BenchmarkFastHTTPWildcard1-4 10780489 106 ns/op 66 B/op 3 allocs/op
BenchmarkFastHTTPWildcard2-4 8331529 145 ns/op 100 B/op 3 allocs/op
BenchmarkFastHTTPWildcard3-4 7211712 245 ns/op 136 B/op 3 allocs/op
BenchmarkFastHTTPWildcard5-4 3829846 289 ns/op 208 B/op 3 allocs/op
BenchmarkFastHTTPWildcard10-4 3918559 360 ns/op 384 B/op 3 allocs/op
BenchmarkFastHTTPWildcard20-4 1796622 583 ns/op 720 B/op 3 allocs/op
BenchmarkFastHTTPRegexp1-4 4201428 267 ns/op 70 B/op 3 allocs/op
BenchmarkFastHTTPRegexp2-4 3115568 429 ns/op 113 B/op 3 allocs/op
BenchmarkFastHTTPRegexp3-4 2258680 521 ns/op 145 B/op 3 allocs/op
BenchmarkFastHTTPRegexp5-4 1577082 785 ns/op 225 B/op 3 allocs/op
BenchmarkFastHTTPRegexp10-4 726134 1620 ns/op 419 B/op 3 allocs/op
BenchmarkFastHTTPRegexp20-4 343843 2915 ns/op 791 B/op 3 allocs/op
PASS
ok github.com/vardius/gorouter/v4 56.069s
I reckon this is definitely a step in the right direction and I also realized something that you mentioned before and we didn't account for yet.
To be honest I am not sure that this issue, can be solved with the current tree implementation.
Basically it all comes down to this:
// try to find node by matching name against nodes
if node == nil {
node, _, _, _ = t.Match(name)
}
What happens here is when we attempt to match it we essentially reference the wildcard one found and matched - x/{param}
.
This reference is undesirable and has side effects in other matches after because the tree remains [name: x/{param}] instead of becoming [0][name: x/{param}], [1][name: x/x]
. Our aim though is only to verify the match so that we can justify creating our new node with the static route. This is different in nature because we don't want to reference x/{param} on match but to merely get a confirmation that we can create a new node with the static route and add it to the tree.
Two approaches stem from that to solve this:
Scenario one - reuse the same tree and add the new node on match:
// try to find node by matching name against nodes
if node == nil {
if node, _, _, _ = t.Match(name); node == nil {
panic("Could not find node for given path")
} else {
newNode := NewNode(parts[0], 0)
newTree = t.withNode(newNode)
}
}
The main difference seems to be that when the request comes in without that change it loops only through x/{param} because we didn't create a new node we just referenced the matching one while with the above change the request will be matched against both x/{param}
and x/x
because we created and added a new node to the tree once we verified that x/x
is matching x/{param}
. The same is valid for the middleware part without the above change the middleware will be attached to x/{param}
because we referenced it instead of creating a new node. Thus it will incorrectly map the middleware to x/{param}
instead of x/x
as intended.
Scenario two - create two separate independent trees and match the request to each of them sequentially:
Step one: Create accessTree with access routes defined by router.GET
:
accessTree := [name: x/{param}]
// WithRoute returns new Tree with Route set to Node // Route is set to Node under the give path, if Node does not exist it is created func (t Tree) WithRoute(path string, route Route, maxParamsSize uint8) Tree { path = pathutils.TrimSlash(path) if path == "" { return t }
parts := strings.Split(path, "/")
name, _ := pathutils.GetNameFromPart(parts[0])
node := t.Find(name)
accessTree := t
if node == nil {
node = NewNode(parts[0], maxParamsSize)
accessTree = t.withNode(node)
}
if len(parts) == 1 {
node.WithRoute(route)
} else {
node.WithChildren(node.Tree().WithRoute(strings.Join(parts[1:], "/"), route, node.MaxParamsSize()))
}
return accessTree
}
Step two: Create middlewareTree with middleware routes defined by router.USE
which are first matched to the corresponding same depth nodes in the accessTree to be verified matching but not referencing them:
// WithMiddleware returns new Tree with Middleware appended to given Node
// Middleware is appended to Node under the give path, if Node does not exist it will panic
func (t Tree) WithMiddleware(path string, m middleware.Middleware) Tree {
path = pathutils.TrimSlash(path)
if path == "" {
return t
}
parts := strings.Split(path, "/")
name, _ := pathutils.GetNameFromPart(parts[0])
node := t.Find(name)
middlewareTree := t
// try to find node by matching name against nodes
if node == nil {
if node, _, _, _ = t.Match(name); node == nil {
panic("Could not find node for given path")
} else {
middlewareTree = NewNode(parts[0], maxParamsSize)
middlewareTree = t.withNode(newNode)
}
}
if len(parts) == 1 {
node.AppendMiddleware(m)
} else {
node.WithChildren(node.Tree().WithMiddleware(strings.Join(parts[1:], "/"), m))
}
return middlewareTree
}
That way we end up with two trees:
accessTree: [name: x/{param}] - no middleware attached
middlewareTree: [name: x/x] - middleware attached
When the request comes in:
router.GET("/x/{param}", func(ctx *fasthttp.RequestCtx) {
params := ctx.UserValue("params").(context.Params)
if _, err := fmt.Fprintf(ctx, "%s", params.Value("param")); err != nil {
t.Fatal(err)
}
})
router.USE(http.MethodGet, "/x/x", mockFastHTTPMiddleware("m1"))
ctx := buildFastHTTPRequestContext(http.MethodGet, "/x/y")
First loop router.GET("/x/{param}"
:
If root := r.routes.Find(method); root != nil {
if node, treeMiddleware, params, subPath := root.accessTree().Match("x/{param}"); node != nil && node.Route() != nil {
// accessTree now contains one node with name x/{param} and no middleware
Second loop router.USE("x/x")
:
If root := r.routes.Find(method); root != nil {
if node, treeMiddleware, params, subPath := root.middlewareTree().Match("x/x"); node != nil && node.Route() != nil {
// middlewareTree now contains one node with name x/x and a middleware attached
Third loop ctx := buildFastHTTPRequestContext(http.MethodGet, "/x/y")
:
Option one - match accessTree
first if successful try matching middlewareTree
:
if node, treeMiddleware, params, subPath := root.accessTree().Match("x/y"); node != nil && node.Route() != nil {
if node, treeMiddleware, params, subPath := root.middlewareTree().Match("x/y"); node != nil && node.Route() != nil {
Option two:
Change structure of type Tree
from []Node
to:
type accessTree []Node
type middlewareTree []Node
type Tree struct {
*accessTree
*middlewareTree
}
Create two more Match methods one for each type like the Match method for the nodes:
// Match path to accessTree Node
func (t *accessTree) Match(path string) (Node, middleware.Middleware, context.Params, string) {
for _, child := range t {
if node, m, params, subPath := child.Match(path); node != nil {
return node, m, params, subPath
}
}
return nil, nil, nil, ""
}
// Match path to middlewareTree Node
func (t *middlewareTree) Match(path string) (Node, middleware.Middleware, context.Params, string) {
for _, child := range t {
if node, m, params, subPath := child.Match(path); node != nil {
return node, m, params, subPath
}
}
return nil, nil, nil, ""
}
I think scenario two is very tedious and creates quite a lot of overhead and complexity so if scenario one works we can avoid resorting to that although as the router matures with more tests and functionality it might become inevitable as a new way to structure the Tree with sub-trees to separate nodes by concerns - access nodes checking only if the request shall pass and independent middleware nodes which are in a separate tree for checking if they match access node routes.
What do you think about all this, are we missing something, do you see any major flaws ?
Everything above is just my grasp so far and I am not yet 100% sure about it because I still don't understand all aspects of tree creation in the original code for example why:
node := t.Find(name)
newTree := t
and it is still working while I was expecting that the tree will be empty before calling t.WithNode(node)
:
node := t.Find(name)
newTree := t.withNode(node)
A hint about that would be more than welcome.
In the meantime I will write some granular tests to try to cover all of the above theory.
Edit: After doing some tests I think that the only viable option is scenario 2. Basically no matter how we tweak it we will always end up with one tree. At the same time the right way for the router to traverse the tree when matching the request is to go first through the tree with router.GET
generated nodes to ensure access to proceed to the router.USE
generated tree. After this match succeeds we start traversing and matching the middleware generated nodes which are held in a separate tree as discussed above.
➜ gorouter git:(master) ✗ go test -bench=. -cpu=4 -benchmem
test
goos: darwin
goarch: amd64
pkg: github.com/vardius/gorouter/v4
BenchmarkStatic1-4 35997880 27.9 ns/op 0 B/op 0 allocs/op
BenchmarkStatic2-4 33372828 37.7 ns/op 0 B/op 0 allocs/op
BenchmarkStatic3-4 28278296 43.6 ns/op 0 B/op 0 allocs/op
BenchmarkStatic5-4 18393667 57.0 ns/op 0 B/op 0 allocs/op
BenchmarkStatic10-4 13458920 95.3 ns/op 0 B/op 0 allocs/op
BenchmarkStatic20-4 7444556 158 ns/op 0 B/op 0 allocs/op
BenchmarkWildcard1-4 5017465 225 ns/op 496 B/op 5 allocs/op
BenchmarkWildcard2-4 4917728 300 ns/op 528 B/op 5 allocs/op
BenchmarkWildcard3-4 4460277 279 ns/op 560 B/op 5 allocs/op
BenchmarkWildcard5-4 4117964 361 ns/op 624 B/op 5 allocs/op
BenchmarkWildcard10-4 2757624 405 ns/op 784 B/op 5 allocs/op
BenchmarkWildcard20-4 1877768 562 ns/op 1104 B/op 5 allocs/op
BenchmarkRegexp1-4 3372494 353 ns/op 500 B/op 5 allocs/op
BenchmarkRegexp2-4 2474242 501 ns/op 532 B/op 5 allocs/op
BenchmarkRegexp3-4 1929957 663 ns/op 564 B/op 5 allocs/op
BenchmarkRegexp5-4 1332510 933 ns/op 629 B/op 5 allocs/op
BenchmarkRegexp10-4 617763 2248 ns/op 791 B/op 5 allocs/op
BenchmarkRegexp20-4 366548 3350 ns/op 1113 B/op 5 allocs/op
BenchmarkFastHTTPStatic1-4 29598078 35.3 ns/op 2 B/op 1 allocs/op
BenchmarkFastHTTPStatic2-4 27048343 45.7 ns/op 4 B/op 1 allocs/op
BenchmarkFastHTTPStatic3-4 20608680 76.4 ns/op 8 B/op 1 allocs/op
BenchmarkFastHTTPStatic5-4 15883792 72.0 ns/op 16 B/op 1 allocs/op
BenchmarkFastHTTPStatic10-4 11401267 124 ns/op 32 B/op 1 allocs/op
BenchmarkFastHTTPStatic20-4 5862892 217 ns/op 48 B/op 1 allocs/op
BenchmarkFastHTTPWildcard1-4 11145861 98.8 ns/op 66 B/op 3 allocs/op
BenchmarkFastHTTPWildcard2-4 9085635 178 ns/op 100 B/op 3 allocs/op
BenchmarkFastHTTPWildcard3-4 6847353 172 ns/op 136 B/op 3 allocs/op
BenchmarkFastHTTPWildcard5-4 6322854 185 ns/op 208 B/op 3 allocs/op
BenchmarkFastHTTPWildcard10-4 4685559 308 ns/op 384 B/op 3 allocs/op
BenchmarkFastHTTPWildcard20-4 2442094 786 ns/op 720 B/op 3 allocs/op
BenchmarkFastHTTPRegexp1-4 4234552 254 ns/op 69 B/op 3 allocs/op
BenchmarkFastHTTPRegexp2-4 2538032 486 ns/op 113 B/op 3 allocs/op
BenchmarkFastHTTPRegexp3-4 1806182 589 ns/op 145 B/op 3 allocs/op
BenchmarkFastHTTPRegexp5-4 1000000 1078 ns/op 226 B/op 3 allocs/op
BenchmarkFastHTTPRegexp10-4 740905 2075 ns/op 419 B/op 3 allocs/op
BenchmarkFastHTTPRegexp20-4 335192 3066 ns/op 791 B/op 3 allocs/op
PASS
ok github.com/vardius/gorouter/v4 56.036s
➜ gorouter git:(hotfix/middleware-by-path) ✗ go test -bench=. -cpu=4 -benchmem
test
goos: darwin
goarch: amd64
pkg: github.com/vardius/gorouter/v4
BenchmarkStatic1-4 28802757 46.3 ns/op 0 B/op 0 allocs/op
BenchmarkStatic2-4 20745105 51.6 ns/op 0 B/op 0 allocs/op
BenchmarkStatic3-4 17873828 63.0 ns/op 0 B/op 0 allocs/op
BenchmarkStatic5-4 12449367 89.2 ns/op 0 B/op 0 allocs/op
BenchmarkStatic10-4 7169173 156 ns/op 0 B/op 0 allocs/op
BenchmarkStatic20-4 3915980 298 ns/op 0 B/op 0 allocs/op
BenchmarkWildcard1-4 4210562 259 ns/op 496 B/op 5 allocs/op
BenchmarkWildcard2-4 3726679 304 ns/op 528 B/op 5 allocs/op
BenchmarkWildcard3-4 3704440 321 ns/op 560 B/op 5 allocs/op
BenchmarkWildcard5-4 2829639 367 ns/op 624 B/op 5 allocs/op
BenchmarkWildcard10-4 2339979 530 ns/op 784 B/op 5 allocs/op
BenchmarkWildcard20-4 1485880 946 ns/op 1104 B/op 5 allocs/op
BenchmarkRegexp1-4 2667024 410 ns/op 500 B/op 5 allocs/op
BenchmarkRegexp2-4 2204582 533 ns/op 532 B/op 5 allocs/op
BenchmarkRegexp3-4 1413793 959 ns/op 566 B/op 5 allocs/op
BenchmarkRegexp5-4 1000000 1020 ns/op 630 B/op 5 allocs/op
BenchmarkRegexp10-4 621724 1808 ns/op 791 B/op 5 allocs/op
BenchmarkRegexp20-4 337516 3303 ns/op 1115 B/op 5 allocs/op
BenchmarkFastHTTPStatic1-4 22720663 51.7 ns/op 2 B/op 1 allocs/op
BenchmarkFastHTTPStatic2-4 14602131 69.3 ns/op 4 B/op 1 allocs/op
BenchmarkFastHTTPStatic3-4 13987215 87.2 ns/op 8 B/op 1 allocs/op
BenchmarkFastHTTPStatic5-4 10888626 112 ns/op 16 B/op 1 allocs/op
BenchmarkFastHTTPStatic10-4 6836913 181 ns/op 32 B/op 1 allocs/op
BenchmarkFastHTTPStatic20-4 3270436 350 ns/op 48 B/op 1 allocs/op
BenchmarkFastHTTPWildcard1-4 8443278 125 ns/op 66 B/op 3 allocs/op
BenchmarkFastHTTPWildcard2-4 7232194 155 ns/op 100 B/op 3 allocs/op
BenchmarkFastHTTPWildcard3-4 6635278 179 ns/op 136 B/op 3 allocs/op
BenchmarkFastHTTPWildcard5-4 4677546 242 ns/op 208 B/op 3 allocs/op
BenchmarkFastHTTPWildcard10-4 3079581 362 ns/op 384 B/op 3 allocs/op
BenchmarkFastHTTPWildcard20-4 1788163 698 ns/op 720 B/op 3 allocs/op
BenchmarkFastHTTPRegexp1-4 5048209 247 ns/op 70 B/op 3 allocs/op
BenchmarkFastHTTPRegexp2-4 2915995 439 ns/op 112 B/op 3 allocs/op
BenchmarkFastHTTPRegexp3-4 2035629 569 ns/op 145 B/op 3 allocs/op
BenchmarkFastHTTPRegexp5-4 1375347 839 ns/op 225 B/op 3 allocs/op
BenchmarkFastHTTPRegexp10-4 619560 1649 ns/op 419 B/op 3 allocs/op
BenchmarkFastHTTPRegexp20-4 422613 3054 ns/op 790 B/op 3 allocs/op
PASS
ok github.com/vardius/gorouter/v4 53.734s
Would be good to add some tests to hit 100% coverage or at least close to that.
It will happen slowly over time as I discover more cases to test.
It was meant to be a quick demo not really intended to be merged.
@mar1n3r0 can you have a look and let me know if this changes are easy to understand. we want code to be readable and not complex
Works like a charm! The solution seems easy to comprehend. You avoided creating a new tree by managing middleware order per node instead. If I got that right then it's understandable :)
@mar1n3r0 so you feel like adding some test cases to increase the coverage so all check are passing ? https://github.com/vardius/gorouter/pull/15#issuecomment-570126051
I think with some more test we can merge and release a new version.
I think codecov
is only relative and should not be taken as an absolute criteria to achieve numbers - the coverage percentage depends on the architecture of the code . In our case a lot of the functionality can not be tested outside of the context of fasthttp/nethttp
. For example MatchRoute
and MatchMiddleware
require a node with route. This is only available within a full request cycle where there is a request handler. To the same extend this is valid for the dependency between the tree and the nodes. Because Match
is recursively calling children.Match
and referencing the tree from within the node we can not isolate this logic in node tests.
Codecov Report
100% <100%> (ø)
90.9% <100%> (+23.05%)
45.53% <26.08%> (-3.95%)
33.06% <33.33%> (-1.13%)
80% <80%> (-20%)
84.4% <91.66%> (-7.98%)
87.61% <97.14%> (-10.56%)
Continue to review full report at Codecov.