Add RegisterIgnoreGoroutine to leakcheck package (#1507)

diff --git a/test/leakcheck/leakcheck.go b/test/leakcheck/leakcheck.go
index 8414354..76f9fc5 100644
--- a/test/leakcheck/leakcheck.go
+++ b/test/leakcheck/leakcheck.go
@@ -28,39 +28,61 @@
 	"time"
 )
 
+var goroutinesToIgnore = []string{
+	"testing.Main(",
+	"testing.tRunner(",
+	"testing.(*M).",
+	"runtime.goexit",
+	"created by runtime.gc",
+	"created by runtime/trace.Start",
+	"interestingGoroutines",
+	"runtime.MHeap_Scavenger",
+	"signal.signal_recv",
+	"sigterm.handler",
+	"runtime_mcall",
+	"(*loggingT).flushDaemon",
+	"goroutine in C code",
+}
+
+// RegisterIgnoreGoroutine appends s into the ignore goroutine list. The
+// goroutines whose stack trace contains s will not be identified as leaked
+// goroutines. Not thread-safe, only call this function in init().
+func RegisterIgnoreGoroutine(s string) {
+	goroutinesToIgnore = append(goroutinesToIgnore, s)
+}
+
+func ignore(g string) bool {
+	sl := strings.SplitN(g, "\n", 2)
+	if len(sl) != 2 {
+		return true
+	}
+	stack := strings.TrimSpace(sl[1])
+	if strings.HasPrefix(stack, "testing.RunTests") {
+		return true
+	}
+
+	if stack == "" {
+		return true
+	}
+
+	for _, s := range goroutinesToIgnore {
+		if strings.Contains(stack, s) {
+			return true
+		}
+	}
+
+	return false
+}
+
 // interestingGoroutines returns all goroutines we care about for the purpose of
 // leak checking. It excludes testing or runtime ones.
 func interestingGoroutines() (gs []string) {
 	buf := make([]byte, 2<<20)
 	buf = buf[:runtime.Stack(buf, true)]
 	for _, g := range strings.Split(string(buf), "\n\n") {
-		sl := strings.SplitN(g, "\n", 2)
-		if len(sl) != 2 {
-			continue
+		if !ignore(g) {
+			gs = append(gs, g)
 		}
-		stack := strings.TrimSpace(sl[1])
-		if strings.HasPrefix(stack, "testing.RunTests") {
-			continue
-		}
-
-		if stack == "" ||
-			strings.Contains(stack, "testing.Main(") ||
-			strings.Contains(stack, "testing.tRunner(") ||
-			strings.Contains(stack, "testing.(*M).") ||
-			strings.Contains(stack, "runtime.goexit") ||
-			strings.Contains(stack, "created by runtime.gc") ||
-			strings.Contains(stack, "created by runtime/trace.Start") ||
-			strings.Contains(stack, "created by google3/base/go/log.init") ||
-			strings.Contains(stack, "interestingGoroutines") ||
-			strings.Contains(stack, "runtime.MHeap_Scavenger") ||
-			strings.Contains(stack, "signal.signal_recv") ||
-			strings.Contains(stack, "sigterm.handler") ||
-			strings.Contains(stack, "runtime_mcall") ||
-			strings.Contains(stack, "(*loggingT).flushDaemon") ||
-			strings.Contains(stack, "goroutine in C code") {
-			continue
-		}
-		gs = append(gs, g)
 	}
 	sort.Strings(gs)
 	return
diff --git a/test/leakcheck/leakcheck_test.go b/test/leakcheck/leakcheck_test.go
index 9c8cc15..50927e9 100644
--- a/test/leakcheck/leakcheck_test.go
+++ b/test/leakcheck/leakcheck_test.go
@@ -19,15 +19,19 @@
 package leakcheck
 
 import (
+	"fmt"
+	"strings"
 	"testing"
 	"time"
 )
 
 type testErrorfer struct {
 	errorCount int
+	errors     []string
 }
 
 func (e *testErrorfer) Errorf(format string, args ...interface{}) {
+	e.errors = append(e.errors, fmt.Sprintf(format, args...))
 	e.errorCount++
 }
 
@@ -43,6 +47,30 @@
 	check(e, time.Second)
 	if e.errorCount != leakCount {
 		t.Errorf("check found %v leaks, want %v leaks", e.errorCount, leakCount)
+		t.Logf("leaked goroutines:\n%v", strings.Join(e.errors, "\n"))
+	}
+	check(t, 3*time.Second)
+}
+
+func ignoredTestingLeak(d time.Duration) {
+	time.Sleep(d)
+}
+
+func TestCheckRegisterIgnore(t *testing.T) {
+	RegisterIgnoreGoroutine("ignoredTestingLeak")
+	const leakCount = 3
+	for i := 0; i < leakCount; i++ {
+		go func() { time.Sleep(2 * time.Second) }()
+	}
+	go func() { ignoredTestingLeak(3 * time.Second) }()
+	if ig := interestingGoroutines(); len(ig) == 0 {
+		t.Error("blah")
+	}
+	e := &testErrorfer{}
+	check(e, time.Second)
+	if e.errorCount != leakCount {
+		t.Errorf("check found %v leaks, want %v leaks", e.errorCount, leakCount)
+		t.Logf("leaked goroutines:\n%v", strings.Join(e.errors, "\n"))
 	}
 	check(t, 3*time.Second)
 }