github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/util/fsm/debug.go (about) 1 // Copyright 2017 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package fsm 12 13 import ( 14 "bytes" 15 "fmt" 16 "io" 17 "sort" 18 "strings" 19 ) 20 21 type debugInfo struct { 22 t Transitions 23 sortedStateNames []string 24 sortedEventNames []string 25 stateNameMap map[string]State 26 eventNameMap map[string]Event 27 reachableStates map[string]struct{} 28 } 29 30 // eventAppliedToState returns the Transition resulting from applying the 31 // specified Event to the specified State and the bool true, or false if there 32 // is no associated transition. 33 func (di debugInfo) eventAppliedToState(sName, eName string) (Transition, bool) { 34 sm := di.t.expanded[di.stateNameMap[sName]] 35 tr, ok := sm[di.eventNameMap[eName]] 36 return tr, ok 37 } 38 39 func (di debugInfo) reachable(sName string) bool { 40 _, ok := di.reachableStates[sName] 41 return ok 42 } 43 44 func typeName(i interface{}) string { 45 s := fmt.Sprintf("%#v", i) 46 parts := strings.Split(s, ".") 47 return parts[len(parts)-1] 48 } 49 func trimState(s string) string { return strings.TrimPrefix(s, "state") } 50 func trimEvent(s string) string { return strings.TrimPrefix(s, "event") } 51 func stateName(s State) string { return trimState(typeName(s)) } 52 func eventName(e Event) string { return trimEvent(typeName(e)) } 53 54 func makeDebugInfo(t Transitions) debugInfo { 55 di := debugInfo{ 56 t: t, 57 stateNameMap: make(map[string]State), 58 eventNameMap: make(map[string]Event), 59 reachableStates: make(map[string]struct{}), 60 } 61 maybeAddState := func(s State, markReachable bool) { 62 sName := stateName(s) 63 if _, ok := di.stateNameMap[sName]; !ok { 64 di.sortedStateNames = append(di.sortedStateNames, sName) 65 di.stateNameMap[sName] = s 66 } 67 if markReachable { 68 di.reachableStates[sName] = struct{}{} 69 } 70 } 71 maybeAddEvent := func(e Event) { 72 eName := eventName(e) 73 if _, ok := di.eventNameMap[eName]; !ok { 74 di.sortedEventNames = append(di.sortedEventNames, eName) 75 di.eventNameMap[eName] = e 76 } 77 } 78 79 for s, sm := range di.t.expanded { 80 maybeAddState(s, false) 81 for e, tr := range sm { 82 maybeAddEvent(e) 83 84 // markReachable if this isn't a self-loop. 85 markReachable := s != tr.Next 86 maybeAddState(tr.Next, markReachable) 87 } 88 } 89 90 sort.Strings(di.sortedStateNames) 91 sort.Strings(di.sortedEventNames) 92 return di 93 } 94 95 // panicWriter wraps an io.Writer, panicing if a call to Write ever fails. 96 type panicWriter struct { 97 w io.Writer 98 } 99 100 // Write implements the io.Writer interface. 101 func (pw *panicWriter) Write(p []byte) (n int, err error) { 102 if n, err = pw.w.Write(p); err != nil { 103 panic(err) 104 } 105 return n, nil 106 } 107 108 func genReport(w io.Writer, t Transitions) { 109 w = &panicWriter{w: w} 110 di := makeDebugInfo(t) 111 var present, missing bytes.Buffer 112 for _, sName := range di.sortedStateNames { 113 defer present.Reset() 114 defer missing.Reset() 115 116 for _, eName := range di.sortedEventNames { 117 handledBuf := &missing 118 if _, ok := di.eventAppliedToState(sName, eName); ok { 119 handledBuf = &present 120 } 121 fmt.Fprintf(handledBuf, "\t\t%s\n", eName) 122 } 123 124 fmt.Fprintf(w, "%s\n", sName) 125 if !di.reachable(sName) { 126 fmt.Fprintf(w, "\tunreachable!\n") 127 } 128 fmt.Fprintf(w, "\thandled events:\n") 129 _, _ = io.Copy(w, &present) 130 fmt.Fprintf(w, "\tmissing events:\n") 131 _, _ = io.Copy(w, &missing) 132 } 133 } 134 135 func genDot(w io.Writer, t Transitions, start string) { 136 dw := dotWriter{w: &panicWriter{w: w}, di: makeDebugInfo(t)} 137 dw.Write(start) 138 } 139 140 // dotWriter writes a graph representation of the debugInfo in the DOT language. 141 type dotWriter struct { 142 w io.Writer 143 di debugInfo 144 } 145 146 func (dw dotWriter) Write(start string) { 147 dw.writeHeader(start) 148 dw.writeEdges(start) 149 dw.writeFooter() 150 } 151 152 func (dw dotWriter) writeHeader(start string) { 153 fmt.Fprintf(dw.w, "digraph finite_state_machine {\n") 154 fmt.Fprintf(dw.w, "\trankdir=LR;\n\n") 155 if start != "" { 156 if _, ok := dw.di.stateNameMap[start]; !ok { 157 panic(fmt.Sprintf("unknown state %q", start)) 158 } 159 fmt.Fprintf(dw.w, "\tnode [shape = doublecircle]; %q;\n", start) 160 fmt.Fprintf(dw.w, "\tnode [shape = point ]; qi\n") 161 fmt.Fprintf(dw.w, "\tqi -> %q;\n\n", start) 162 } 163 fmt.Fprintf(dw.w, "\tnode [shape = circle];\n") 164 } 165 166 func (dw dotWriter) writeEdges(start string) { 167 di := dw.di 168 for _, sName := range di.sortedStateNames { 169 if start != "" && start != sName { 170 if !di.reachable(sName) { 171 // If the state isn't reachable and it's not the starting state, 172 // don't include it in the graph. 173 continue 174 } 175 } 176 for _, eName := range di.sortedEventNames { 177 if tr, ok := di.eventAppliedToState(sName, eName); ok { 178 var label string 179 if tr.Description == "" { 180 label = fmt.Sprintf("%q", eName) 181 } else { 182 // We'll use an HTML label with the description on the 2nd line. 183 label = fmt.Sprintf("<%s<BR/><I>%s</I>>", eName, tr.Description) 184 } 185 fmt.Fprintf(dw.w, "\t%q -> %q [label = %s]\n", 186 sName, stateName(tr.Next), label) 187 } 188 } 189 } 190 } 191 192 func (dw dotWriter) writeFooter() { 193 fmt.Fprintf(dw.w, "}\n") 194 } 195 196 // WriteReport writes a report of the Transitions graph, reporting on which 197 // Events each State handles and which Events each state does not. 198 func (t Transitions) WriteReport(w io.Writer) { 199 genReport(w, t) 200 } 201 202 // WriteDotGraph writes a representaWriteDotGraphStringtion of the Transitions graph in the 203 // graphviz dot format. It accepts a starting State that will be expressed as 204 // such in the graph, if provided. 205 func (t Transitions) WriteDotGraph(w io.Writer, start State) { 206 genDot(w, t, stateName(start)) 207 } 208 209 // WriteDotGraphString is like WriteDotGraph, but takes the string 210 // representation of the start State. 211 func (t Transitions) WriteDotGraphString(w io.Writer, start string) { 212 start = trimState(start) 213 if !strings.Contains(start, "{") { 214 start += "{}" 215 } 216 genDot(w, t, start) 217 } 218 219 // Silence unused warning for Transitions.WriteDotGraphString. The method 220 // is used by write_reports.go.tmpl. 221 var _ = (Transitions).WriteDotGraphString