...

Source file src/github.com/onsi/gomega/gleak/have_leaked_matcher.go

Documentation: github.com/onsi/gomega/gleak

     1  package gleak
     2  
     3  import (
     4  	"fmt"
     5  	"path"
     6  	"path/filepath"
     7  	"reflect"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/onsi/gomega"
    12  	"github.com/onsi/gomega/format"
    13  	"github.com/onsi/gomega/gleak/goroutine"
    14  	"github.com/onsi/gomega/types"
    15  )
    16  
    17  // ReportFilenameWithPath controls whether to show call locations in leak
    18  // reports by default in abbreviated form with only source code filename with
    19  // package name and line number, or alternatively with source code filename with
    20  // path and line number.
    21  //
    22  // That is, with ReportFilenameWithPath==false:
    23  //
    24  //	foo/bar.go:123
    25  //
    26  // Or with ReportFilenameWithPath==true:
    27  //
    28  //	/home/goworld/coolprojects/mymodule/foo/bar.go:123
    29  var ReportFilenameWithPath = false
    30  
    31  // standardFilters specifies the always automatically included no-leak goroutine
    32  // filter matchers.
    33  //
    34  // Note: it's okay to instantiate the Gomega Matchers here, as all goroutine
    35  // filtering-related gleak matchers are stateless with respect to any actual
    36  // value they try to match. This allows us to simply prepend them to any
    37  // user-supplied optional matchers when HaveLeaked returns a new goroutine
    38  // leakage detecting matcher.
    39  //
    40  // Note: cgo's goroutines with status "[syscall, locked to thread]" do not
    41  // appear any longer (since mid-2017), as these cgo goroutines are put into the
    42  // "dead" state when not in use. See: https://github.com/golang/go/issues/16714
    43  // and https://go-review.googlesource.com/c/go/+/45030/.
    44  var standardFilters = []types.GomegaMatcher{
    45  	// Ginkgo testing framework
    46  	IgnoringTopFunction("github.com/onsi/ginkgo/v2/internal.(*Suite).runNode"),
    47  	IgnoringTopFunction("github.com/onsi/ginkgo/v2/internal.(*Suite).runNode..."),
    48  	gomega.And(IgnoringTopFunction("runtime.goexit1"), IgnoringCreator("github.com/onsi/ginkgo/v2/internal.(*Suite).runNode")),
    49  	IgnoringTopFunction("github.com/onsi/ginkgo/v2/internal/interrupt_handler.(*InterruptHandler).registerForInterrupts..."),
    50  	IgnoringTopFunction("github.com/onsi/ginkgo/internal/specrunner.(*SpecRunner).registerForInterrupts"),
    51  	IgnoringCreator("github.com/onsi/ginkgo/v2/internal.(*genericOutputInterceptor).ResumeIntercepting"),
    52  	IgnoringCreator("github.com/onsi/ginkgo/v2/internal.(*genericOutputInterceptor).ResumeIntercepting..."),
    53  	IgnoringCreator("github.com/onsi/ginkgo/v2/internal.RegisterForProgressSignal"),
    54  
    55  	// goroutines of Go's own testing package for its own workings...
    56  	IgnoringTopFunction("testing.RunTests [chan receive]"),
    57  	IgnoringTopFunction("testing.(*T).Run [chan receive]"),
    58  	IgnoringTopFunction("testing.(*T).Parallel [chan receive]"),
    59  
    60  	// os/signal starts its own runtime goroutine, where loop calls signal_recv
    61  	// in a loop, so we need to expect them both...
    62  	IgnoringTopFunction("os/signal.signal_recv"),
    63  	IgnoringTopFunction("os/signal.loop"),
    64  
    65  	// signal.Notify starts a runtime goroutine...
    66  	IgnoringInBacktrace("runtime.ensureSigM"),
    67  
    68  	// reading a trace...
    69  	IgnoringInBacktrace("runtime.ReadTrace"),
    70  }
    71  
    72  // HaveLeaked succeeds (or rather, "suckceeds" considering it appears in failing
    73  // tests) if after filtering out ("ignoring") the expected goroutines from the
    74  // list of actual goroutines the remaining list of goroutines is non-empty.
    75  // These goroutines not filtered out are considered to have been leaked.
    76  //
    77  // For convenience, HaveLeaked automatically filters out well-known runtime and
    78  // testing goroutines using a built-in standard filter matchers list. In
    79  // addition to the built-in filters, HaveLeaked accepts an optional list of
    80  // non-leaky goroutine filter matchers. These filtering matchers can be
    81  // specified in different formats, as described below.
    82  //
    83  // Since there might be "pending" goroutines at the end of tests that eventually
    84  // will properly wind down so they aren't leaking, HaveLeaked is best paired
    85  // with Eventually instead of Expect. In its shortest form this will use
    86  // Eventually's default timeout and polling interval settings, but these can be
    87  // overridden as usual:
    88  //
    89  //	// Remember to use "Goroutines" and not "Goroutines()" with Eventually()!
    90  //	Eventually(Goroutines).ShouldNot(HaveLeaked())
    91  //	Eventually(Goroutines).WithTimeout(5 * time.Second).ShouldNot(HaveLeaked())
    92  //
    93  // In its simplest form, an expected non-leaky goroutine can be identified by
    94  // passing the (fully qualified) name (in form of a string) of the topmost
    95  // function in the backtrace. For instance:
    96  //
    97  //	Eventually(Goroutines).ShouldNot(HaveLeaked("foo.bar"))
    98  //
    99  // This is the shorthand equivalent to this explicit form:
   100  //
   101  //	Eventually(Goroutines).ShouldNot(HaveLeaked(IgnoringTopFunction("foo.bar")))
   102  //
   103  // HaveLeak also accepts passing a slice of Goroutine objects to be considered
   104  // non-leaky goroutines.
   105  //
   106  //	snapshot := Goroutines()
   107  //	DoSomething()
   108  //	Eventually(Goroutines).ShouldNot(HaveLeaked(snapshot))
   109  //
   110  // Again, this is shorthand for the following explicit form:
   111  //
   112  //	snapshot := Goroutines()
   113  //	DoSomething()
   114  //	Eventually(Goroutines).ShouldNot(HaveLeaked(IgnoringGoroutines(snapshot)))
   115  //
   116  // Finally, HaveLeaked accepts any GomegaMatcher and will repeatedly pass it a
   117  // Goroutine object: if the matcher succeeds, the Goroutine object in question
   118  // is considered to be non-leaked and thus filtered out. While the following
   119  // built-in Goroutine filter matchers should hopefully cover most situations,
   120  // any suitable GomegaMatcher can be used for tricky leaky Goroutine filtering.
   121  //
   122  //	IgnoringTopFunction("foo.bar")
   123  //	IgnoringTopFunction("foo.bar...")
   124  //	IgnoringTopFunction("foo.bar [chan receive]")
   125  //	IgnoringGoroutines(expectedGoroutines)
   126  //	IgnoringInBacktrace("foo.bar.baz")
   127  func HaveLeaked(ignoring ...interface{}) types.GomegaMatcher {
   128  	m := &HaveLeakedMatcher{filters: standardFilters}
   129  	for _, ign := range ignoring {
   130  		switch ign := ign.(type) {
   131  		case string:
   132  			m.filters = append(m.filters, IgnoringTopFunction(ign))
   133  		case []Goroutine:
   134  			m.filters = append(m.filters, IgnoringGoroutines(ign))
   135  		case types.GomegaMatcher:
   136  			m.filters = append(m.filters, ign)
   137  		default:
   138  			panic(fmt.Sprintf("HaveLeaked expected a string, []Goroutine, or GomegaMatcher, but got:\n%s", format.Object(ign, 1)))
   139  		}
   140  	}
   141  	return m
   142  }
   143  
   144  // HaveLeakedMatcher implements the HaveLeaked Gomega Matcher that succeeds if
   145  // the actual list of goroutines is non-empty after filtering out the expected
   146  // goroutines.
   147  type HaveLeakedMatcher struct {
   148  	filters []types.GomegaMatcher // expected goroutines that aren't leaks.
   149  	leaked  []Goroutine           // surplus goroutines which we consider to be leaks.
   150  }
   151  
   152  var gsT = reflect.TypeOf([]Goroutine{})
   153  
   154  // Match succeeds if actual is an array or slice of Goroutine
   155  // information and still contains goroutines after filtering out all expected
   156  // goroutines that were specified when creating the matcher.
   157  func (matcher *HaveLeakedMatcher) Match(actual interface{}) (success bool, err error) {
   158  	val := reflect.ValueOf(actual)
   159  	switch val.Kind() {
   160  	case reflect.Array, reflect.Slice:
   161  		if !val.Type().AssignableTo(gsT) {
   162  			return false, fmt.Errorf(
   163  				"HaveLeaked matcher expects an array or slice of goroutines.  Got:\n%s",
   164  				format.Object(actual, 1))
   165  		}
   166  	default:
   167  		return false, fmt.Errorf(
   168  			"HaveLeaked matcher expects an array or slice of goroutines.  Got:\n%s",
   169  			format.Object(actual, 1))
   170  	}
   171  	goroutines := val.Convert(gsT).Interface().([]Goroutine)
   172  	matcher.leaked, err = matcher.filter(goroutines, matcher.filters)
   173  	if err != nil {
   174  		return false, err
   175  	}
   176  	if len(matcher.leaked) == 0 {
   177  		return false, nil
   178  	}
   179  	return true, nil // we have leak(ed)
   180  }
   181  
   182  // FailureMessage returns a failure message if there are leaked goroutines.
   183  func (matcher *HaveLeakedMatcher) FailureMessage(actual interface{}) (message string) {
   184  	return fmt.Sprintf("Expected to leak %d goroutines:\n%s", len(matcher.leaked), matcher.listGoroutines(matcher.leaked, 1))
   185  }
   186  
   187  // NegatedFailureMessage returns a negated failure message if there aren't any leaked goroutines.
   188  func (matcher *HaveLeakedMatcher) NegatedFailureMessage(actual interface{}) (message string) {
   189  	return fmt.Sprintf("Expected not to leak %d goroutines:\n%s", len(matcher.leaked), matcher.listGoroutines(matcher.leaked, 1))
   190  }
   191  
   192  // listGoroutines returns a somewhat compact textual representation of the
   193  // specified goroutines, by ignoring the often quite lengthy backtrace
   194  // information.
   195  func (matcher *HaveLeakedMatcher) listGoroutines(gs []Goroutine, indentation uint) string {
   196  	var buff strings.Builder
   197  	indent := strings.Repeat(format.Indent, int(indentation))
   198  	backtraceIdent := strings.Repeat(format.Indent, int(indentation+1))
   199  	for gidx, g := range gs {
   200  		if gidx > 0 {
   201  			buff.WriteRune('\n')
   202  		}
   203  		buff.WriteString(indent)
   204  		buff.WriteString("goroutine ")
   205  		buff.WriteString(strconv.FormatUint(g.ID, 10))
   206  		buff.WriteString(" [")
   207  		buff.WriteString(g.State)
   208  		buff.WriteString("]\n")
   209  
   210  		backtrace := g.Backtrace
   211  		for backtrace != "" {
   212  			buff.WriteString(backtraceIdent)
   213  			// take the next two lines (function name and file name plus line
   214  			// number) and output them as a single indented line.
   215  			nlIdx := strings.IndexRune(backtrace, '\n')
   216  			if nlIdx < 0 {
   217  				// ...a dodgy single line
   218  				buff.WriteString(backtrace)
   219  				break
   220  			}
   221  			calledFuncName := backtrace[:nlIdx]
   222  			// Take care of not mangling the optional "created by " prefix is
   223  			// present, when formatting the location to use either long or
   224  			// shortened filenames and paths.
   225  			location := backtrace[nlIdx+1:]
   226  			nnlIdx := strings.IndexRune(location, '\n')
   227  			if nnlIdx >= 0 {
   228  				backtrace, location = location[nnlIdx+1:], location[:nnlIdx]
   229  			} else {
   230  				backtrace = "" // ...the next location line is missing
   231  			}
   232  			// Don't accidentally strip off the "created by" prefix when
   233  			// shortening the call site location filename...
   234  			location = strings.TrimSpace(location) // strip of indentation
   235  			lineno := ""
   236  			if linenoIdx := strings.LastIndex(location, ":"); linenoIdx >= 0 {
   237  				location, lineno = location[:linenoIdx], location[linenoIdx+1:]
   238  			}
   239  			location = formatFilename(location) + ":" + lineno
   240  			// Add to compact backtrace
   241  			buff.WriteString(calledFuncName)
   242  			buff.WriteString(" at ")
   243  			// Don't output any program counter hex offsets, so strip them out
   244  			// here, if present; well, they should always be present, but better
   245  			// safe than sorry.
   246  			if offsetIdx := strings.LastIndexFunc(location,
   247  				func(r rune) bool { return r == ' ' }); offsetIdx >= 0 {
   248  				buff.WriteString(location[:offsetIdx])
   249  			} else {
   250  				buff.WriteString(location)
   251  			}
   252  			if backtrace != "" {
   253  				buff.WriteRune('\n')
   254  			}
   255  		}
   256  	}
   257  	return buff.String()
   258  }
   259  
   260  // filter returns a list of leaked goroutines by removing all expected
   261  // goroutines from the given list of goroutines, using the specified checkers.
   262  // The calling goroutine is always filtered out automatically. A checker checks
   263  // if a certain goroutine is expected (then it gets filtered out), or not. If
   264  // all checkers do not signal that they expect a certain goroutine then this
   265  // goroutine is considered to be a leak.
   266  func (matcher *HaveLeakedMatcher) filter(
   267  	goroutines []Goroutine, filters []types.GomegaMatcher,
   268  ) ([]Goroutine, error) {
   269  	gs := make([]Goroutine, 0, len(goroutines))
   270  	myID := goroutine.Current().ID
   271  nextgoroutine:
   272  	for _, g := range goroutines {
   273  		if g.ID == myID {
   274  			continue
   275  		}
   276  		for _, filter := range filters {
   277  			matches, err := filter.Match(g)
   278  			if err != nil {
   279  				return nil, err
   280  			}
   281  			if matches {
   282  				continue nextgoroutine
   283  			}
   284  		}
   285  		gs = append(gs, g)
   286  	}
   287  	return gs, nil
   288  }
   289  
   290  // formatFilename takes the ReportFilenameWithPath setting into account to
   291  // either return the full specified filename with a path or alternatively
   292  // shortening it to contain only the package name and the filename, but not the
   293  // full path.
   294  func formatFilename(filename string) string {
   295  	if ReportFilenameWithPath {
   296  		return filename
   297  	}
   298  	dir := filepath.Dir(filename)
   299  	pkg := filepath.Base(dir)
   300  	switch pkg {
   301  	case ".", "..", "/", "\\":
   302  		pkg = ""
   303  	}
   304  	// Go dumps stacks always with file locations containing forward slashes,
   305  	// even on Windows. Thus, we do NOT use filepath.Join here, but instead
   306  	// path.Join in order to keep with using forward slashes.
   307  	return path.Join(pkg, filepath.ToSlash(filepath.Base(filename)))
   308  }
   309  

View as plain text