github.com/onsi/gomega@v1.32.0/gleak/have_leaked_matcher.go (about) 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 }