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