Skip to content

Commit

Permalink
feat(grove): support removing subtrees
Browse files Browse the repository at this point in the history
  • Loading branch information
whereswaldon committed Oct 11, 2020
1 parent d32d433 commit e3b60d5
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 0 deletions.
34 changes: 34 additions & 0 deletions grove/grove.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type FS interface {
Open(path string) (File, error)
Create(path string) (File, error)
OpenFile(path string, flag int, perm os.FileMode) (File, error)
Remove(path string) error
}

// RelativeFS is a file system that acts relative to a specific path
Expand Down Expand Up @@ -70,6 +71,12 @@ func (r RelativeFS) OpenFile(path string, flag int, perm os.FileMode) (File, err
return os.OpenFile(r.resolve(path), flag, perm)
}

// Remove removes the given path relative to the root
// of the RelativeFS.
func (r RelativeFS) Remove(path string) error {
return os.Remove(r.resolve(path))
}

// Grove is an on-disk store for arbor forest nodes. It maintains internal
// in-memory caches in order to accelerate certain expensive operations.
// Because of this, it must be notified when new content appears on disk.
Expand Down Expand Up @@ -415,3 +422,30 @@ func (g *Grove) GetReply(communityID, conversationID, replyID *fields.QualifiedH
func (g *Grove) CopyInto(other forest.Store) error {
return fmt.Errorf("method CopyInto() is not currently implemented on Grove")
}

// RemoveSubtree removes the subtree rooted at the node
// with the provided ID from the grove.
func (g *Grove) RemoveSubtree(id *fields.QualifiedHash) error {
children, err := g.Children(id)
if err != nil {
return fmt.Errorf("failed looking up children of %s: %w", id, err)
}
for _, child := range children {
if err := g.RemoveSubtree(child); err != nil {
return fmt.Errorf("failed removing children of %s: %w", child, err)
}
}
child, _, err := g.Get(id)
if err != nil {
return fmt.Errorf("failed looking up child %s during removal: %w", id, err)
}
g.ChildCache.RemoveChild(child.ParentID(), id)
g.ChildCache.RemoveParent(id)
if err := g.NodeCache.RemoveSubtree(id); err != nil {
return fmt.Errorf("failed removing node %s from internal cache: %w", id, err)
}
if err := g.Remove(id.String()); err != nil {
return fmt.Errorf("failed removing node %s from filesystem: %w", id, err)
}
return nil
}
72 changes: 72 additions & 0 deletions grove/grove_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,15 @@ func (r fakeFS) OpenFile(path string, flag int, perm os.FileMode) (grove.File, e
return r.Open(path)
}

func (r fakeFS) Remove(path string) error {
_, exists := r.files[path]
if !exists {
return fmt.Errorf("file doesn't exist")
}
delete(r.files, path)
return nil
}

// errFS is a testing type that wraps an ordinary FS with the ability to
// return a specific error on any function call.
type errFS struct {
Expand Down Expand Up @@ -293,6 +302,13 @@ func (r errFS) OpenFile(path string, flag int, perm os.FileMode) (grove.File, er
return r.fs.OpenFile(path, flag, perm)
}

func (r errFS) Remove(path string) error {
if r.error != nil {
return r.error
}
return r.fs.Remove(path)
}

type testNodeBuilder struct {
*testing.T
*forest.Builder
Expand Down Expand Up @@ -851,3 +867,59 @@ func TestGroveRecentOpenNodeFails(t *testing.T) {
t.Errorf("Expected no recent nodes for when reading a node failed, found %d", len(replies))
}
}

func TestGroveRemoveSubtree(t *testing.T) {
fs := newFakeFS()
fakeNodeBuilder := NewNodeBuilder(t)
reply, replyFile := fakeNodeBuilder.newReplyFile("test content")
reply1, replyFile1 := fakeNodeBuilder.newReplyFile("test content")
identity := fakeNodeBuilder.Builder.User
identityData, err := identity.MarshalBinary()
idFileName, _ := identity.ID().MarshalString()
idFile := newFakeFile(idFileName, identityData)
community := fakeNodeBuilder.Community
communityData, err := community.MarshalBinary()
communityFileName, _ := community.ID().MarshalString()
communityFile := newFakeFile(communityFileName, communityData)

resetAll := func() {
replyFile.ResetBuffer()
replyFile1.ResetBuffer()
idFile.ResetBuffer()
communityFile.ResetBuffer()
}
g, err := grove.NewWithFS(fs)
if err != nil {
t.Errorf("Failed constructing grove: %v", err)
}

// add node to fs, now should be discoverable
fs.files[replyFile.Name()] = replyFile
fs.files[idFile.Name()] = idFile
fs.files[communityFile.Name()] = communityFile
fs.files[replyFile1.Name()] = replyFile1

if err := g.RemoveSubtree(reply1.ID()); err != nil {
t.Errorf("should not have failed to remove node: %v", err)
}
if children, err := g.Children(community.ID()); err != nil {
t.Errorf("Expected looking for community children to succeed: %v", err)
} else if len(children) > 1 {
t.Errorf("Expected 1 child nodes for community, found %d", len(children))
}
resetAll()

if err := g.RemoveSubtree(community.ID()); err != nil {
t.Errorf("should not have failed to remove node: %v", err)
}
if _, has, err := g.GetCommunity(community.ID()); err != nil {
t.Errorf("should not error when looking up nonexistent node: %v", err)
} else if has {
t.Errorf("should no longer have community after removing subtree rooted at it.")
}
if _, has, err := g.GetCommunity(reply.ID()); err != nil {
t.Errorf("should not error when looking up nonexistent node: %v", err)
} else if has {
t.Errorf("should no longer have reply after removing community above it.")
}
}

0 comments on commit e3b60d5

Please sign in to comment.