From a47105cc7d81e762013a541d197281d07b715126 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Sat, 16 Jun 2018 19:32:38 -0400 Subject: [PATCH] decoupled node data scopes --- filetree/changeinfo.go | 84 ----------- filetree/data.go | 134 ++++++++++++++++++ filetree/{changeinfo_test.go => data_test.go} | 24 +--- filetree/node.go | 33 ++--- filetree/node_test.go | 31 +++- filetree/tree.go | 26 ++-- filetree/tree_test.go | 39 +++-- image/image.go | 6 +- ui/filetreeview.go | 12 +- 9 files changed, 217 insertions(+), 172 deletions(-) delete mode 100644 filetree/changeinfo.go create mode 100644 filetree/data.go rename filetree/{changeinfo_test.go => data_test.go} (58%) diff --git a/filetree/changeinfo.go b/filetree/changeinfo.go deleted file mode 100644 index 95a6529..0000000 --- a/filetree/changeinfo.go +++ /dev/null @@ -1,84 +0,0 @@ -package filetree - -import ( - "archive/tar" - "bytes" - "crypto/md5" - "fmt" - "io" -) - -type FileChangeInfo struct { - Path string - Typeflag byte - MD5sum [16]byte - DiffType DiffType -} - -type DiffType int - -// enum to show whether a file has changed -const ( - Unchanged DiffType = iota - Changed - Added - Removed -) - -func NewFileChangeInfo(reader *tar.Reader, header *tar.Header, path string) FileChangeInfo { - if header.Typeflag == tar.TypeDir { - return FileChangeInfo{ - Path: path, - Typeflag: header.Typeflag, - MD5sum: [16]byte{}, - } - } - fileBytes := make([]byte, header.Size) - _, err := reader.Read(fileBytes) - if err != nil && err != io.EOF { - panic(err) - } - return FileChangeInfo{ - Path: path, - Typeflag: header.Typeflag, - MD5sum: md5.Sum(fileBytes), - DiffType: Unchanged, - } -} - -func (d DiffType) String() string { - switch d { - case Unchanged: - return "Unchanged" - case Changed: - return "Changed" - case Added: - return "Added" - case Removed: - return "Removed" - default: - return fmt.Sprintf("%d", int(d)) - } -} - -func (a DiffType) merge(b DiffType) DiffType { - if a == b { - return a - } - return Changed -} - -func (a *FileChangeInfo) getDiffType(b *FileChangeInfo) DiffType { - if a == nil && b == nil { - return Unchanged - } - if a == nil || b == nil { - return Changed - } - if a.Typeflag == b.Typeflag { - if bytes.Compare(a.MD5sum[:], b.MD5sum[:]) == 0 { - return Unchanged - } - } - return Changed -} diff --git a/filetree/data.go b/filetree/data.go new file mode 100644 index 0000000..05114b9 --- /dev/null +++ b/filetree/data.go @@ -0,0 +1,134 @@ +package filetree + +import ( + "archive/tar" + "bytes" + "crypto/md5" + "fmt" + "io" +) + +// enum to show whether a file has changed +const ( + Unchanged DiffType = iota + Changed + Added + Removed +) + +type NodeData struct { + ViewInfo ViewInfo + FileInfo *FileInfo + DiffType DiffType +} + +type ViewInfo struct { + Collapsed bool + Hidden bool +} + +type FileInfo struct { + Path string + Typeflag byte + MD5sum [16]byte +} + +type DiffType int + +func NewNodeData() (*NodeData) { + return &NodeData{ + ViewInfo: *NewViewInfo(), + FileInfo: nil, + DiffType: Unchanged, + } +} + +func (data *NodeData) Copy() (*NodeData) { + return &NodeData{ + ViewInfo: *data.ViewInfo.Copy(), + FileInfo: data.FileInfo.Copy(), + DiffType: data.DiffType, + } +} + + +func NewViewInfo() (view *ViewInfo) { + return &ViewInfo{ + Collapsed: false, + Hidden: false, + } +} + +func (view *ViewInfo) Copy() (newView *ViewInfo) { + newView = NewViewInfo() + *newView = *view + return newView +} + +func NewFileInfo(reader *tar.Reader, header *tar.Header, path string) FileInfo { + if header.Typeflag == tar.TypeDir { + return FileInfo{ + Path: path, + Typeflag: header.Typeflag, + MD5sum: [16]byte{}, + } + } + fileBytes := make([]byte, header.Size) + _, err := reader.Read(fileBytes) + if err != nil && err != io.EOF { + panic(err) + } + return FileInfo{ + Path: path, + Typeflag: header.Typeflag, + MD5sum: md5.Sum(fileBytes), + } +} + +func (d DiffType) String() string { + switch d { + case Unchanged: + return "Unchanged" + case Changed: + return "Changed" + case Added: + return "Added" + case Removed: + return "Removed" + default: + return fmt.Sprintf("%d", int(d)) + } +} + +func (a DiffType) merge(b DiffType) DiffType { + if a == b { + return a + } + return Changed +} + +func (data *FileInfo) Copy() *FileInfo { + if data == nil { + return nil + } + return &FileInfo{ + Path: data.Path, + Typeflag: data.Typeflag, + MD5sum: data.MD5sum, + } +} + +func (data *FileInfo) getDiffType(other *FileInfo) DiffType { + if data == nil && other == nil { + return Unchanged + } + if data == nil || other == nil { + return Changed + } + if data.Typeflag == other.Typeflag { + if bytes.Compare(data.MD5sum[:], other.MD5sum[:]) == 0 { + return Unchanged + } + } + return Changed +} diff --git a/filetree/changeinfo_test.go b/filetree/data_test.go similarity index 58% rename from filetree/changeinfo_test.go rename to filetree/data_test.go index 1892ac7..0d6fcbd 100644 --- a/filetree/changeinfo_test.go +++ b/filetree/data_test.go @@ -28,23 +28,10 @@ func TestMergeDiffTypes(t *testing.T) { } } -func TestDiffTypeFromChildren(t *testing.T) { - tree := NewFileTree() - tree.AddPath("/usr", BlankFileChangeInfo("/usr", Unchanged)) - info1 := BlankFileChangeInfo("/usr/bin", Added) - tree.AddPath("/usr/bin", info1) - info2 := BlankFileChangeInfo("/usr/bin2", Removed) - tree.AddPath("/usr/bin2", info2) - tree.Root.Children["usr"].deriveDiffType(Unchanged) - if tree.Root.Children["usr"].Data.DiffType != Changed { - t.Errorf("Expected Changed but got %v", tree.Root.Children["usr"].Data.DiffType) - } -} - func AssertDiffType(node *FileNode, expectedDiffType DiffType, t *testing.T) error { - if node.Data == nil { - t.Errorf("Expected *FileChangeInfo but got nil at Path %s", node.Path()) - return fmt.Errorf("expected *FileChangeInfo but got nil at Path %s", node.Path()) + if node.Data.FileInfo == nil { + t.Errorf("Expected *FileInfo but got nil at Path %s", node.Path()) + return fmt.Errorf("expected *FileInfo but got nil at Path %s", node.Path()) } if node.Data.DiffType != expectedDiffType { t.Errorf("Expecting node at %s to have DiffType %v, but had %v", node.Path(), expectedDiffType, node.Data.DiffType) @@ -53,12 +40,11 @@ func AssertDiffType(node *FileNode, expectedDiffType DiffType, t *testing.T) err return nil } -func BlankFileChangeInfo(path string, diffType DiffType) (f *FileChangeInfo) { - result := FileChangeInfo{ +func BlankFileChangeInfo(path string, diffType DiffType) (f *FileInfo) { + result := FileInfo{ Path: path, Typeflag: 1, MD5sum: [16]byte{1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0}, - DiffType: diffType, } return &result } diff --git a/filetree/node.go b/filetree/node.go index 61a7abb..2239b62 100644 --- a/filetree/node.go +++ b/filetree/node.go @@ -11,19 +11,17 @@ type FileNode struct { Tree *FileTree Parent *FileNode Name string - Collapsed bool - Hidden bool - Data *FileChangeInfo + Data NodeData Children map[string]*FileNode } -func NewNode(parent *FileNode, name string, data *FileChangeInfo) (node *FileNode) { +func NewNode(parent *FileNode, name string, data *FileInfo) (node *FileNode) { node = new(FileNode) node.Name = name - if data == nil { - data = &FileChangeInfo{} + node.Data = *NewNodeData() + if data != nil { + node.Data.FileInfo = data.Copy() } - node.Data = data node.Children = make(map[string]*FileNode) node.Parent = parent if parent != nil { @@ -33,21 +31,19 @@ func NewNode(parent *FileNode, name string, data *FileChangeInfo) (node *FileNod } func (node *FileNode) Copy() *FileNode { - // newNode := new(FileNode) - // *newNode = *node - // return newNode - newNode := NewNode(node.Parent, node.Name, node.Data) + newNode := NewNode(node.Parent, node.Name, node.Data.FileInfo) for name, child := range node.Children { newNode.Children[name] = child.Copy() + child.Parent = newNode } return newNode } -func (node *FileNode) AddChild(name string, data *FileChangeInfo) (child *FileNode) { +func (node *FileNode) AddChild(name string, data *FileInfo) (child *FileNode) { child = NewNode(node, name, data) if node.Children[name] != nil { // tree node already exists, replace the payload, keep the children - node.Children[name].Data = data + node.Children[name].Data.FileInfo = data.Copy() } else { node.Children[name] = child node.Tree.Size++ @@ -68,7 +64,7 @@ func (node *FileNode) String() string { var style *color.Color if node == nil { return "" - } else if node.Data == nil { + } else if node.Data.FileInfo == nil { return node.Name } switch node.Data.DiffType { @@ -86,7 +82,7 @@ func (node *FileNode) String() string { return style.Sprint(node.Name) } -func (node *FileNode) Visit(visiter Visiter) error { +func (node *FileNode) VisitDepthChildFirst(visiter Visiter) error { var keys []string for key := range node.Children { keys = append(keys, key) @@ -94,7 +90,7 @@ func (node *FileNode) Visit(visiter Visiter) error { sort.Strings(keys) for _, name := range keys { child := node.Children[name] - err := child.Visit(visiter) + err := child.VisitDepthChildFirst(visiter) if err != nil { return err } @@ -165,8 +161,7 @@ func (node *FileNode) deriveDiffType(diffType DiffType) error { myDiffType := diffType for _, v := range node.Children { - vData := v.Data - myDiffType = myDiffType.merge(vData.DiffType) + myDiffType = myDiffType.merge(v.Data.DiffType) } node.AssignDiffType(myDiffType) @@ -217,5 +212,5 @@ func (a *FileNode) compare(b *FileNode) DiffType { } // TODO: fails on nil - return a.Data.getDiffType(b.Data) + return a.Data.FileInfo.getDiffType(b.Data.FileInfo) } diff --git a/filetree/node_test.go b/filetree/node_test.go index 03a262d..18bdb27 100644 --- a/filetree/node_test.go +++ b/filetree/node_test.go @@ -1,12 +1,14 @@ package filetree -import "testing" +import ( + "testing" +) func TestAddChild(t *testing.T) { var expected, actual int tree := NewFileTree() - payload := FileChangeInfo{ + payload := FileInfo{ Path: "stufffffs", } @@ -34,16 +36,16 @@ func TestAddChild(t *testing.T) { t.Errorf("Expected 'twos' number of children to be %d got %d.", expected, actual) } - expectedFC := &FileChangeInfo{ + expectedFC := &FileInfo{ Path: "stufffffs", } - actualFC := one.Data + actualFC := one.Data.FileInfo if *expectedFC != *actualFC { t.Errorf("Expected 'ones' payload to be %+v got %+v.", expectedFC, actualFC) } - if *two.Data != *new(FileChangeInfo) { - t.Errorf("Expected 'twos' payload to be nil got %+v.", two.Data) + if two.Data.FileInfo != nil { + t.Errorf("Expected 'twos' payload to be nil got %+v.", two.Data.FileInfo) } } @@ -106,3 +108,20 @@ func TestIsWhiteout(t *testing.T) { t.Errorf("Expected Path '%s' to be a whiteout file", p2.Name) } } + +func TestDiffTypeFromChildren(t *testing.T) { + tree := NewFileTree() + tree.AddPath("/usr", BlankFileChangeInfo("/usr", Unchanged)) + + info1 := BlankFileChangeInfo("/usr/bin", Added) + tree.AddPath("/usr/bin", info1) + + info2 := BlankFileChangeInfo("/usr/bin2", Removed) + tree.AddPath("/usr/bin2", info2) + + tree.Root.Children["usr"].deriveDiffType(Unchanged) + + if tree.Root.Children["usr"].Data.DiffType != Changed { + t.Errorf("Expected Changed but got %v", tree.Root.Children["usr"].Data.DiffType) + } +} diff --git a/filetree/tree.go b/filetree/tree.go index 9ee443f..8879862 100644 --- a/filetree/tree.go +++ b/filetree/tree.go @@ -69,13 +69,13 @@ func (tree *FileTree) String() string { sort.Strings(keys) for idx, name := range keys { child := node.Children[name] - if child.Hidden { + if child.Data.ViewInfo.Hidden { continue } last := idx == (len(node.Children) - 1) - showCollapsed := child.Collapsed && len(child.Children) > 0 + showCollapsed := child.Data.ViewInfo.Collapsed && len(child.Children) > 0 result += renderLine(child.String(), spaces, last, showCollapsed) - if len(child.Children) > 0 && !child.Collapsed { + if len(child.Children) > 0 && !child.Data.ViewInfo.Collapsed { spacesChild := append(spaces, last) result += walkTree(child, spacesChild, depth+1) } @@ -90,7 +90,7 @@ func (tree *FileTree) Copy() *FileTree { newTree := NewFileTree() *newTree = *tree newTree.Root = tree.Root.Copy() - newTree.Visit(func(node *FileNode) error { + newTree.VisitDepthChildFirst(func(node *FileNode) error { node.Tree = newTree return nil }) @@ -101,10 +101,12 @@ func (tree *FileTree) Copy() *FileTree { type Visiter func(*FileNode) error type VisitEvaluator func(*FileNode) bool -func (tree *FileTree) Visit(visiter Visiter) error { - return tree.Root.Visit(visiter) +// DFS bubble up +func (tree *FileTree) VisitDepthChildFirst(visiter Visiter) error { + return tree.Root.VisitDepthChildFirst(visiter) } +// DFS sink down func (tree *FileTree) VisitDepthParentFirst(visiter Visiter, evaluator VisitEvaluator) error { return tree.Root.VisitDepthParentFirst(visiter, evaluator) } @@ -117,14 +119,14 @@ func (tree *FileTree) Stack(upper *FileTree) error { return fmt.Errorf("Cannot remove node %s: %v", node.Path(), err.Error()) } } else { - newNode, err := tree.AddPath(node.Path(), node.Data) + newNode, err := tree.AddPath(node.Path(), node.Data.FileInfo) if err != nil { return fmt.Errorf("Cannot add node %s: %v", newNode.Path(), err.Error()) } } return nil } - return upper.Visit(graft) + return upper.VisitDepthChildFirst(graft) } func (tree *FileTree) GetNode(path string) (*FileNode, error) { @@ -142,7 +144,7 @@ func (tree *FileTree) GetNode(path string) (*FileNode, error) { return node, nil } -func (tree *FileTree) AddPath(path string, data *FileChangeInfo) (*FileNode, error) { +func (tree *FileTree) AddPath(path string, data *FileInfo) (*FileNode, error) { // fmt.Printf("ADDPATH: %s %+v\n", path, data) nodeNames := strings.Split(path, "/") node := tree.Root @@ -161,7 +163,7 @@ func (tree *FileTree) AddPath(path string, data *FileChangeInfo) (*FileNode, err // attach payload to the last specified node if idx == len(nodeNames)-1 { - node.Data = data + node.Data.FileInfo = data } } @@ -186,7 +188,7 @@ func (tree *FileTree) Compare(upper *FileTree) error { } else { existingNode, _ := tree.GetNode(node.Path()) if existingNode == nil { - newNode, err := tree.AddPath(node.Path(), node.Data) + newNode, err := tree.AddPath(node.Path(), node.Data.FileInfo) // fmt.Printf("added new node at %s\n", newNode.Path()) if err != nil { return fmt.Errorf("Cannot add new node %s: %v", node.Path(), err.Error()) @@ -200,7 +202,7 @@ func (tree *FileTree) Compare(upper *FileTree) error { } return nil } - return upper.Visit(graft) + return upper.VisitDepthChildFirst(graft) } func (tree *FileTree) MarkRemoved(path string) error { diff --git a/filetree/tree_test.go b/filetree/tree_test.go index 14cb61b..a9c6652 100644 --- a/filetree/tree_test.go +++ b/filetree/tree_test.go @@ -86,7 +86,7 @@ func TestRemovePath(t *testing.T) { func TestStack(t *testing.T) { payloadKey := "/var/run/systemd" - payloadValue := FileChangeInfo{ + payloadValue := FileInfo{ Path: "yup", } @@ -128,8 +128,8 @@ func TestStack(t *testing.T) { t.Errorf("Expected '%s' to still exist, but it doesn't", payloadKey) } - if *node.Data != payloadValue { - t.Errorf("Expected '%s' value to be %+v but got %+v", payloadKey, payloadValue, node.Data) + if *node.Data.FileInfo != payloadValue { + t.Errorf("Expected '%s' value to be %+v but got %+v", payloadKey, payloadValue, node.Data.FileInfo) } actual := tree1.String() @@ -177,11 +177,10 @@ func TestCompareWithNoChanges(t *testing.T) { paths := [...]string{"/etc", "/etc/sudoers", "/etc/hosts", "/usr/bin", "/usr/bin/bash", "/usr"} for _, value := range paths { - fakeData := FileChangeInfo{ + fakeData := FileInfo{ Path: value, Typeflag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - DiffType: Unchanged, } lowerTree.AddPath(value, &fakeData) upperTree.AddPath(value, &fakeData) @@ -191,16 +190,16 @@ func TestCompareWithNoChanges(t *testing.T) { if n.Path() == "/" { return nil } - if n.Data == nil { - t.Errorf("Expected *FileChangeInfo but got nil") - return fmt.Errorf("expected *FileChangeInfo but got nil") + if n.Data.FileInfo == nil { + t.Errorf("Expected *FileInfo but got nil") + return fmt.Errorf("expected *FileInfo but got nil") } if (n.Data.DiffType) != Unchanged { t.Errorf("Expecting node at %s to have DiffType unchanged, but had %v", n.Path(), n.Data.DiffType) } return nil } - err := lowerTree.Visit(asserter) + err := lowerTree.VisitDepthChildFirst(asserter) if err != nil { t.Error(err) } @@ -213,21 +212,19 @@ func TestCompareWithAdds(t *testing.T) { upperPaths := [...]string{"/etc", "/etc/sudoers", "/usr", "/etc/hosts", "/usr/bin", "/usr/bin/bash"} for _, value := range lowerPaths { - fakeData := FileChangeInfo{ + fakeData := FileInfo{ Path: value, Typeflag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - DiffType: Unchanged, } lowerTree.AddPath(value, &fakeData) } for _, value := range upperPaths { - fakeData := FileChangeInfo{ + fakeData := FileInfo{ Path: value, Typeflag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - DiffType: Unchanged, } upperTree.AddPath(value, &fakeData) } @@ -251,7 +248,7 @@ func TestCompareWithAdds(t *testing.T) { } return AssertDiffType(n, Unchanged, t) } - err := lowerTree.Visit(asserter) + err := lowerTree.VisitDepthChildFirst(asserter) if err != nil { t.Error(err) } @@ -264,21 +261,19 @@ func TestCompareWithChanges(t *testing.T) { upperPaths := [...]string{"/etc", "/usr", "/etc/hosts", "/etc/sudoers", "/usr/bin"} for _, value := range lowerPaths { - fakeData := FileChangeInfo{ + fakeData := FileInfo{ Path: value, Typeflag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - DiffType: Unchanged, } lowerTree.AddPath(value, &fakeData) } for _, value := range upperPaths { - fakeData := FileChangeInfo{ + fakeData := FileInfo{ Path: value, Typeflag: 1, MD5sum: [16]byte{1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0}, - DiffType: Unchanged, } upperTree.AddPath(value, &fakeData) } @@ -291,7 +286,7 @@ func TestCompareWithChanges(t *testing.T) { } return AssertDiffType(n, Changed, t) } - err := lowerTree.Visit(asserter) + err := lowerTree.VisitDepthChildFirst(asserter) if err != nil { t.Error(err) } @@ -315,21 +310,19 @@ func TestStackRange(t *testing.T) { upperPaths := [...]string{"/etc", "/usr", "/etc/hosts", "/etc/sudoers", "/usr/bin"} for _, value := range lowerPaths { - fakeData := FileChangeInfo{ + fakeData := FileInfo{ Path: value, Typeflag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - DiffType: Unchanged, } lowerTree.AddPath(value, &fakeData) } for _, value := range upperPaths { - fakeData := FileChangeInfo{ + fakeData := FileInfo{ Path: value, Typeflag: 1, MD5sum: [16]byte{1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0}, - DiffType: Unchanged, } upperTree.AddPath(value, &fakeData) } diff --git a/image/image.go b/image/image.go index 140f4ba..96bc0cd 100644 --- a/image/image.go +++ b/image/image.go @@ -184,8 +184,8 @@ func saveImage(imageID string) (string, string) { return imageTarPath, tmpDir } -func getFileList(parentReader *tar.Reader, h *tar.Header) []filetree.FileChangeInfo { - var files []filetree.FileChangeInfo +func getFileList(parentReader *tar.Reader, h *tar.Header) []filetree.FileInfo { + var files []filetree.FileInfo var tarredBytes = make([]byte, h.Size) _, err := parentReader.Read(tarredBytes) @@ -214,7 +214,7 @@ func getFileList(parentReader *tar.Reader, h *tar.Header) []filetree.FileChangeI case tar.TypeXHeader: fmt.Printf("ERRG: XHeader: %v: %s\n", header.Typeflag, name) default: - files = append(files, filetree.NewFileChangeInfo(tarReader, header, name)) + files = append(files, filetree.NewFileInfo(tarReader, header, name)) } } return files diff --git a/ui/filetreeview.go b/ui/filetreeview.go index 9d81cf8..85dadeb 100644 --- a/ui/filetreeview.go +++ b/ui/filetreeview.go @@ -81,11 +81,11 @@ func (view *FileTreeView) setLayer(layerIndex int) error { visitor := func(node *filetree.FileNode) error { newNode, err := newTree.GetNode(node.Path()) if err == nil { - newNode.Collapsed = node.Collapsed + newNode.Data.ViewInfo.Collapsed = node.Data.ViewInfo.Collapsed } return nil } - view.Tree.Visit(visitor) + view.Tree.VisitDepthChildFirst(visitor) // now that the tree has been rebuilt, keep the view seleciton in parity with the previous selection view.setHiddenFromDiffTypes() @@ -132,7 +132,7 @@ func (view *FileTreeView) getAbsPositionNode() (node *filetree.FileNode) { } evaluator = func(curNode *filetree.FileNode) bool { - return !curNode.Collapsed && !curNode.Hidden + return !curNode.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden } err := view.Tree.VisitDepthParentFirst(visiter, evaluator) @@ -145,16 +145,16 @@ func (view *FileTreeView) getAbsPositionNode() (node *filetree.FileNode) { func (view *FileTreeView) toggleCollapse() error { node := view.getAbsPositionNode() - node.Collapsed = !node.Collapsed + node.Data.ViewInfo.Collapsed = !node.Data.ViewInfo.Collapsed return view.Render() } func (view *FileTreeView) setHiddenFromDiffTypes() error { visitor := func(node *filetree.FileNode) error { - node.Hidden = view.HiddenDiffTypes[node.Data.DiffType] + node.Data.ViewInfo.Hidden = view.HiddenDiffTypes[node.Data.DiffType] return nil } - view.Tree.Visit(visitor) + view.Tree.VisitDepthChildFirst(visitor) return view.Render() }