Skip to content

Commit

Permalink
Add RenderGraphvizDot() (#2670)
Browse files Browse the repository at this point in the history
This was particularly useful when writing unit tests for #2651
  • Loading branch information
tleyden authored Jun 23, 2017
1 parent 8338da6 commit f3e0075
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 0 deletions.
102 changes: 102 additions & 0 deletions db/revtree.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"errors"

"github.com/couchbase/sync_gateway/base"
"bytes"
)

type RevKey string
Expand All @@ -31,6 +32,10 @@ type RevInfo struct {
Channels base.Set
}

func (rev RevInfo) IsRoot() bool {
return rev.Parent == ""
}

// A revision tree maps each revision ID to its RevInfo.
type RevTree map[string]*RevInfo

Expand Down Expand Up @@ -348,6 +353,103 @@ func (tree RevTree) pruneRevisions(maxDepth uint32, keepRev string) (pruned int)
return
}

// Render the RevTree in Graphviz Dot format, which can then be used to generate a PNG diagram
// like http://cbmobile-bucket.s3.amazonaws.com/diagrams/example-sync-gateway-revtrees/three_branches.png
// using the command: dot -Tpng revtree.dot > revtree.png or an online tool such as webgraphviz.com
func (tree RevTree) RenderGraphvizDot() string {

resultBuffer := bytes.Buffer{}

// Helper func to surround graph node w/ double quotes
surroundWithDoubleQuotes := func(orig string) string {
return fmt.Sprintf(`"%s"`, orig)
}

// Helper func to get the graphviz dot representation of a node
dotRepresentation := func(node *RevInfo) string {
switch node.Deleted {
case false:
return fmt.Sprintf(
"%s -> %s; ",
surroundWithDoubleQuotes(node.Parent),
surroundWithDoubleQuotes(node.ID),
)
default:
multilineResult := bytes.Buffer{}
multilineResult.WriteString(
fmt.Sprintf(
`%s [fontcolor=red];`,
surroundWithDoubleQuotes(node.ID),
),
)
multilineResult.WriteString(
fmt.Sprintf(
`%s -> %s [label="Tombstone", fontcolor=red];`,
surroundWithDoubleQuotes(node.Parent),
surroundWithDoubleQuotes(node.ID),
),
)

return multilineResult.String()
}

}

// Helper func to append node to result: parent -> child;
dupes := base.Set{}
appendNodeToResult := func(node *RevInfo) {
nodeAsDotText := dotRepresentation(node)
if dupes.Contains(nodeAsDotText) {
return
} else {
dupes[nodeAsDotText] = struct{}{}
}
resultBuffer.WriteString(nodeAsDotText)
}

// Start graphviz dot file
resultBuffer.WriteString("digraph graphname{")

// This function will be called back for every leaf node in tree
leafProcessor := func(leaf *RevInfo) {

// Append the leaf to the output
appendNodeToResult(leaf)

// Walk up the tree until we find a root, and append each node
node := leaf
for {

node = tree[node.Parent]

// Not sure how this can happen, but in any case .. probably nothing left to do for this branch
if node == nil {
break
}

// Reached a root, we're done -- there's no need
// to call appendNodeToResult() on the root, since
// the child of the root will have already added a node
// pointing to the root.
if node.IsRoot() {
break
}

appendNodeToResult(node)
}
}

// Iterate over leaves
tree.forEachLeaf(leafProcessor)

// Finish graphviz dot file
resultBuffer.WriteString("}")

return resultBuffer.String()

}


//////// ENCODED REVISION LISTS (_revisions):

// Parses a CouchDB _rev or _revisions property into a list of revision IDs
Expand Down
12 changes: 12 additions & 0 deletions rest/admin_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,18 @@ func (h *handler) handleGetRawDoc() error {
return err
}

func (h *handler) handleGetRevTree() error {
h.assertAdminOnly()
docid := h.PathVar("docid")
doc, err := h.db.GetDoc(docid)

if doc != nil {
h.writeText([]byte(doc.History.RenderGraphvizDot()))
}
return err
}


func (h *handler) handleGetLogging() error {
h.writeJSON(base.GetLogKeys())
return nil
Expand Down
23 changes: 23 additions & 0 deletions rest/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,29 @@ func (h *handler) writeJSON(value interface{}) {
h.writeJSONStatus(http.StatusOK, value)
}

func (h *handler) writeText(value []byte) {
h.writeTextStatus(http.StatusOK, value)
}

func (h *handler) writeTextStatus(status int, value []byte) {
if !h.requestAccepts("text/plain") {
base.Warn("Client won't accept text/plain, only %s", h.rq.Header.Get("Accept"))
h.writeStatus(http.StatusNotAcceptable, "only text/plain available")
return
}

h.setHeader("Content-Type", "text/plain charset=utf-8")
h.setHeader("Content-Length", fmt.Sprintf("%d", len(value)))
if status > 0 {
h.response.WriteHeader(status)
h.setStatus(status, "")
}
h.response.Write(value)

}



func (h *handler) addJSON(value interface{}) {
encoder := json.NewEncoder(h.response)
err := encoder.Encode(value)
Expand Down
3 changes: 3 additions & 0 deletions rest/routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ func CreateAdminRouter(sc *ServerContext) *mux.Router {
dbr.Handle("/_raw/{docid:"+docRegex+"}",
makeHandler(sc, adminPrivs, (*handler).handleGetRawDoc)).Methods("GET", "HEAD")

dbr.Handle("/_revtree/{docid:"+docRegex+"}",
makeHandler(sc, adminPrivs, (*handler).handleGetRevTree)).Methods("GET")

dbr.Handle("/_user/",
makeHandler(sc, adminPrivs, (*handler).getUsers)).Methods("GET", "HEAD")
dbr.Handle("/_user/",
Expand Down

0 comments on commit f3e0075

Please sign in to comment.