gocore: break up test to make it more understandable

Also, remove unused test code and add common elements between the test
programs to testenv.

Change-Id: Ib5c8e2d1768016df54f3212d79ec9542492367dd
Reviewed-on: https://go-review.googlesource.com/c/debug/+/659795
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Nicolas Hillegeer <aktau@google.com>
Auto-Submit: Michael Knyszek <mknyszek@google.com>
diff --git a/internal/gocore/gocore_test.go b/internal/gocore/gocore_test.go
index 4ab0704..ffb9bd4 100644
--- a/internal/gocore/gocore_test.go
+++ b/internal/gocore/gocore_test.go
@@ -1,5 +1,5 @@
 // Copyright 2017 The Go Authors.  All rights reserved.
-// Use of this source code is governed by a BSD-style
+// Use of this srcFile code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
 //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
@@ -36,9 +36,8 @@
 	return p
 }
 
-// loadExampleGenerated generates a core from a binary built with
-// runtime.GOROOT().
-func loadExampleGenerated(t *testing.T, buildFlags, env []string) *Process {
+// createAndLoadCore generates a core from a binary built with runtime.GOROOT().
+func createAndLoadCore(t *testing.T, srcFile string, buildFlags, env []string) *Process {
 	t.Helper()
 	testenv.MustHaveGoBuild(t)
 	switch runtime.GOOS {
@@ -57,7 +56,7 @@
 	}
 
 	dir := t.TempDir()
-	file, output, err := generateCore(dir, buildFlags, env)
+	file, output, err := generateCore(srcFile, dir, buildFlags, env)
 	t.Logf("crasher output: %s", output)
 	if err != nil {
 		t.Fatalf("generateCore() got err %v want nil", err)
@@ -158,24 +157,23 @@
 	return cmd.Process.Pid, b.Bytes(), nil
 }
 
-func generateCore(dir string, buildFlags, env []string) (string, []byte, error) {
+func generateCore(srcFile, dir string, buildFlags, env []string) (string, []byte, error) {
 	goTool, err := testenv.GoTool()
 	if err != nil {
 		return "", nil, fmt.Errorf("cannot find go tool: %w", err)
 	}
 
-	const source = "./testdata/coretest/test.go"
 	cwd, err := os.Getwd()
 	if err != nil {
 		return "", nil, fmt.Errorf("erroring getting cwd: %w", err)
 	}
 
-	srcPath := filepath.Join(cwd, source)
+	srcPath := filepath.Join(cwd, srcFile)
 	argv := []string{"build"}
 	argv = append(argv, buildFlags...)
-	argv = append(argv, "-o", "test.exe", srcPath)
+	argv = append(argv, "-o", filepath.Join(dir, "test.exe"), "./"+filepath.Base(srcFile))
 	cmd := exec.Command(goTool, argv...)
-	cmd.Dir = dir
+	cmd.Dir = filepath.Dir(srcPath)
 
 	b, err := cmd.CombinedOutput()
 	if err != nil {
@@ -241,7 +239,7 @@
 	if len(p.env) != 0 {
 		parts = append(parts, "env="+strings.Join(p.env, ","))
 	}
-	return cmp.Or(strings.Join(parts, "%"), "DEFAULT")
+	return cmp.Or(strings.Join(parts, "%"), "default")
 }
 
 // Variations in build and execution environments common to different tests.
@@ -251,77 +249,122 @@
 	{buildFlags: []string{"-buildmode=pie"}, env: []string{"GO_DEBUG_TEST_COREDUMP_FILTER=0x3f"}},
 }
 
+func testSrcFiles(t *testing.T) []string {
+	srcs, err := filepath.Glob("testdata/testprogs/*.go")
+	if err != nil {
+		t.Skipf("failed to find sources: %v", err)
+	}
+	return srcs
+}
+
 func TestVersions(t *testing.T) {
 	t.Run("goroot", func(t *testing.T) {
 		for _, test := range variations {
-			t.Run(test.String(), func(t *testing.T) {
-				p := loadExampleGenerated(t, test.buildFlags, test.env)
-				checkProcess(t, p)
-			})
+			for _, src := range testSrcFiles(t) {
+				t.Run(test.String()+"/"+filepath.Base(src), func(t *testing.T) {
+					p := createAndLoadCore(t, src, test.buildFlags, test.env)
+					checkProcess(t, p)
+				})
+			}
 		}
 	})
 }
 
 func TestObjects(t *testing.T) {
+	const largeObjectThreshold = 32768
+
 	t.Run("goroot", func(t *testing.T) {
 		for _, test := range variations {
 			t.Run(test.String(), func(t *testing.T) {
-				const largeObjectThreshold = 32768
+				t.Run("bigslice.go", func(t *testing.T) {
+					p := createAndLoadCore(t, "testdata/testprogs/bigslice.go", test.buildFlags, test.env)
 
-				p := loadExampleGenerated(t, test.buildFlags, test.env)
+					// Statistics to check.
+					largeObjects := 0 // Number of objects larger than (or equal to largeObjectThreshold)
+					bigSliceElemObjects := 0
 
-				// Statistics to check.
-				n := 0
-				largeObjects := 0 // Number of objects larger than (or equal to largeObjectThreshold)
-				myPairObjects := 0
-				anyNodeObjects := 0
-				typeSafeNodeObjects := 0
-				bigSliceElemObjects := 0
-
-				p.ForEachObject(func(x Object) bool {
-					siz := p.Size(x)
-					typ := typeName(p, x)
-					t.Logf("%s size=%d", typ, p.Size(x))
-					if siz >= largeObjectThreshold {
-						largeObjects++
+					p.ForEachObject(func(x Object) bool {
+						siz := p.Size(x)
+						typ := typeName(p, x)
+						//t.Logf("%s size=%d", typ, p.Size(x))
+						if siz >= largeObjectThreshold {
+							largeObjects++
+						}
+						switch typ {
+						case "main.bigSliceElem":
+							bigSliceElemObjects++
+						}
+						return true
+					})
+					if largeObjects != 1 {
+						t.Errorf("expected exactly one object larger than %d, found %d", largeObjectThreshold, largeObjects)
 					}
-					switch typ {
-					case "main.myPair":
-						myPairObjects++
-					case "main.anyNode":
-						anyNodeObjects++
-					case "main.typeSafeNode[main.myPair]":
-						typeSafeNodeObjects++
-					case "main.bigSliceElem":
-						bigSliceElemObjects++
+
+					// Check object counts.
+					if want := 32 << 10; bigSliceElemObjects != want {
+						t.Errorf("expected exactly %d main.globalBigSliceInt objects, found %d", want, bigSliceElemObjects)
 					}
-					n++
-					return true
 				})
-				if n < 10 {
-					t.Errorf("#objects = %d, want >10", n)
-				}
-				if largeObjects != 2 {
-					t.Errorf("expected exactly one object larger than %d, found %d", largeObjectThreshold, largeObjects)
-				}
+				t.Run("large.go", func(t *testing.T) {
+					p := createAndLoadCore(t, "testdata/testprogs/large.go", test.buildFlags, test.env)
 
-				// Check object counts.
-				const depth = 5
-				const tsTrees = 3
-				const anTrees = 2
-				const nodes = 1<<depth - 1
-				if want := tsTrees*nodes + anTrees*nodes*2; myPairObjects != want {
-					t.Errorf("expected exactly %d main.myPair objects, found %d", want, myPairObjects)
-				}
-				if want := anTrees * nodes; anyNodeObjects != want {
-					t.Errorf("expected exactly %d main.anyNode objects, found %d", want, anyNodeObjects)
-				}
-				if want := tsTrees * nodes; typeSafeNodeObjects != want {
-					t.Errorf("expected exactly %d main.typeSafeNode[main.myPair] objects, found %d", want, typeSafeNodeObjects)
-				}
-				if want := 32 << 10; bigSliceElemObjects != want {
-					t.Errorf("expected exactly %d main.globalBigSliceInt objects, found %d", want, bigSliceElemObjects)
-				}
+					// Statistics to check.
+					largeObjects := 0 // Number of objects larger than (or equal to largeObjectThreshold)
+					p.ForEachObject(func(x Object) bool {
+						siz := p.Size(x)
+						//typ := typeName(p, x)
+						//t.Logf("%s size=%d", typ, p.Size(x))
+						if siz >= largeObjectThreshold {
+							largeObjects++
+						}
+						return true
+					})
+					if largeObjects != 1 {
+						t.Errorf("expected exactly one object larger than %d, found %d", largeObjectThreshold, largeObjects)
+					}
+				})
+				t.Run("trees.go", func(t *testing.T) {
+					p := createAndLoadCore(t, "testdata/testprogs/trees.go", test.buildFlags, test.env)
+
+					// Statistics to check.
+					n := 0
+					myPairObjects := 0
+					anyNodeObjects := 0
+					typeSafeNodeObjects := 0
+
+					p.ForEachObject(func(x Object) bool {
+						typ := typeName(p, x)
+						//t.Logf("%s size=%d", typ, p.Size(x))
+						switch typ {
+						case "main.myPair":
+							myPairObjects++
+						case "main.anyNode":
+							anyNodeObjects++
+						case "main.typeSafeNode[main.myPair]":
+							typeSafeNodeObjects++
+						}
+						n++
+						return true
+					})
+					if n < 10 {
+						t.Errorf("#objects = %d, want >10", n)
+					}
+
+					// Check object counts.
+					const depth = 5
+					const tsTrees = 3
+					const anTrees = 2
+					const nodes = 1<<depth - 1
+					if want := tsTrees*nodes + anTrees*nodes*2; myPairObjects != want {
+						t.Errorf("expected exactly %d main.myPair objects, found %d", want, myPairObjects)
+					}
+					if want := anTrees * nodes; anyNodeObjects != want {
+						t.Errorf("expected exactly %d main.anyNode objects, found %d", want, anyNodeObjects)
+					}
+					if want := tsTrees * nodes; typeSafeNodeObjects != want {
+						t.Errorf("expected exactly %d main.typeSafeNode[main.myPair] objects, found %d", want, typeSafeNodeObjects)
+					}
+				})
 			})
 		}
 	})
diff --git a/internal/gocore/testdata/README b/internal/gocore/testdata/README
deleted file mode 100644
index 7fa4d21..0000000
--- a/internal/gocore/testdata/README
+++ /dev/null
@@ -1,44 +0,0 @@
-This directory contains a simple core file for use in testing.
-
-It was generated by running the following program with go1.9.0.
-
-package main
-
-func main() {
-	_ = *(*int)(nil)
-}
-
-The core file includes the executable by reference using an absolute
-path.  The executable was at /tmp/test, so that path is reproduced
-here so the core dump reader can find the executable using this
-testdata directory as the base directory.
-
-core1.10 is produced in the same way, except using go1.10.0.
-core1.11 is produced in the same way, except using go1.11.0.
-
-
-steps for subsequent versions:
-
-mkdir /tmp/coretest
-rm -fr /tmp/coretest/*   # in case there's old junk there
-cp coretest/test.go /tmp/coretest
-cd /tmp/coretest
-go build test.go
-GOTRACEBACK=crash ./test
-zip 1.12.zip /tmp/coretest/core /tmp/coretest/test  // use your version number
-
-Then move the 1.12.zip to this directory.
-Add a new test to TestVersions in ../gocore_test.go.
-
-## runtimetype
-
-This directory also contains the source code to generate the runtimetype core,
-which is used to test getting runtime type for a interface.
-
-steps for generate runtimetype core:
-
-cd testdata/runtimetype
-go build -o runtimetype .
-ulimit -c unlimited
-GOTRACEBACK=crash ./runtimetype
-zip runtimetype.zip runtimetype core
diff --git a/internal/gocore/testdata/coretest/test.go b/internal/gocore/testdata/coretest/test.go
deleted file mode 100644
index d217112..0000000
--- a/internal/gocore/testdata/coretest/test.go
+++ /dev/null
@@ -1,197 +0,0 @@
-package main
-
-import (
-	"os"
-	"runtime"
-)
-
-// Large is an object that (since Go 1.22) is allocated in a span that has a
-// non-nil largeType field. Meaning it must be (>maxSmallSize-mallocHeaderSize).
-// At the time of writing this is (32768 - 8).
-type Large struct {
-	ptr *uint8 // Object must contain a pointer to trigger code path.
-	arr [32768 - 8]uint8
-}
-
-func useLarge(o *Large, ready chan<- struct{}) {
-	o.ptr = &o.arr[5]
-	o.arr[5] = 0xCA
-	ready <- struct{}{}
-	<-block
-}
-
-type AnyTree struct {
-	root any
-}
-
-type anyNode struct {
-	left, right any // *anyNode, to test direct eface type walking
-	entry1      any // myPair, to test indirect eface type walking
-	entry2      Xer // myPair, to test indirect iface type walking
-}
-
-func makeAnyTree(depth int64) any {
-	if depth == 0 {
-		return nil
-	}
-	e1 := myPair{depth, depth}
-	e2 := myPair{depth, depth}
-	n := &anyNode{
-		left:   makeAnyTree(depth - 1),
-		right:  makeAnyTree(depth - 1),
-		entry1: e1,
-		entry2: e2,
-	}
-	if depth%2 == 0 {
-		// Test dealing with direct interfaces of wrapped structures.
-		return anyNodeWrap2{{n}}
-	}
-	return n
-}
-
-//go:noinline
-func (a *AnyTree) count() int {
-	return countAnyNode(a.root)
-}
-
-func countAnyNode(a any) int {
-	switch v := a.(type) {
-	case *anyNode:
-		return v.count()
-	case anyNodeWrap2:
-		return v.unwrap().count()
-	}
-	return 0
-}
-
-// This is load-bearing to make sure anyNodeWrap2 ends up in the binary.
-//
-//go:noinline
-func (w anyNodeWrap2) unwrap() *anyNode {
-	return w[0].anyNode
-}
-
-func (a *anyNode) count() int {
-	return 1 + countAnyNode(a.left) + countAnyNode(a.right)
-}
-
-type anyNodeWrap struct{ *anyNode }
-type anyNodeWrap2 [1]anyNodeWrap
-
-type TypeSafeTree[K any] struct {
-	root *typeSafeNode[K]
-}
-
-type typeSafeNode[K any] struct {
-	left, right *typeSafeNode[K]
-	entry       *K
-}
-
-func makeTypeSafeTree(depth int64) *typeSafeNode[myPair] {
-	if depth == 0 {
-		return nil
-	}
-	return &typeSafeNode[myPair]{
-		left:  makeTypeSafeTree(depth - 1),
-		right: makeTypeSafeTree(depth - 1),
-		entry: &myPair{depth, depth},
-	}
-}
-
-//go:noinline
-func (t *TypeSafeTree[K]) count() int {
-	return t.root.count()
-}
-
-func (t *typeSafeNode[K]) count() int {
-	if t == nil {
-		return 0
-	}
-	return 1 + t.left.count() + t.right.count()
-}
-
-type myPair struct {
-	x, y int64
-}
-
-func (p myPair) X() int64 {
-	return p.x
-}
-
-type Xer interface {
-	X() int64
-}
-
-type bigSliceElem struct {
-	x, y, z float64
-}
-
-var globalAnyTree AnyTree
-var globalAnyTreeFM func() int
-var globalTypeSafeTree TypeSafeTree[myPair]
-var globalTypeSafeTreeFM func() int
-var globalBigSlice []*bigSliceElem
-
-var block = make(chan struct{})
-
-var a anyNode
-
-func init() {
-	if coredumpFilter := os.Getenv("GO_DEBUG_TEST_COREDUMP_FILTER"); coredumpFilter != "" {
-		if err := os.WriteFile("/proc/self/coredump_filter", []byte(coredumpFilter), 0600); err != nil {
-			os.Stderr.WriteString("crash: unable to set coredump_filter: ")
-			os.Stderr.WriteString(err.Error())
-			os.Stderr.WriteString("\n")
-			os.Exit(0) // Don't crash (which is an error for the called).
-		}
-	}
-}
-
-func main() {
-	globalBigSlice = make([]*bigSliceElem, 32<<10)
-	for i := range globalBigSlice {
-		globalBigSlice[i] = &bigSliceElem{float64(i), float64(i) - 0.5, float64(i * 124)}
-	}
-	globalAnyTree.root = makeAnyTree(5)
-	globalTypeSafeTree.root = makeTypeSafeTree(5)
-
-	ready := make(chan struct{})
-	go func() {
-		var anyTree AnyTree
-		var anyTreeFM AnyTree // Captured in a method value.
-		var typeSafeTree TypeSafeTree[myPair]
-		var typeSafeTreeFM TypeSafeTree[myPair] // Captured in a method value.
-
-		// TODO(mknyszek): AnyTree is described in DWARF in pieces, and we can't handle
-		// that yet.
-		//
-		// anyTree.root = makeAnyTree(5)
-		anyTreeFM.root = makeAnyTree(5)
-		globalAnyTreeFM = anyTreeFM.count
-		typeSafeTree.root = makeTypeSafeTree(5)
-		typeSafeTreeFM.root = makeTypeSafeTree(5)
-		globalTypeSafeTreeFM = typeSafeTreeFM.count
-
-		ready <- struct{}{}
-		<-block
-
-		runtime.KeepAlive(anyTree)
-		runtime.KeepAlive(typeSafeTree)
-	}()
-
-	// Create a large value and reference
-	var o Large
-	go useLarge(&o, ready) // Force an escape of o.
-	o.arr[14] = 0xDE       // Prevent a future smart compiler from allocating o directly on useLarge's stack.
-
-	// This is load-bearing to make sure anyNodeWrap2 and the count methods end up in the DWARF.
-	println("tree counts:", globalAnyTree.count(), globalTypeSafeTree.count())
-
-	// Make sure both goroutines are ready.
-	<-ready
-	<-ready
-
-	_ = *(*int)(nil)
-
-	runtime.KeepAlive(&o)
-}
diff --git a/internal/gocore/testdata/runtimetype/go.mod b/internal/gocore/testdata/runtimetype/go.mod
deleted file mode 100644
index 594b4df..0000000
--- a/internal/gocore/testdata/runtimetype/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module example.com/m
-
-go 1.17
diff --git a/internal/gocore/testdata/runtimetype/main.go b/internal/gocore/testdata/runtimetype/main.go
deleted file mode 100644
index df647f9..0000000
--- a/internal/gocore/testdata/runtimetype/main.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package main
-
-import (
-	pkga "example.com/m/path-a/pkg"
-	pkgb "example.com/m/path-b/pkg"
-)
-
-var global []interface{}
-
-func main() {
-	global = append(global, pkga.NewIfaceDirect(), pkga.NewIfaceInDirect(), pkgb.NewIfaceDirect(), pkgb.NewIfaceInDirect())
-
-	_ = *(*int)(nil)
-}
diff --git a/internal/gocore/testdata/runtimetype/path-a/pkg/pkg.go b/internal/gocore/testdata/runtimetype/path-a/pkg/pkg.go
deleted file mode 100644
index e0dfa4f..0000000
--- a/internal/gocore/testdata/runtimetype/path-a/pkg/pkg.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package pkg
-
-type T1 struct {
-	f1 uint64
-	f2 uint64
-}
-
-type T2 struct {
-	f1 uint64
-}
-
-type IfaceDirect interface {
-	M1()
-}
-
-type IfaceInDirect interface {
-	M2()
-}
-
-func (t *T1) M1() {
-}
-
-func (t T2) M2() {
-}
-
-func NewIfaceDirect() IfaceDirect {
-	return &T1{100, 10}
-}
-
-func NewIfaceInDirect() IfaceInDirect {
-	return T2{200}
-}
diff --git a/internal/gocore/testdata/runtimetype/path-b/pkg/pkg.go b/internal/gocore/testdata/runtimetype/path-b/pkg/pkg.go
deleted file mode 100644
index f31a5ff..0000000
--- a/internal/gocore/testdata/runtimetype/path-b/pkg/pkg.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package pkg
-
-type T1 struct {
-	f1 uint64
-	f2 uint64
-	f3 uint64
-	f4 uint64
-}
-
-type T2 struct {
-	f1 uint64
-	f2 uint64
-	f3 uint64
-}
-
-type IfaceDirect interface {
-	M1()
-}
-
-type IfaceInDirect interface {
-	M2()
-}
-
-func (t *T1) M1() {
-}
-
-func (t T2) M2() {
-}
-
-func NewIfaceDirect() IfaceDirect {
-	return &T1{1000, 0, 0, 0}
-}
-
-func NewIfaceInDirect() IfaceInDirect {
-	return T2{2000, 0, 0}
-}
diff --git a/internal/gocore/testdata/testprogs/bigslice.go b/internal/gocore/testdata/testprogs/bigslice.go
new file mode 100644
index 0000000..d383fea
--- /dev/null
+++ b/internal/gocore/testdata/testprogs/bigslice.go
@@ -0,0 +1,31 @@
+// Copyright 2025 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build ignore
+
+// Tests to make sure pointers in big slices are handled correctly.
+
+package main
+
+import (
+	"os"
+
+	"golang.org/x/debug/internal/testenv"
+)
+
+type bigSliceElem struct {
+	x, y, z float64
+}
+
+var globalBigSlice []*bigSliceElem
+
+func main() {
+	testenv.RunThenCrash(os.Getenv("GO_DEBUG_TEST_COREDUMP_FILTER"), func() any {
+		globalBigSlice = make([]*bigSliceElem, 32<<10)
+		for i := range globalBigSlice {
+			globalBigSlice[i] = &bigSliceElem{float64(i), float64(i) - 0.5, float64(i * 124)}
+		}
+		return nil
+	})
+}
diff --git a/internal/gocore/testdata/testprogs/large.go b/internal/gocore/testdata/testprogs/large.go
new file mode 100644
index 0000000..a37078d
--- /dev/null
+++ b/internal/gocore/testdata/testprogs/large.go
@@ -0,0 +1,47 @@
+// Copyright 2025 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build ignore
+
+// Tests to make sure a large object referenced only from a stack
+// can be found.
+
+package main
+
+import (
+	"os"
+
+	"golang.org/x/debug/internal/testenv"
+)
+
+// Large is an object that (since Go 1.22) is allocated in a span that has a
+// non-nil largeType field. Meaning it must be (>maxSmallSize-mallocHeaderSize).
+// At the time of writing this is (32768 - 8).
+type Large struct {
+	ptr *uint8 // Object must contain a pointer to trigger code path.
+	arr [32768 - 8]uint8
+}
+
+func useLarge(o *Large, ready chan<- struct{}) {
+	o.ptr = &o.arr[5]
+	o.arr[5] = 0xCA
+	ready <- struct{}{}
+	<-block
+}
+
+var block = make(chan struct{})
+
+func main() {
+	testenv.RunThenCrash(os.Getenv("GO_DEBUG_TEST_COREDUMP_FILTER"), func() any {
+		ready := make(chan struct{})
+
+		// Create a large value and reference
+		var o Large
+		go useLarge(&o, ready) // Force an escape of o.
+		o.arr[14] = 0xDE       // Prevent a future smart compiler from allocating o directly on useLarge's stack.
+
+		<-ready
+		return &o
+	})
+}
diff --git a/internal/gocore/testdata/testprogs/trees.go b/internal/gocore/testdata/testprogs/trees.go
new file mode 100644
index 0000000..8cfc986
--- /dev/null
+++ b/internal/gocore/testdata/testprogs/trees.go
@@ -0,0 +1,161 @@
+// Copyright 2025 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build ignore
+
+package main
+
+import (
+	"os"
+	"runtime"
+
+	"golang.org/x/debug/internal/testenv"
+)
+
+type AnyTree struct {
+	root any
+}
+
+type anyNode struct {
+	left, right any // *anyNode, to test direct eface type walking
+	entry1      any // myPair, to test indirect eface type walking
+	entry2      Xer // myPair, to test indirect iface type walking
+}
+
+func makeAnyTree(depth int64) any {
+	if depth == 0 {
+		return nil
+	}
+	e1 := myPair{depth, depth}
+	e2 := myPair{depth, depth}
+	n := &anyNode{
+		left:   makeAnyTree(depth - 1),
+		right:  makeAnyTree(depth - 1),
+		entry1: e1,
+		entry2: e2,
+	}
+	if depth%2 == 0 {
+		// Test dealing with direct interfaces of wrapped structures.
+		return anyNodeWrap2{{n}}
+	}
+	return n
+}
+
+//go:noinline
+func (a *AnyTree) count() int {
+	return countAnyNode(a.root)
+}
+
+func countAnyNode(a any) int {
+	switch v := a.(type) {
+	case *anyNode:
+		return v.count()
+	case anyNodeWrap2:
+		return v.unwrap().count()
+	}
+	return 0
+}
+
+// This is load-bearing to make sure anyNodeWrap2 ends up in the binary.
+//
+//go:noinline
+func (w anyNodeWrap2) unwrap() *anyNode {
+	return w[0].anyNode
+}
+
+func (a *anyNode) count() int {
+	return 1 + countAnyNode(a.left) + countAnyNode(a.right)
+}
+
+type anyNodeWrap struct{ *anyNode }
+type anyNodeWrap2 [1]anyNodeWrap
+
+type TypeSafeTree[K any] struct {
+	root *typeSafeNode[K]
+}
+
+type typeSafeNode[K any] struct {
+	left, right *typeSafeNode[K]
+	entry       *K
+}
+
+func makeTypeSafeTree(depth int64) *typeSafeNode[myPair] {
+	if depth == 0 {
+		return nil
+	}
+	return &typeSafeNode[myPair]{
+		left:  makeTypeSafeTree(depth - 1),
+		right: makeTypeSafeTree(depth - 1),
+		entry: &myPair{depth, depth},
+	}
+}
+
+//go:noinline
+func (t *TypeSafeTree[K]) count() int {
+	return t.root.count()
+}
+
+func (t *typeSafeNode[K]) count() int {
+	if t == nil {
+		return 0
+	}
+	return 1 + t.left.count() + t.right.count()
+}
+
+type myPair struct {
+	x, y int64
+}
+
+func (p myPair) X() int64 {
+	return p.x
+}
+
+type Xer interface {
+	X() int64
+}
+
+var globalAnyTree AnyTree
+var globalAnyTreeFM func() int
+var globalTypeSafeTree TypeSafeTree[myPair]
+var globalTypeSafeTreeFM func() int
+
+var block = make(chan struct{})
+var a anyNode
+
+func main() {
+	testenv.RunThenCrash(os.Getenv("GO_DEBUG_TEST_COREDUMP_FILTER"), func() any {
+		globalAnyTree.root = makeAnyTree(5)
+		globalTypeSafeTree.root = makeTypeSafeTree(5)
+
+		ready := make(chan struct{})
+		go func() {
+			var anyTree AnyTree
+			var anyTreeFM AnyTree // Captured in a method value.
+			var typeSafeTree TypeSafeTree[myPair]
+			var typeSafeTreeFM TypeSafeTree[myPair] // Captured in a method value.
+
+			// TODO(mknyszek): AnyTree is described in DWARF in pieces, and we can't handle
+			// that yet.
+			//
+			// anyTree.root = makeAnyTree(5)
+			anyTreeFM.root = makeAnyTree(5)
+			globalAnyTreeFM = anyTreeFM.count
+			typeSafeTree.root = makeTypeSafeTree(5)
+			typeSafeTreeFM.root = makeTypeSafeTree(5)
+			globalTypeSafeTreeFM = typeSafeTreeFM.count
+
+			ready <- struct{}{}
+			<-block
+
+			runtime.KeepAlive(anyTree)
+			runtime.KeepAlive(typeSafeTree)
+		}()
+
+		// This is load-bearing to make sure anyNodeWrap2 and the count methods end up in the DWARF.
+		println("tree counts:", globalAnyTree.count(), globalTypeSafeTree.count())
+
+		<-ready
+		return nil
+	})
+}
diff --git a/internal/testenv/crash.go b/internal/testenv/crash.go
new file mode 100644
index 0000000..4a775be
--- /dev/null
+++ b/internal/testenv/crash.go
@@ -0,0 +1,41 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package testenv
+
+import (
+	"os"
+	"runtime"
+)
+
+// RunThenCrash sets the provided core dump filter (optional) for
+// the process, runs f, then crashes.
+//
+// The slice returned by f is kept alive across the crash.
+func RunThenCrash(coredumpFilter string, f func() any) {
+	// Set coredump filter (Linux only).
+	if runtime.GOOS == "linux" && coredumpFilter != "" {
+		if err := os.WriteFile("/proc/self/coredump_filter", []byte(coredumpFilter), 0600); err != nil {
+			os.Stderr.WriteString("crash: unable to set coredump_filter: ")
+			os.Stderr.WriteString(err.Error())
+			os.Stderr.WriteString("\n")
+			os.Exit(0) // Don't crash (which is an error for the called).
+		}
+	}
+
+	// Run f.
+	result := f()
+	crash()
+	runtime.KeepAlive(result)
+}
+
+// Crash crashes the program.
+//
+// Make it noinline so registers are spilled before entering, otherwise imprecise DWARF will be our doom.
+// Delve has trouble with this too; 'result' in RunThenCrash won't be visible otherwise.
+//
+//go:noinline
+func crash() {
+	_ = *(*int)(nil)
+}