github.com/onsi/ginkgo@v1.16.6-0.20211118180735-4e1925ba4c95/ginkgo/outline/ginkgo.go (about)

     1  package outline
     2  
     3  import (
     4  	"go/ast"
     5  	"go/token"
     6  	"strconv"
     7  )
     8  
     9  const (
    10  	// undefinedTextAlt is used if the spec/container text cannot be derived
    11  	undefinedTextAlt = "undefined"
    12  )
    13  
    14  // ginkgoMetadata holds useful bits of information for every entry in the outline
    15  type ginkgoMetadata struct {
    16  	// Name is the spec or container function name, e.g. `Describe` or `It`
    17  	Name string `json:"name"`
    18  
    19  	// Text is the `text` argument passed to specs, and some containers
    20  	Text string `json:"text"`
    21  
    22  	// Start is the position of first character of the spec or container block
    23  	Start int `json:"start"`
    24  
    25  	// End is the position of first character immediately after the spec or container block
    26  	End int `json:"end"`
    27  
    28  	Spec    bool `json:"spec"`
    29  	Focused bool `json:"focused"`
    30  	Pending bool `json:"pending"`
    31  }
    32  
    33  // ginkgoNode is used to construct the outline as a tree
    34  type ginkgoNode struct {
    35  	ginkgoMetadata
    36  	Nodes []*ginkgoNode `json:"nodes"`
    37  }
    38  
    39  type walkFunc func(n *ginkgoNode)
    40  
    41  func (n *ginkgoNode) PreOrder(f walkFunc) {
    42  	f(n)
    43  	for _, m := range n.Nodes {
    44  		m.PreOrder(f)
    45  	}
    46  }
    47  
    48  func (n *ginkgoNode) PostOrder(f walkFunc) {
    49  	for _, m := range n.Nodes {
    50  		m.PostOrder(f)
    51  	}
    52  	f(n)
    53  }
    54  
    55  func (n *ginkgoNode) Walk(pre, post walkFunc) {
    56  	pre(n)
    57  	for _, m := range n.Nodes {
    58  		m.Walk(pre, post)
    59  	}
    60  	post(n)
    61  }
    62  
    63  // PropagateInheritedProperties propagates the Pending and Focused properties
    64  // through the subtree rooted at n.
    65  func (n *ginkgoNode) PropagateInheritedProperties() {
    66  	n.PreOrder(func(thisNode *ginkgoNode) {
    67  		for _, descendantNode := range thisNode.Nodes {
    68  			if thisNode.Pending {
    69  				descendantNode.Pending = true
    70  				descendantNode.Focused = false
    71  			}
    72  			if thisNode.Focused && !descendantNode.Pending {
    73  				descendantNode.Focused = true
    74  			}
    75  		}
    76  	})
    77  }
    78  
    79  // BackpropagateUnfocus propagates the Focused property through the subtree
    80  // rooted at n. It applies the rule described in the Ginkgo docs:
    81  // > Nested programmatically focused specs follow a simple rule: if a
    82  // > leaf-node is marked focused, any of its ancestor nodes that are marked
    83  // > focus will be unfocused.
    84  func (n *ginkgoNode) BackpropagateUnfocus() {
    85  	focusedSpecInSubtreeStack := []bool{}
    86  	n.PostOrder(func(thisNode *ginkgoNode) {
    87  		if thisNode.Spec {
    88  			focusedSpecInSubtreeStack = append(focusedSpecInSubtreeStack, thisNode.Focused)
    89  			return
    90  		}
    91  		focusedSpecInSubtree := false
    92  		for range thisNode.Nodes {
    93  			focusedSpecInSubtree = focusedSpecInSubtree || focusedSpecInSubtreeStack[len(focusedSpecInSubtreeStack)-1]
    94  			focusedSpecInSubtreeStack = focusedSpecInSubtreeStack[0 : len(focusedSpecInSubtreeStack)-1]
    95  		}
    96  		focusedSpecInSubtreeStack = append(focusedSpecInSubtreeStack, focusedSpecInSubtree)
    97  		if focusedSpecInSubtree {
    98  			thisNode.Focused = false
    99  		}
   100  	})
   101  
   102  }
   103  
   104  func packageAndIdentNamesFromCallExpr(ce *ast.CallExpr) (string, string, bool) {
   105  	switch ex := ce.Fun.(type) {
   106  	case *ast.Ident:
   107  		return "", ex.Name, true
   108  	case *ast.SelectorExpr:
   109  		pkgID, ok := ex.X.(*ast.Ident)
   110  		if !ok {
   111  			return "", "", false
   112  		}
   113  		// A package identifier is top-level, so Obj must be nil
   114  		if pkgID.Obj != nil {
   115  			return "", "", false
   116  		}
   117  		if ex.Sel == nil {
   118  			return "", "", false
   119  		}
   120  		return pkgID.Name, ex.Sel.Name, true
   121  	default:
   122  		return "", "", false
   123  	}
   124  }
   125  
   126  // absoluteOffsetsForNode derives the absolute character offsets of the node start and
   127  // end positions.
   128  func absoluteOffsetsForNode(fset *token.FileSet, n ast.Node) (start, end int) {
   129  	return fset.PositionFor(n.Pos(), false).Offset, fset.PositionFor(n.End(), false).Offset
   130  }
   131  
   132  // ginkgoNodeFromCallExpr derives an outline entry from a go AST subtree
   133  // corresponding to a Ginkgo container or spec.
   134  func ginkgoNodeFromCallExpr(fset *token.FileSet, ce *ast.CallExpr, ginkgoPackageName *string) (*ginkgoNode, bool) {
   135  	packageName, identName, ok := packageAndIdentNamesFromCallExpr(ce)
   136  	if !ok {
   137  		return nil, false
   138  	}
   139  
   140  	n := ginkgoNode{}
   141  	n.Name = identName
   142  	n.Start, n.End = absoluteOffsetsForNode(fset, ce)
   143  	n.Nodes = make([]*ginkgoNode, 0)
   144  	switch identName {
   145  	case "It", "Specify", "Entry":
   146  		n.Spec = true
   147  		n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
   148  		return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
   149  	case "FIt", "FSpecify", "FEntry":
   150  		n.Spec = true
   151  		n.Focused = true
   152  		n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
   153  		return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
   154  	case "PIt", "PSpecify", "XIt", "XSpecify", "PEntry", "XEntry":
   155  		n.Spec = true
   156  		n.Pending = true
   157  		n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
   158  		return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
   159  	case "Context", "Describe", "When", "DescribeTable":
   160  		n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
   161  		return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
   162  	case "FContext", "FDescribe", "FWhen", "FDescribeTable":
   163  		n.Focused = true
   164  		n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
   165  		return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
   166  	case "PContext", "PDescribe", "PWhen", "XContext", "XDescribe", "XWhen", "PDescribeTable", "XDescribeTable":
   167  		n.Pending = true
   168  		n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
   169  		return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
   170  	case "By":
   171  		n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
   172  		return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
   173  	case "AfterEach", "BeforeEach":
   174  		return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
   175  	case "JustAfterEach", "JustBeforeEach":
   176  		return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
   177  	case "AfterSuite", "BeforeSuite":
   178  		return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
   179  	case "SynchronizedAfterSuite", "SynchronizedBeforeSuite":
   180  		return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
   181  	default:
   182  		return nil, false
   183  	}
   184  }
   185  
   186  // textOrAltFromCallExpr tries to derive the "text" of a Ginkgo spec or
   187  // container. If it cannot derive it, it returns the alt text.
   188  func textOrAltFromCallExpr(ce *ast.CallExpr, alt string) string {
   189  	text, defined := textFromCallExpr(ce)
   190  	if !defined {
   191  		return alt
   192  	}
   193  	return text
   194  }
   195  
   196  // textFromCallExpr tries to derive the "text" of a Ginkgo spec or container. If
   197  // it cannot derive it, it returns false.
   198  func textFromCallExpr(ce *ast.CallExpr) (string, bool) {
   199  	if len(ce.Args) < 1 {
   200  		return "", false
   201  	}
   202  	text, ok := ce.Args[0].(*ast.BasicLit)
   203  	if !ok {
   204  		return "", false
   205  	}
   206  	switch text.Kind {
   207  	case token.CHAR, token.STRING:
   208  		// For token.CHAR and token.STRING, Value is quoted
   209  		unquoted, err := strconv.Unquote(text.Value)
   210  		if err != nil {
   211  			// If unquoting fails, just use the raw Value
   212  			return text.Value, true
   213  		}
   214  		return unquoted, true
   215  	default:
   216  		return text.Value, true
   217  	}
   218  }