// Copyright 2018 Marc-Antoine Ruel. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.

package stack

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/maruel/panicparse/v2/internal/internaltest"
)

func TestScanSnapshotErr(t *testing.T) {
	t.Parallel()
	data := []*Opts{
		nil,
		{LocalGOROOT: "\\"},
		{LocalGOPATHs: []string{"\\"}},
	}
	for _, opts := range data {
		if _, _, err := ScanSnapshot(&bytes.Buffer{}, ioutil.Discard, opts); err == nil {
			t.Fatal("expected error")
		}
	}
}

func TestScanSnapshotSynthetic(t *testing.T) {
	t.Parallel()
	data := []struct {
		name   string
		in     []string
		prefix string
		suffix string
		err    error
		want   []*Goroutine
	}{
		{
			name: "Nothing",
			err:  io.EOF,
		},
		{
			name:   "NothingEmpty",
			in:     make([]string, 111),
			prefix: strings.Repeat("\n", 110),
			err:    io.EOF,
		},
		{
			name:   "NothingLong",
			in:     []string{strings.Repeat("a", bufio.MaxScanTokenSize+10)},
			prefix: strings.Repeat("a", bufio.MaxScanTokenSize+10),
			err:    io.EOF,
		},

		// One call from main, one from stdlib, one from third party.
		// Create a long first line that will be ignored. It is to guard against
		// https://github.com/maruel/panicparse/issues/17.
		{
			name: "long,main,stdlib,third",
			in: []string{
				strings.Repeat("a", bufio.MaxScanTokenSize+1),
				"panic: reflect.Set: value of type",
				"",
				"goroutine 1 [running]:",
				"github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek()",
				" ??:0 +0x6d",
				"gopkg.in/yaml%2ev2.handleErr(0x433b20)",
				"\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6",
				"reflect.Value.assignTo(0x570860, 0x803f3e0, 0x15)",
				"\t/goroot/src/reflect/value.go:2125 +0x368",
				"main.main()",
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:428 +0x27",
				"",
			},
			prefix: strings.Repeat("a", bufio.MaxScanTokenSize+1) + "\npanic: reflect.Set: value of type\n\n",
			err:    io.EOF,
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						Stack: Stack{
							Calls: []Call{
								newCall(
									"github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek",
									Args{}, "??", 0),
								newCall(
									"gopkg.in/yaml%2ev2.handleErr",
									Args{Values: []Arg{{Value: 0x433b20, IsPtr: true}}},
									"/gopath/src/gopkg.in/yaml.v2/yaml.go",
									153),
								newCall(
									"reflect.Value.assignTo",
									Args{Values: []Arg{{Value: 0x570860, IsPtr: true}, {Value: 0x803f3e0, IsPtr: true}, {Value: 0x15}}},
									"/goroot/src/reflect/value.go",
									2125),
								newCall(
									"main.main",
									Args{},
									"/gopath/src/github.com/maruel/panicparse/stack/stack.go",
									428),
							},
						},
					},
					ID:    1,
					First: true,
				},
			},
		},

		{
			name: "LongWait",
			in: []string{
				"panic: bleh",
				"",
				"goroutine 1 [chan send, 100 minutes]:",
				"gopkg.in/yaml%2ev2.handleErr(0x433b20)",
				"\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6",
				"",
				"goroutine 2 [chan send, locked to thread]:",
				"gopkg.in/yaml%2ev2.handleErr(0x8033b21)",
				"\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6",
				"",
				"goroutine 3 [chan send, 101 minutes, locked to thread]:",
				"gopkg.in/yaml%2ev2.handleErr(0x8033b22)",
				"\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6",
				"",
			},
			prefix: "panic: bleh\n\n",
			err:    io.EOF,
			want: []*Goroutine{
				{
					Signature: Signature{
						State:    "chan send",
						SleepMin: 100,
						SleepMax: 100,
						Stack: Stack{
							Calls: []Call{
								newCall(
									"gopkg.in/yaml%2ev2.handleErr",
									Args{Values: []Arg{{Value: 0x433b20, IsPtr: true}}},
									"/gopath/src/gopkg.in/yaml.v2/yaml.go",
									153),
							},
						},
					},
					ID:    1,
					First: true,
				},
				{
					Signature: Signature{
						State:  "chan send",
						Locked: true,
						Stack: Stack{
							Calls: []Call{
								newCall(
									"gopkg.in/yaml%2ev2.handleErr",
									Args{Values: []Arg{{Value: 0x8033b21, Name: "#1", IsPtr: true}}},
									"/gopath/src/gopkg.in/yaml.v2/yaml.go",
									153),
							},
						},
					},
					ID: 2,
				},
				{
					Signature: Signature{
						State:    "chan send",
						SleepMin: 101,
						SleepMax: 101,
						Stack: Stack{
							Calls: []Call{
								newCall(
									"gopkg.in/yaml%2ev2.handleErr",
									Args{Values: []Arg{{Value: 0x8033b22, Name: "#2", IsPtr: true}}},
									"/gopath/src/gopkg.in/yaml.v2/yaml.go",
									153),
							},
						},
						Locked: true,
					},
					ID: 3,
				},
			},
		},

		{
			name: "Assembly",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 16 [garbage collection]:",
				"runtime.switchtoM()",
				"\t/goroot/src/runtime/asm_amd64.s:198 fp=0xc20cfb80d8 sp=0xc20cfb80d0",
				"",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			err:    io.EOF,
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "garbage collection",
						Stack: Stack{
							Calls: []Call{
								newCall(
									"runtime.switchtoM",
									Args{},
									"/goroot/src/runtime/asm_amd64.s",
									198),
							},
						},
					},
					ID:    16,
					First: true,
				},
			},
		},

		{
			name: "Assembly1.3",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 16 [garbage collection]:",
				"runtime.switchtoM()",
				"\t/goroot/src/runtime/asm_amd64.s:198 fp=0xc20cfb80d8 sp=0xc20cfb80d0 pc=0x5007be",
				"",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			err:    io.EOF,
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "garbage collection",
						Stack: Stack{
							Calls: []Call{
								newCall(
									"runtime.switchtoM",
									Args{},
									"/goroot/src/runtime/asm_amd64.s",
									198),
							},
						},
					},
					ID:    16,
					First: true,
				},
			},
		},

		{
			name: "LineErr",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 1 [running]:",
				"github.com/maruel/panicparse/stack/stack.recurseType()",
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:12345678901234567890",
				"",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			suffix: "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:12345678901234567890\n",
			err:    errors.New("failed to parse int on line: \"/gopath/src/github.com/maruel/panicparse/stack/stack.go:12345678901234567890\""),
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						Stack: Stack{
							Calls: []Call{
								newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0),
							},
						},
					},
					ID:    1,
					First: true,
				},
			},
		},

		{
			name: "CreatedErr",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 1 [running]:",
				"github.com/maruel/panicparse/stack/stack.recurseType()",
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:1",
				"created by testing.RunTests",
				"\t/goroot/src/testing/testing.go:123456789012345678901 +0xa8b",
				"",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			suffix: "\t/goroot/src/testing/testing.go:123456789012345678901 +0xa8b\n",
			err:    errors.New("failed to parse int on line: \"/goroot/src/testing/testing.go:123456789012345678901 +0xa8b\""),
			want: []*Goroutine{
				{
					Signature: Signature{
						State:     "running",
						CreatedBy: Stack{Calls: []Call{newCall("testing.RunTests", Args{}, "", 0)}},
						Stack: Stack{
							Calls: []Call{
								newCall(
									"github.com/maruel/panicparse/stack/stack.recurseType",
									Args{},
									"/gopath/src/github.com/maruel/panicparse/stack/stack.go",
									1),
							},
						},
					},
					ID:    1,
					First: true,
				},
			},
		},

		{
			name: "ValueErr",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 1 [running]:",
				"github.com/maruel/panicparse/stack/stack.recurseType(123456789012345678901)",
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9",
				"",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			suffix: "github.com/maruel/panicparse/stack/stack.recurseType(123456789012345678901)\n" +
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9\n",
			err: errors.New("failed to parse int on line: \"github.com/maruel/panicparse/stack/stack.recurseType(123456789012345678901)\""),
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						Stack: Stack{
							Calls: []Call{
								newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0),
							},
						},
					},
					ID:    1,
					First: true,
				},
			},
		},

		{
			name: "MaxNestingDepth",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 1 [running]:",
				"github.com/maruel/panicparse/stack/stack.recurseType({{{{{...}}}}})",
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9",
				"",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			err:    io.EOF,
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						Stack: Stack{
							Calls: []Call{
								newCall(
									"github.com/maruel/panicparse/stack/stack.recurseType",
									Args{
										Values: []Arg{{IsAggregate: true, Fields: Args{
											Values: []Arg{{IsAggregate: true, Fields: Args{
												Values: []Arg{{IsAggregate: true, Fields: Args{
													Values: []Arg{{IsAggregate: true, Fields: Args{
														Values: []Arg{{IsAggregate: true, Fields: Args{
															Elided: true,
														}}},
													}}},
												}}},
											}}},
										}}},
									},
									"/gopath/src/github.com/maruel/panicparse/stack/stack.go",
									9),
							},
						},
					},
					ID:    1,
					First: true,
				},
			},
		},

		{
			name: "MaxNestingDepthExceededErr",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 1 [running]:",
				"github.com/maruel/panicparse/stack/stack.recurseType({{{{{{...}}}}}})",
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9",
				"",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			suffix: "github.com/maruel/panicparse/stack/stack.recurseType({{{{{{...}}}}}})\n" +
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9\n",
			err: errors.New("nested aggregate-typed arguments exceeded depth limit on line: \"github.com/maruel/panicparse/stack/stack.recurseType({{{{{{...}}}}}})\""),
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						Stack: Stack{
							Calls: []Call{
								newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0),
							},
						},
					},
					ID:    1,
					First: true,
				},
			},
		},

		{
			name: "UnmatchedOpeningCurlyBracketErr",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 1 [running]:",
				"github.com/maruel/panicparse/stack/stack.recurseType({{{{{...}}}})",
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9",
				"",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			suffix: "github.com/maruel/panicparse/stack/stack.recurseType({{{{{...}}}})\n" +
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9\n",
			err: errors.New("unmatched opening curly bracket on line: \"github.com/maruel/panicparse/stack/stack.recurseType({{{{{...}}}})\""),
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						Stack: Stack{
							Calls: []Call{
								newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0),
							},
						},
					},
					ID:    1,
					First: true,
				},
			},
		},

		{
			name: "UnmatchedClosingCurlyBracketErr",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 1 [running]:",
				"github.com/maruel/panicparse/stack/stack.recurseType({{{{...}}}}})",
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9",
				"",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			suffix: "github.com/maruel/panicparse/stack/stack.recurseType({{{{...}}}}})\n" +
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9\n",
			err: errors.New("unmatched closing curly bracket on line: \"github.com/maruel/panicparse/stack/stack.recurseType({{{{...}}}}})\""),
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						Stack: Stack{
							Calls: []Call{
								newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0),
							},
						},
					},
					ID:    1,
					First: true,
				},
			},
		},

		{
			name: "InconsistentIndent",
			in: []string{
				"  goroutine 1 [running]:",
				"  github.com/maruel/panicparse/stack/stack.recurseType()",
				" \t/gopath/src/github.com/maruel/panicparse/stack/stack.go:1",
				"",
			},
			suffix: " \t/gopath/src/github.com/maruel/panicparse/stack/stack.go:1\n",
			err:    errors.New(`inconsistent indentation: " \t/gopath/src/github.com/maruel/panicparse/stack/stack.go:1", expected "  "`),
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						Stack: Stack{
							Calls: []Call{
								newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0),
							},
						},
					},
					ID:    1,
					First: true,
				},
			},
		},

		{
			name: "OrderErr",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 16 [garbage collection]:",
				"\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6",
				"runtime.switchtoM()",
				"\t/goroot/src/runtime/asm_amd64.s:198 fp=0xc20cfb80d8 sp=0xc20cfb80d0",
				"",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			suffix: "\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6\n" +
				"runtime.switchtoM()\n" +
				"\t/goroot/src/runtime/asm_amd64.s:198 fp=0xc20cfb80d8 sp=0xc20cfb80d0\n",
			err: errors.New("expected a function after a goroutine header, got: \"/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6\""),
			want: []*Goroutine{
				{
					Signature: Signature{State: "garbage collection"},
					ID:        16,
					First:     true,
				},
			},
		},

		{
			name: "Elided",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 16 [garbage collection]:",
				"github.com/maruel/panicparse/stack/stack.recurseType(0x9a3ec70, 0x8062580, 0x9a3e818, 0x50a820, 0x803a8a0)",
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:53 +0x845 fp=0xc20cfc66d8 sp=0xc20cfc6470",
				"...additional frames elided...",
				"created by testing.RunTests",
				"\t/goroot/src/testing/testing.go:555 +0xa8b",
				"",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			err:    io.EOF,
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "garbage collection",
						CreatedBy: Stack{
							Calls: []Call{
								newCall(
									"testing.RunTests",
									Args{},
									"/goroot/src/testing/testing.go",
									555),
							},
						},
						Stack: Stack{
							Calls: []Call{
								newCall(
									"github.com/maruel/panicparse/stack/stack.recurseType",
									Args{
										Values: []Arg{
											{Value: 0x9a3ec70, IsPtr: true},
											{Value: 0x8062580, IsPtr: true},
											{Value: 0x9a3e818, IsPtr: true},
											{Value: 0x50a820, IsPtr: true},
											{Value: 0x803a8a0, IsPtr: true},
										},
									},
									"/gopath/src/github.com/maruel/panicparse/stack/stack.go",
									53),
							},
							Elided: true,
						},
					},
					ID:    16,
					First: true,
				},
			},
		},

		{
			name: "Syscall",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 5 [syscall]:",
				"runtime.notetsleepg(0x918100, 0xffffffffffffffff, 0x1)",
				"\t/goroot/src/runtime/lock_futex.go:201 +0x52 fp=0xc208018f68 sp=0xc208018f40",
				"runtime.signal_recv(0x0)",
				"\t/goroot/src/runtime/sigqueue.go:109 +0x135 fp=0xc208018fa0 sp=0xc208018f68",
				"os/signal.loop()",
				"\t/goroot/src/os/signal/signal_unix.go:21 +0x1f fp=0xc208018fe0 sp=0xc208018fa0",
				"runtime.goexit()",
				"\t/goroot/src/runtime/asm_amd64.s:2232 +0x1 fp=0xc208018fe8 sp=0xc208018fe0",
				"created by os/signal.init·1",
				"\t/goroot/src/os/signal/signal_unix.go:27 +0x35",
				"",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			err:    io.EOF,
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "syscall",
						CreatedBy: Stack{
							Calls: []Call{
								newCall(
									"os/signal.init·1",
									Args{},
									"/goroot/src/os/signal/signal_unix.go",
									27),
							},
						},
						Stack: Stack{
							Calls: []Call{
								newCall(
									"runtime.notetsleepg",
									Args{
										Values: []Arg{
											{Value: 0x918100, IsPtr: true},
											{Value: 0xffffffffffffffff},
											{Value: 0x1},
										},
									},
									"/goroot/src/runtime/lock_futex.go",
									201),
								newCall(
									"runtime.signal_recv",
									Args{Values: []Arg{{}}},
									"/goroot/src/runtime/sigqueue.go",
									109),
								newCall(
									"os/signal.loop",
									Args{},
									"/goroot/src/os/signal/signal_unix.go",
									21),
								newCall(
									"runtime.goexit",
									Args{},
									"/goroot/src/runtime/asm_amd64.s",
									2232),
							},
						},
					},
					ID:    5,
					First: true,
				},
			},
		},

		{
			name: "UnavailCreated",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 24 [running]:",
				"\tgoroutine running on other thread; stack unavailable",
				"created by github.com/maruel/panicparse/stack.New",
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:131 +0x381",
				"",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			err:    io.EOF,
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						CreatedBy: Stack{
							Calls: []Call{
								newCall(
									"github.com/maruel/panicparse/stack.New",
									Args{},
									"/gopath/src/github.com/maruel/panicparse/stack/stack.go",
									131),
							},
						},
						Stack: Stack{
							Calls: []Call{newCall("", Args{}, "<unavailable>", 0)},
						},
					},
					ID:    24,
					First: true,
				},
			},
		},

		{
			name: "Unavail",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 24 [running]:",
				"\tgoroutine running on other thread; stack unavailable",
				"",
				"",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			err:    io.EOF,
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						Stack: Stack{
							Calls: []Call{newCall("", Args{}, "<unavailable>", 0)},
						},
					},
					ID:    24,
					First: true,
				},
			},
		},

		{
			name: "UnavailError",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 24 [running]:",
				"\tgoroutine running on other thread; stack unavailable",
				"junk",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			suffix: "junk",
			err:    errors.New("expected empty line after unavailable stack, got: \"junk\""),
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						Stack: Stack{
							Calls: []Call{newCall("", Args{}, "<unavailable>", 0)},
						},
					},
					ID:    24,
					First: true,
				},
			},
		},

		{
			name: "NoOffset",
			in: []string{
				"panic: runtime error: index out of range",
				"",
				"goroutine 37 [runnable]:",
				"github.com/maruel/panicparse/stack.func·002()",
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:110",
				"created by github.com/maruel/panicparse/stack.New",
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:113 +0x43b",
				"",
			},
			prefix: "panic: runtime error: index out of range\n\n",
			err:    io.EOF,
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "runnable",
						CreatedBy: Stack{
							Calls: []Call{
								newCall(
									"github.com/maruel/panicparse/stack.New",
									Args{},
									"/gopath/src/github.com/maruel/panicparse/stack/stack.go",
									113),
							},
						},
						Stack: Stack{
							Calls: []Call{
								newCall(
									"github.com/maruel/panicparse/stack.func·002",
									Args{},
									"/gopath/src/github.com/maruel/panicparse/stack/stack.go",
									110),
							},
						},
					},
					ID:    37,
					First: true,
				},
			},
		},

		// For coverage of scanLines.
		{
			name: "HeaderError",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 1 [running]:",
				"junk",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			suffix: "junk",
			err:    errors.New("expected a function after a goroutine header, got: \"junk\""),
			want: []*Goroutine{
				{
					Signature: Signature{State: "running"},
					ID:        1,
					First:     true,
				},
			},
		},

		// For coverage of scanLines.
		{
			name: "FileError",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 1 [running]:",
				"github.com/maruel/panicparse/stack.func·002()",
				"junk",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			suffix: "junk",
			err:    errors.New("expected a file after a function, got: \"junk\""),
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						Stack: Stack{
							Calls: []Call{
								newCall("github.com/maruel/panicparse/stack.func·002", Args{}, "", 0),
							},
						},
					},
					ID:    1,
					First: true,
				},
			},
		},

		// For coverage of scanLines.
		{
			name: "Created",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 1 [running]:",
				"github.com/maruel/panicparse/stack.func·002()",
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:110",
				"created by github.com/maruel/panicparse/stack.New",
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:131 +0x381",
				"exit status 2",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			suffix: "exit status 2",
			err:    io.EOF,
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						CreatedBy: Stack{
							Calls: []Call{
								newCall(
									"github.com/maruel/panicparse/stack.New",
									Args{},
									"/gopath/src/github.com/maruel/panicparse/stack/stack.go",
									131),
							},
						},
						Stack: Stack{
							Calls: []Call{
								newCall(
									"github.com/maruel/panicparse/stack.func·002",
									Args{},
									"/gopath/src/github.com/maruel/panicparse/stack/stack.go",
									110),
							},
						},
					},
					ID:    1,
					First: true,
				},
			},
		},

		// For coverage of scanLines.
		{
			name: "CreatedError",
			in: []string{
				"panic: reflect.Set: value of type",
				"",
				"goroutine 1 [running]:",
				"github.com/maruel/panicparse/stack.func·002()",
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:110",
				"created by github.com/maruel/panicparse/stack.New",
				"junk",
			},
			prefix: "panic: reflect.Set: value of type\n\n",
			suffix: "junk",
			err:    errors.New("expected a file after a created line, got: \"junk\""),
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						CreatedBy: Stack{
							Calls: []Call{
								newCall("github.com/maruel/panicparse/stack.New", Args{}, "", 0),
							},
						},
						Stack: Stack{
							Calls: []Call{
								newCall(
									"github.com/maruel/panicparse/stack.func·002",
									Args{},
									"/gopath/src/github.com/maruel/panicparse/stack/stack.go",
									110),
							},
						},
					},
					ID:    1,
					First: true,
				},
			},
		},

		{
			name: "CCode",
			in: []string{
				"SIGQUIT: quit",
				"PC=0x43f349",
				"",
				"goroutine 0 [idle]:",
				"runtime.epollwait(0x4, 0x71c7118, 0xffffffff00000080, 0x0, 0xffffffff0028c1be, 0x0, 0x0, 0x0, 0x0, 0x0, ...)",
				"        /goroot/src/runtime/sys_linux_amd64.s:400 +0x19",
				"runtime.netpoll(0x901b01, 0x0)",
				"        /goroot/src/runtime/netpoll_epoll.go:68 +0xa3",
				"findrunnable(0x8012000)",
				"        /goroot/src/runtime/proc.c:1472 +0x485",
				"schedule()",
				"        /goroot/src/runtime/proc.c:1575 +0x151",
				"runtime.park_m(0x80017a0)",
				"        /goroot/src/runtime/proc.c:1654 +0x113",
				"runtime.mcall(0x432684)",
				"        /goroot/src/runtime/asm_amd64.s:186 +0x5a",
				"",
			},
			prefix: "SIGQUIT: quit\nPC=0x43f349\n\n",
			err:    io.EOF,
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "idle",
						Stack: Stack{
							Calls: []Call{
								newCall(
									"runtime.epollwait",
									Args{
										Values: []Arg{
											{Value: 0x4},
											{Value: 0x71c7118, IsPtr: true},
											{Value: 0xffffffff00000080},
											{},
											{Value: 0xffffffff0028c1be},
											{},
											{},
											{},
											{},
											{},
										},
										Elided: true,
									},
									"/goroot/src/runtime/sys_linux_amd64.s",
									400),
								newCall(
									"runtime.netpoll",
									Args{Values: []Arg{{Value: 0x901b01, IsPtr: true}, {}}},
									"/goroot/src/runtime/netpoll_epoll.go",
									68),
								newCall(
									"findrunnable",
									Args{Values: []Arg{{Value: 0x8012000, IsPtr: true}}},
									"/goroot/src/runtime/proc.c",
									1472),
								newCall("schedule", Args{}, "/goroot/src/runtime/proc.c", 1575),
								newCall(
									"runtime.park_m",
									Args{Values: []Arg{{Value: 0x80017a0, IsPtr: true}}},
									"/goroot/src/runtime/proc.c",
									1654),
								newCall(
									"runtime.mcall",
									Args{Values: []Arg{{Value: 0x432684, IsPtr: true}}},
									"/goroot/src/runtime/asm_amd64.s",
									186),
							},
						},
					},
					ID:    0,
					First: true,
				},
			},
		},

		{
			name: "WithCarriageReturn",
			in: []string{
				"goroutine 1 [running]:",
				"github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek()",
				" ??:0 +0x6d",
				"gopkg.in/yaml%2ev2.handleErr(0x433b20)",
				"\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6",
				"reflect.Value.assignTo(0x570860, 0x803f3e0, 0x15)",
				"\t/goroot/src/reflect/value.go:2125 +0x368",
				"main.main()",
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:428 +0x27",
				"",
			},
			err: io.EOF,
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						Stack: Stack{
							Calls: []Call{
								newCall(
									"github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek",
									Args{},
									"??",
									0),
								newCall(
									"gopkg.in/yaml%2ev2.handleErr",
									Args{Values: []Arg{{Value: 0x433b20, IsPtr: true}}},
									"/gopath/src/gopkg.in/yaml.v2/yaml.go",
									153),
								newCall(
									"reflect.Value.assignTo",
									Args{Values: []Arg{{Value: 0x570860, IsPtr: true}, {Value: 0x803f3e0, IsPtr: true}, {Value: 0x15}}},
									"/goroot/src/reflect/value.go",
									2125),
								newCall(
									"main.main",
									Args{},
									"/gopath/src/github.com/maruel/panicparse/stack/stack.go",
									428),
							},
						},
					},
					ID:    1,
					First: true,
				},
			},
		},

		{
			name: "WithCarriageReturn1.18Inaccurate",
			in: []string{
				"goroutine 1 [running]:",
				"github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek()",
				" ??:0 +0x6d",
				"gopkg.in/yaml%2ev2.handleErr(0x433b20?)",
				"\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6",
				"reflect.Value.assignTo(0x570860, 0x803f3e0, 0x15)",
				"\t/goroot/src/reflect/value.go:2125 +0x368",
				"main.main()",
				"\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:428 +0x27",
				"",
			},
			err: io.EOF,
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						Stack: Stack{
							Calls: []Call{
								newCall(
									"github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek",
									Args{},
									"??",
									0),
								newCall(
									"gopkg.in/yaml%2ev2.handleErr",
									Args{Values: []Arg{{Value: 0x433b20, IsPtr: true, IsInaccurate: true}}},
									"/gopath/src/gopkg.in/yaml.v2/yaml.go",
									153),
								newCall(
									"reflect.Value.assignTo",
									Args{Values: []Arg{{Value: 0x570860, IsPtr: true}, {Value: 0x803f3e0, IsPtr: true}, {Value: 0x15}}},
									"/goroot/src/reflect/value.go",
									2125),
								newCall(
									"main.main",
									Args{},
									"/gopath/src/github.com/maruel/panicparse/stack/stack.go",
									428),
							},
						},
					},
					ID:    1,
					First: true,
				},
			},
		},

		// goconvey is culprit of this.
		{
			name: "Indented",
			in: []string{
				"Failures:",
				"",
				"  * /home/maruel/go/src/foo/bar_test.go",
				"  Line 209:",
				"  Expected: '(*errors.errorString){s:\"context canceled\"}'",
				"  Actual:   'nil'",
				"  (Should resemble)!",
				"  goroutine 8 [running]:",
				"  foo/bar.TestArchiveFail.func1.2()",
				"        /home/maruel/go/foo/bar_test.go:209 +0x469",
				"  foo/bar.TestArchiveFail(0x3382000)",
				"        /home/maruel/go/src/foo/bar_test.go:155 +0xf1",
				"  testing.tRunner(0x3382000, 0x1615bf8)",
				"        /home/maruel/golang/go/src/testing/testing.go:865 +0xc0",
				"  created by testing.(*T).Run",
				"        /home/maruel/golang/go/src/testing/testing.go:916 +0x35a",
				"",
				"",
			},
			prefix: strings.Join([]string{
				"Failures:",
				"",
				"  * /home/maruel/go/src/foo/bar_test.go",
				"  Line 209:",
				"  Expected: '(*errors.errorString){s:\"context canceled\"}'",
				"  Actual:   'nil'",
				"  (Should resemble)!",
				"",
			}, "\n"),
			err: io.EOF,
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						CreatedBy: Stack{
							Calls: []Call{
								newCall(
									"testing.(*T).Run",
									Args{},
									"/home/maruel/golang/go/src/testing/testing.go",
									916),
							},
						},
						Stack: Stack{
							Calls: []Call{
								newCall(
									"foo/bar.TestArchiveFail.func1.2",
									Args{},
									"/home/maruel/go/foo/bar_test.go",
									209),
								newCall(
									"foo/bar.TestArchiveFail",
									Args{Values: []Arg{{Value: 0x3382000, Name: "#1", IsPtr: true}}},
									"/home/maruel/go/src/foo/bar_test.go",
									155),
								newCall(
									"testing.tRunner",
									Args{Values: []Arg{{Value: 0x3382000, Name: "#1", IsPtr: true}, {Value: 0x1615bf8, IsPtr: true}}},
									"/home/maruel/golang/go/src/testing/testing.go",
									865),
							},
						},
					},
					ID:    8,
					First: true,
				},
			},
		},

		{
			name:   "Race",
			in:     []string{string(internaltest.StaticPanicRaceOutput())},
			prefix: "\nGOTRACEBACK=all\n",
			want: []*Goroutine{
				{
					Signature: Signature{
						State: "running",
						CreatedBy: Stack{
							Calls: []Call{
								newCall(
									"main.panicRace",
									Args{},
									"/go/src/github.com/maruel/panicparse/cmd/panic/main.go",
									153,
								),
								newCall(
									"main.main",
									Args{},
									"/go/src/github.com/maruel/panicparse/cmd/panic/main.go",
									54,
								),
							},
						},
						Stack: Stack{
							Calls: []Call{
								newCall(
									"main.panicDoRaceRead",
									Args{},
									"/go/src/github.com/maruel/panicparse/cmd/panic/main.go",
									137,
								),
								newCall(
									"main.panicRace.func2",
									Args{},
									"/go/src/github.com/maruel/panicparse/cmd/panic/main.go",
									154),
							},
						},
					},
					ID:       8,
					First:    true,
					RaceAddr: 0xc000014100,
				},
				{
					Signature: Signature{
						State: "running",
						CreatedBy: Stack{
							Calls: []Call{
								newCall(
									"main.panicRace",
									Args{},
									"/go/src/github.com/maruel/panicparse/cmd/panic/main.go",
									150,
								),
								newCall(
									"main.main",
									Args{},
									"/go/src/github.com/maruel/panicparse/cmd/panic/main.go",
									54,
								),
							},
						},
						Stack: Stack{
							Calls: []Call{
								newCall(
									"main.panicDoRaceWrite",
									Args{},
									"/go/src/github.com/maruel/panicparse/cmd/panic/main.go",
									132),
								newCall(
									"main.panicRace.func1",
									Args{},
									"/go/src/github.com/maruel/panicparse/cmd/panic/main.go",
									151),
							},
						},
					},
					ID:        7,
					RaceWrite: true,
					RaceAddr:  0xc000014100,
				},
			},
		},

		{
			name: "RaceHdr1Err",
			in: []string{
				string(raceHeaderFooter),
			},
			prefix: string(raceHeaderFooter),
			err:    io.EOF,
		},

		{
			name: "RaceHdr2Err",
			in: []string{
				string(raceHeaderFooter),
				"",
			},
			// TODO(maruel): This is incorrect.
			prefix: "",
			err:    io.EOF,
		},

		{
			name: "RaceHdr3Err",
			in: []string{
				string(raceHeaderFooter),
				string(raceHeader),
			},
			// TODO(maruel): This is incorrect.
			prefix: "",
			err:    io.EOF,
		},

		{
			name: "RaceHdr4Err",
			in: []string{
				string(raceHeaderFooter),
				string(raceHeader),
				"",
			},
			// TODO(maruel): This is incorrect.
			prefix: "",
			err:    io.EOF,
		},
	}
	for i, line := range data {
		line := line
		t.Run(fmt.Sprintf("%d-%s", i, line.name), func(t *testing.T) {
			t.Parallel()
			prefix := bytes.Buffer{}
			r := bytes.NewBufferString(strings.Join(line.in, "\n"))
			s, suffix, err := ScanSnapshot(r, &prefix, defaultOpts())
			compareErr(t, line.err, err)
			if line.want == nil {
				if s != nil {
					t.Fatalf("unexpected %v", s)
				}
			} else {
				if s == nil {
					t.Fatalf("expected snapshot")
				}
				compareGoroutines(t, line.want, s.Goroutines)
			}
			compareString(t, line.prefix, prefix.String())
			rest, err := ioutil.ReadAll(r)
			compareErr(t, nil, err)
			compareString(t, line.suffix, string(suffix)+string(rest))
		})
	}
}

func TestScanSnapshotSyntheticTwoSnapshots(t *testing.T) {
	t.Parallel()
	in := bytes.Buffer{}
	in.WriteString("Ya\n")
	in.Write(internaltest.PanicOutputs()["simple"])
	in.WriteString("Ye\n")
	in.Write(internaltest.PanicOutputs()["int"])
	in.WriteString("Yo\n")
	panicParseDir := getPanicParseDir(t)
	ppDir := pathJoin(panicParseDir, "cmd", "panic")

	// First stack:
	prefix := bytes.Buffer{}
	s, suffix, err := ScanSnapshot(&in, &prefix, defaultOpts())
	compareErr(t, nil, err)
	if !s.guessPaths() {
		t.Error("expected success")
	}
	want := []*Goroutine{
		{
			Signature: Signature{
				State: "running",
				Stack: Stack{
					Calls: []Call{
						newCallLocal(
							"main.main",
							Args{},
							pathJoin(ppDir, "main.go"),
							74,
						),
					},
				},
			},
			ID:    1,
			First: true,
		},
	}
	compareGoroutines(t, want, s.Goroutines)
	compareString(t, "Ya\nGOTRACEBACK=all\npanic: simple\n\n", prefix.String())

	prefix.Reset()
	r := io.MultiReader(bytes.NewReader(suffix), &in)
	s, suffix, err = ScanSnapshot(r, &prefix, defaultOpts())
	compareErr(t, nil, err)
	if !s.guessPaths() {
		t.Error("expected success")
	}
	// This is a change detector on internal/main.go.
	want = []*Goroutine{
		{
			Signature: Signature{
				State: "running",
				Stack: Stack{
					Calls: []Call{
						newCallLocal(
							"main.panicint",
							Args{Values: []Arg{{Value: 42}}},
							pathJoin(ppDir, "main.go"),
							93,
						),
						newCallLocal(
							"main.glob..func9",
							Args{},
							pathJoin(ppDir, "main.go"),
							315,
						),
						newCallLocal(
							"main.main",
							Args{},
							pathJoin(ppDir, "main.go"),
							76,
						),
					},
				},
			},
			ID:    1,
			First: true,
		},
	}
	compareGoroutines(t, want, s.Goroutines)
	compareString(t, "Ye\nGOTRACEBACK=all\npanic: 42\n\n", prefix.String())
	compareString(t, "Yo\n", string(suffix))
}

func TestSplitPath(t *testing.T) {
	t.Parallel()
	if p := splitPath(""); p != nil {
		t.Fatalf("expected nil, got: %v", p)
	}
}

func TestGetGOPATHs(t *testing.T) {
	// This test cannot run in parallel.
	old := os.Getenv("GOPATH")
	defer os.Setenv("GOPATH", old)
	os.Setenv("GOPATH", "")
	if p := getGOPATHs(); len(p) != 1 {
		// It's the home directory + /go.
		t.Fatalf("expected only one path: %v", p)
	}

	root, err := ioutil.TempDir("", "stack")
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		if err = os.RemoveAll(root); err != nil {
			t.Error(err)
		}
	}()
	os.Setenv("GOPATH", filepath.Join(root, "a")+string(filepath.ListSeparator)+filepath.Join(root, "b")+string(filepath.Separator))
	if p := getGOPATHs(); len(p) != 2 {
		t.Fatalf("expected two paths: %v", p)
	}
}

// TestGomoduleComplex is an integration test that creates a non-trivial tree
// of go modules using the "replace" statement.
func TestGomoduleComplex(t *testing.T) {
	// This test cannot run in parallel.
	if internaltest.GetGoMinorVersion() < 11 {
		t.Skip("requires go module support")
	}
	old := os.Getenv("GOPATH")
	defer os.Setenv("GOPATH", old)
	root, err := ioutil.TempDir("", "stack")
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		if err = os.RemoveAll(root); err != nil {
			t.Error(err)
		}
	}()

	os.Setenv("GOPATH", filepath.Join(root, "go"))
	os.Setenv("GO111MODULE", "on")
	tree := map[string]string{
		"pkg1/go.mod": "module example.com/pkg1\n" +
			"require (\n" +
			"\texample.com/pkg2 v0.0.1\n" +
			"\texample.com/pkg3 v0.0.1\n" +
			")\n" +
			"replace example.com/pkg2 => ../pkg2\n" +
			// This is kind of a hack to force testing with a package inside GOPATH,
			// since this won't normally work by default.
			"replace example.com/pkg3 => ../go/src/example.com/pkg3\n",
		"pkg1/cmd/main.go": "package main\n" +
			"import \"example.com/pkg1/internal\"\n" +
			"func main() {\n" +
			"\tinternal.CallCallDie()\n" +
			"}\n",
		"pkg1/internal/int.go": "package internal\n" +
			"import \"example.com/pkg2\"\n" +
			"func CallCallDie() {\n" +
			"\tpkg2.CallDie()\n" +
			"}\n",

		"pkg2/go.mod": "module example.com/pkg2\n" +
			"require (\n" +
			"\texample.com/pkg3 v0.0.1\n" +
			")\n" +
			// This is kind of a hack to force testing with a package inside GOPATH,
			// since this won't normally work by default.
			"replace example.com/pkg3 => ../go/src/example.com/pkg3\n",
		"pkg2/src2.go": "package pkg2\n" +
			"import \"example.com/pkg3\"\n" +
			"func CallDie() { pkg3.Die() }\n",

		"go/src/example.com/pkg3/go.mod": "module example.com/pkg3\n",
		"go/src/example.com/pkg3/src3.go": "package pkg3\n" +
			"func Die() { panic(42) }\n",
	}
	createTree(t, root, tree)

	exe := filepath.Join(root, "yo")
	if runtime.GOOS == "windows" {
		exe += ".exe"
	}
	if err = internaltest.Compile("./cmd", exe, filepath.Join(root, "pkg1"), true, false); err != nil {
		t.Fatal(err)
	}

	out, err := exec.Command(exe).CombinedOutput()
	if err == nil {
		t.Error("expected failure")
	}
	prefix := bytes.Buffer{}
	s, suffix, err := ScanSnapshot(bytes.NewReader(out), &prefix, defaultOpts())
	compareErr(t, io.EOF, err)
	if !s.guessPaths() {
		t.Error("expected success")
	}
	if s == nil {
		t.Fatal("expected snapshot")
	}
	if s.IsRace() {
		t.Fatal("unexpected race")
	}
	compareString(t, "panic: 42\n\n", prefix.String())
	compareString(t, "", string(suffix))
	wantGOROOT := ""
	compareString(t, wantGOROOT, s.RemoteGOROOT)
	compareString(t, runtime.GOROOT(), strings.Replace(s.LocalGOROOT, "/", pathSeparator, -1))

	rootRemote := root
	if runtime.GOOS == "windows" {
		// On Windows, we must make the path to be POSIX style.
		rootRemote = strings.Replace(root, pathSeparator, "/", -1)
	}
	rootLocal := rootRemote
	if runtime.GOOS == "darwin" {
		if ver := internaltest.GetGoMinorVersion(); ver > 0 && ver < 19 {
			// On MacOS, $TMPDIR path is a symlink /var -> /private/var.
			//
			// On versions before go1.19, it was somehow evaluated by exec.Cmd.Run().
			// This stopped being true on go1.19. I suspect it is a side-effect of
			// https://go.dev/doc/go1.19#os-exec-path
			//
			// This must NOT be run on Windows otherwise the path will be converted
			// to 8.3 format.
			if rootRemote, err = filepath.EvalSymlinks(rootLocal); err != nil {
				t.Fatal(err)
			}
		}
	}

	// This part is a bit tricky. The symlink is evaluated on the left since,
	// since it's what is the "remote" path, but it is not on the right since,
	// which is the "local" path. This difference only exists on MacOS.
	wantGOPATHs := map[string]string{
		pathJoin(rootRemote, "go"): pathJoin(rootLocal, "go"),
	}
	if diff := cmp.Diff(s.RemoteGOPATHs, wantGOPATHs); diff != "" {
		t.Fatalf("+want/-got: %s", diff)
	}

	// Local go module search is on the path with symlink evaluated on MacOS.
	// This is kind of confusing because it is the "remote" path.
	wantGomods := map[string]string{
		pathJoin(rootRemote, "pkg1"): "example.com/pkg1",
		pathJoin(rootRemote, "pkg2"): "example.com/pkg2",
	}
	if diff := cmp.Diff(s.LocalGomods, wantGomods); diff != "" {
		t.Fatalf("+want/-got: %s", diff)
	}

	want := []*Goroutine{
		{
			Signature: Signature{
				State: "running",
				Stack: Stack{
					Calls: []Call{
						{
							Func:          newFunc("example.com/pkg3.Die"),
							Args:          Args{Elided: true},
							RemoteSrcPath: pathJoin(rootRemote, "go", "src", "example.com", "pkg3", "src3.go"),
							Line:          2,
							SrcName:       "src3.go",
							DirSrc:        "pkg3/src3.go",
							LocalSrcPath:  pathJoin(rootLocal, "go", "src", "example.com", "pkg3", "src3.go"),
							RelSrcPath:    "example.com/pkg3/src3.go",
							ImportPath:    "example.com/pkg3",
							Location:      GOPATH,
						},
						{
							Func:          newFunc("example.com/pkg2.CallDie"),
							Args:          Args{Elided: true},
							RemoteSrcPath: pathJoin(rootRemote, "pkg2", "src2.go"),
							Line:          3,
							SrcName:       "src2.go",
							DirSrc:        "pkg2/src2.go",
							// Since this was found locally as a go module using the remote
							// path, this is correct, even if confusing.
							LocalSrcPath: pathJoin(rootRemote, "pkg2", "src2.go"),
							RelSrcPath:   "src2.go",
							ImportPath:   "example.com/pkg2",
							Location:     GoMod,
						},
						{
							Func:          newFunc("example.com/pkg1/internal.CallCallDie"),
							RemoteSrcPath: pathJoin(rootRemote, "pkg1", "internal", "int.go"),
							Line:          2,
							SrcName:       "int.go",
							DirSrc:        "internal/int.go",
							// Since this was found locally as a go module using the remote
							// path, this is correct, even if confusing.
							LocalSrcPath: pathJoin(rootRemote, "pkg1", "internal", "int.go"),
							RelSrcPath:   "internal/int.go",
							ImportPath:   "example.com/pkg1/internal",
							Location:     GoMod,
						},
						{
							Func:          newFunc("main.main"),
							RemoteSrcPath: pathJoin(rootRemote, "pkg1", "cmd", "main.go"),
							Line:          4,
							SrcName:       "main.go",
							DirSrc:        "cmd/main.go",
							// Since this was found locally as a go module using the remote
							// path, this is correct, even if confusing.
							LocalSrcPath: pathJoin(rootRemote, "pkg1", "cmd", "main.go"),
							RelSrcPath:   "cmd/main.go",
							ImportPath:   "example.com/pkg1/cmd",
							Location:     GoMod,
						},
					},
				},
			},
			ID:    1,
			First: true,
		},
	}
	similarGoroutines(t, want, s.Goroutines)
}

func TestGoRun(t *testing.T) {
	t.Parallel()
	root, err := ioutil.TempDir("", "stack")
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		if err = os.RemoveAll(root); err != nil {
			t.Error(err)
		}
	}()

	p := filepath.Join(root, "main.go")
	content := "package main\nfunc main() { panic(42) }\n"
	if err = ioutil.WriteFile(p, []byte(content), 0600); err != nil {
		t.Fatal(err)
	}
	c := exec.Command("go", "run", p)
	out, err := c.CombinedOutput()
	if err == nil {
		t.Fatal("expected failure")
	}
	prefix := bytes.Buffer{}
	s, suffix, err := ScanSnapshot(bytes.NewReader(out), &prefix, defaultOpts())
	compareErr(t, nil, err)
	compareString(t, "panic: 42\n\n", prefix.String())
	compareString(t, "exit status 2\n", string(suffix))
	if s == nil {
		t.Fatal("expected snapshot")
	}
	if runtime.GOOS == "windows" {
		// On Windows, we must make the path to be POSIX style.
		p = strings.Replace(p, pathSeparator, "/", -1)
	}

	want := []*Goroutine{
		{
			Signature: Signature{
				State: "running",
				Stack: Stack{
					Calls: []Call{
						{
							Func: Func{
								Complete:   "main.main",
								ImportPath: "main",
								DirName:    "main",
								Name:       "main",
								IsExported: true,
								IsPkgMain:  true,
							},
							RemoteSrcPath: p,
							Line:          2,
							SrcName:       "main.go",
							DirSrc:        path.Base(path.Dir(p)) + "/main.go",
							ImportPath:    "main",
							Location:      LocationUnknown,
						},
					},
				},
			},
			ID:    1,
			First: true,
		},
	}
	similarGoroutines(t, want, s.Goroutines)

	if !s.guessPaths() {
		t.Error("expected success")
	}
	want[0].Stack.Calls[0].LocalSrcPath = p
	want[0].Stack.Calls[0].RelSrcPath = "main.go"
	// This is not technically true, when using go run there's no need for a
	// go.mod file, but I don't think it's worth handling specifically.
	want[0].Stack.Calls[0].Location = GoMod
	similarGoroutines(t, want, s.Goroutines)
}

// TestPanic runs github.com/maruel/panicparse/v2/cmd/panic with every
// supported panic modes.
func TestPanic(t *testing.T) {
	t.Parallel()
	cmds := internaltest.PanicOutputs()
	want := map[string]int{
		"chan_receive":              2,
		"chan_send":                 2,
		"goroutine_1":               2,
		"goroutine_dedupe_pointers": 101,
		"goroutine_100":             101,
	}

	panicParseDir := getPanicParseDir(t)
	ppDir := pathJoin(panicParseDir, "cmd", "panic")

	// Test runtime code. For those not in "custom", just assert that they
	// succeed.
	custom := map[string]func(*testing.T, *Snapshot, *bytes.Buffer, string){
		"args_elided": testPanicArgsElided,
		"mismatched":  testPanicMismatched,
		"race":        testPanicRace,
		"str":         testPanicStr,
		"utf8":        testPanicUTF8,
	}
	// Make sure all custom handlers are showing up in cmds.
	for n := range custom {
		if _, ok := cmds[n]; !ok {
			if n == "race" {
				t.Skip("race is unsupported")
			}
			t.Fatalf("untested mode %q:\n%v", n, cmds[n])
		}
	}

	for cmd, data := range cmds {
		cmd := cmd
		data := data
		t.Run(cmd, func(t *testing.T) {
			t.Parallel()
			prefix := bytes.Buffer{}
			s, suffix, err := ScanSnapshot(bytes.NewReader(data), &prefix, defaultOpts())
			if err != nil && err != io.EOF {
				t.Fatal(err)
			}
			if s == nil {
				t.Fatal("context is nil")
			}
			if !s.guessPaths() {
				t.Fatal("expected GuessPaths to work")
			}
			if f := custom[cmd]; f != nil {
				f(t, s, &prefix, ppDir)
				return
			}
			e := want[cmd]
			if e == 0 {
				e = 1
			}
			if got := len(s.Goroutines); got != e {
				t.Fatalf("unexpected Goroutines; want %d, got %d", e, got)
			}
			compareString(t, "", string(suffix))
		})
	}
}

func testPanicArgsElided(t *testing.T, s *Snapshot, b *bytes.Buffer, ppDir string) {
	if s.RemoteGOROOT != "" {
		t.Fatalf("RemoteGOROOT is %q", s.RemoteGOROOT)
	}
	if b.String() != "GOTRACEBACK=all\npanic: 1\n\n" {
		t.Fatalf("output: %q", b.String())
	}
	want := []*Goroutine{
		{
			Signature: Signature{
				State: "running",
				Stack: Stack{
					Calls: []Call{
						newCallLocal(
							"main.panicArgsElided",
							Args{
								Values: []Arg{{Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5}, {Value: 6}, {Value: 7}, {Value: 8}, {Value: 9}, {Value: 10}},
								Elided: true,
							},
							pathJoin(ppDir, "main.go"),
							58),
						newCallLocal("main.glob..func1", Args{}, pathJoin(ppDir, "main.go"), 134),
						newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340),
					},
				},
			},
			ID:    1,
			First: true,
		},
	}
	similarGoroutines(t, want, s.Goroutines)
}

func testPanicMismatched(t *testing.T, s *Snapshot, b *bytes.Buffer, ppDir string) {
	if s.RemoteGOROOT != "" {
		t.Fatalf("RemoteGOROOT is %q", s.RemoteGOROOT)
	}
	if b.String() != "GOTRACEBACK=all\npanic: 42\n\n" {
		t.Fatalf("output: %q", b.String())
	}
	ver := "/v2"
	if !internaltest.IsUsingModules() {
		ver = ""
	}
	want := []*Goroutine{
		{
			Signature: Signature{
				State: "running",
				Stack: Stack{
					Calls: []Call{
						newCallLocal(
							// This is important to note here that the Go runtime prints out
							// the package path, and not the package name.
							//
							// Here the package name is "correct". There is no way to deduce
							// this from the stack trace.
							"github.com/maruel/panicparse"+ver+"/cmd/panic/internal/incorrect.Panic",
							Args{},
							pathJoin(ppDir, "internal", "incorrect", "correct.go"),
							7),
						newCallLocal("main.glob..func20", Args{}, pathJoin(ppDir, "main.go"), 314),
						newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340),
					},
				},
			},
			ID:    1,
			First: true,
		},
	}
	similarGoroutines(t, want, s.Goroutines)
}

func testPanicRace(t *testing.T, s *Snapshot, b *bytes.Buffer, ppDir string) {
	if s.RemoteGOROOT != "" {
		t.Fatalf("RemoteGOROOT is %q", s.RemoteGOROOT)
	}
	if b.String() != "GOTRACEBACK=all\n" {
		t.Fatalf("output: %q", b.String())
	}
	want := []*Goroutine{
		{
			Signature: Signature{
				State: "running",
				CreatedBy: Stack{
					Calls: []Call{
						newCallLocal(
							"main.panicRace",
							Args{},
							pathJoin(ppDir, "main.go"),
							151,
						),
						newCallLocal(
							"main.main",
							Args{},
							pathJoin(ppDir, "main.go"),
							76,
						),
					},
				},
				Stack: Stack{
					Calls: []Call{
						newCallLocal(
							"main.panicDoRaceRead",
							Args{},
							pathJoin(ppDir, "main.go"),
							150),
						newCallLocal(
							"main.panicRace.func2",
							Args{},
							pathJoin(ppDir, "main.go"),
							135),
					},
				},
			},
			RaceAddr: pointer,
		},
		{
			Signature: Signature{
				State: "running",
				CreatedBy: Stack{
					Calls: []Call{
						newCallLocal(
							"main.panicRace",
							Args{},
							pathJoin(ppDir, "main.go"),
							151,
						),
						newCallLocal(
							"main.main",
							Args{},
							pathJoin(ppDir, "main.go"),
							76,
						),
					},
				},
				Stack: Stack{
					Calls: []Call{
						newCallLocal(
							"main.panicDoRaceWrite",
							Args{},
							pathJoin(ppDir, "main.go"),
							145),
						newCallLocal(
							"main.panicRace.func1",
							Args{},
							pathJoin(ppDir, "main.go"),
							132),
					},
				},
			},
			RaceWrite: true,
			RaceAddr:  pointer,
		},
	}
	// IDs are not deterministic, so zap them too but take them for the race
	// detector first.
	for i, g := range s.Goroutines {
		g.ID = i + 1
		if g.RaceAddr > 4*1024*1024 {
			g.RaceAddr = pointer
		}
	}
	// Sometimes the read is detected first.
	if s.Goroutines[0].RaceWrite {
		want[0], want[1] = want[1], want[0]
	}
	// These fields are order-dependent, so set them last.
	want[0].ID = 1
	want[1].ID = 2
	want[0].First = true
	want[1].First = false
	similarGoroutines(t, want, s.Goroutines)
}

func testPanicStr(t *testing.T, s *Snapshot, b *bytes.Buffer, ppDir string) {
	if s.RemoteGOROOT != "" {
		t.Fatalf("RemoteGOROOT is %q", s.RemoteGOROOT)
	}
	if b.String() != "GOTRACEBACK=all\npanic: allo\n\n" {
		t.Fatalf("output: %q", b.String())
	}
	want := []*Goroutine{
		{
			Signature: Signature{
				State: "running",
				Stack: Stack{
					Calls: []Call{
						newCallLocal(
							"main.panicstr",
							ifCombinedAggregateArgs(
								Args{Values: []Arg{{IsAggregate: true, Fields: Args{
									Values: []Arg{{Value: 0x123456, IsPtr: true}, {Value: 4}},
								}}}},
								// else
								Args{Values: []Arg{{Value: 0x123456, IsPtr: true}, {Value: 4}}},
							),
							pathJoin(ppDir, "main.go"),
							50),
						newCallLocal("main.glob..func19", Args{}, pathJoin(ppDir, "main.go"), 307),
						newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340),
					},
				},
			},
			ID:    1,
			First: true,
		},
	}
	similarGoroutines(t, want, s.Goroutines)
}

func testPanicUTF8(t *testing.T, s *Snapshot, b *bytes.Buffer, ppDir string) {
	if s.RemoteGOROOT != "" {
		t.Fatalf("RemoteGOROOT is %q", s.RemoteGOROOT)
	}
	if b.String() != "GOTRACEBACK=all\npanic: 42\n\n" {
		t.Fatalf("output: %q", b.String())
	}
	ver := "/v2"
	if !internaltest.IsUsingModules() {
		ver = ""
	}
	want := []*Goroutine{
		{
			Signature: Signature{
				State: "running",
				Stack: Stack{
					Calls: []Call{
						newCallLocal(
							// This is important to note here the inconsistency in the Go
							// runtime stack generator. The path is escaped, but symbols are
							// not.
							"github.com/maruel/panicparse"+ver+"/cmd/panic/internal/utf8.(*Strùct).Pànic",
							ifCombinedAggregateArgs(
								Args{Values: []Arg{{Value: 1, IsInaccurate: true}}},
								// else
								Args{Values: []Arg{{Value: 0xc0000b2e48, IsPtr: true, IsInaccurate: true}}},
							),
							// See TestCallUTF8 in stack_test.go for exercising the methods on
							// Call in this situation.
							pathJoin(ppDir, "internal", "utf8", "utf8.go"),
							10),
						newCallLocal("main.glob..func21", Args{}, pathJoin(ppDir, "main.go"), 322),
						newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340),
					},
				},
			},
			ID:    1,
			First: true,
		},
	}
	similarGoroutines(t, want, s.Goroutines)
}

// TestPanicweb implements the parsing of panicweb output.
//
// panicweb is a separate binary from the rest of panic because importing the
// "net" package causes a background thread to be started, which breaks "panic
// asleep".
func TestPanicweb(t *testing.T) {
	t.Parallel()
	if runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le" {
		t.Skip("https://github.com/maruel/panicparse/issues/66")
	}
	prefix := bytes.Buffer{}
	s, suffix, err := ScanSnapshot(bytes.NewReader(internaltest.PanicwebOutput()), &prefix, defaultOpts())
	if err != io.EOF {
		t.Fatal(err)
	}
	if s == nil {
		t.Fatal("snapshot is nil")
	}
	compareString(t, "panic: Here's a snapshot of a normal web server.\n\n", prefix.String())
	compareString(t, "", string(suffix))
	if s.RemoteGOROOT != "" {
		t.Fatalf("unexpected RemoteGOROOT: %q", s.RemoteGOROOT)
	}
	if !s.guessPaths() {
		t.Error("expected success")
	}
	if s.RemoteGOROOT != strings.Replace(runtime.GOROOT(), "\\", "/", -1) {
		t.Fatalf("RemoteGOROOT mismatch; want:%q got:%q", runtime.GOROOT(), s.RemoteGOROOT)
	}
	if got := len(s.Goroutines); got < 30 {
		t.Fatalf("unexpected Goroutines; want at least 30, got %d", got)
	}
	// The goal here is not to find the exact match since it'll change across
	// OSes and Go versions, but to find some of the expected signatures.
	pwebDir := pathJoin(getPanicParseDir(t), "cmd", "panicweb")
	// Reduce the goroutines and categorize the signatures.
	var types []panicwebSignatureType
	for _, b := range s.Aggregate(AnyPointer).Buckets {
		types = append(types, identifyPanicwebSignature(t, b, pwebDir))
	}
	// Count the expected types.
	if v := pstCount(types, pstUnknown); v != 0 {
		t.Fatalf("found %d unknown signatures", v)
	}
	if v := pstCount(types, pstMain); v != 1 {
		t.Fatalf("found %d pstMain signatures", v)
	}
	if v := pstCount(types, pstURL1handler); v != 1 && v != 2 {
		t.Fatalf("found %d URL1Handler signatures", v)
	}
	if v := pstCount(types, pstURL2handler); v != 1 && v != 2 {
		t.Fatalf("found %d URL2Handler signatures", v)
	}
	if v := pstCount(types, pstClient); v == 0 {
		t.Fatalf("found %d client signatures", v)
	}
	if v := pstCount(types, pstServe); v != 1 {
		t.Fatalf("found %d serve signatures", v)
	}
	if v := pstCount(types, pstColorable); v != 1 {
		t.Fatalf("found %d colorable signatures", v)
	}
	if v := pstCount(types, pstStdlib); v < 3 {
		t.Fatalf("found %d stdlib signatures", v)
	}
}

func TestIsGomodule(t *testing.T) {
	t.Parallel()
	pwd, err := os.Getwd()
	if err != nil {
		t.Fatal(err)
	}
	// Our internal functions work with '/' as path separator.
	parts := splitPath(strings.Replace(pwd, "\\", "/", -1))
	gmc := gomodCache{}
	root, importPath := gmc.isGoModule(parts)
	if want := strings.Join(parts[:len(parts)-1], "/"); want != root {
		t.Errorf("want: %q, got: %q", want, root)
	}
	if want := "github.com/maruel/panicparse/v2"; want != importPath {
		t.Errorf("want: %q, got: %q", want, importPath)
	}
	got := reModule.FindStringSubmatch("foo\r\nmodule bar\r\nbaz")
	if diff := cmp.Diff([]string{"module bar\r", "bar"}, got); diff != "" {
		t.Fatalf("-want, +got:\n%s", diff)
	}
}

func TestAtou(t *testing.T) {
	t.Parallel()
	if i, b := atou([]byte("a")); i != 0 || b {
		t.Error("oops")
	}
}

func TestTrimLeftSpace(t *testing.T) {
	t.Parallel()
	if trimLeftSpace(nil) != nil {
		t.Error("oops")
	}
}

func TestTrimCurlyBrackets(t *testing.T) {
	t.Parallel()
	data := []struct {
		input                  []byte
		want                   []byte
		wantOpened, wantClosed int
	}{
		{nil, nil, 0, 0},
		{[]byte(""), []byte(""), 0, 0},
		{[]byte("a"), []byte("a"), 0, 0},
		{[]byte("{a"), []byte("a"), 1, 0},
		{[]byte("{{a"), []byte("a"), 2, 0},
		{[]byte("{{a}}"), []byte("a"), 2, 2},
		{[]byte("{a}}"), []byte("a"), 1, 2},
		{[]byte("a}}"), []byte("a"), 0, 2},
		{[]byte("{}"), []byte(""), 1, 1},
		{[]byte("{{}}"), []byte(""), 2, 2},
		// Not expected in practice.
		{[]byte("}{"), []byte("}{"), 0, 0},
		{[]byte("{{}}a{{}}"), []byte("}}a{{"), 2, 2},
	}
	for i, line := range data {
		line := line
		t.Run(fmt.Sprintf("%d-%s", i, line.input), func(t *testing.T) {
			gotOpened, got, gotClosed := trimCurlyBrackets(line.input)
			if !bytes.Equal(line.want, got) {
				t.Errorf("want %s, got %s", line.want, got)
			}
			equiv := bytes.TrimRight(bytes.TrimLeft(line.input, "{"), "}")
			if !bytes.Equal(line.want, equiv) {
				t.Errorf("want %s, got %s", line.want, got)
			}
			if line.wantOpened != gotOpened {
				t.Errorf("want %d opening curly brackets, got %d", line.wantOpened, gotOpened)
			}
			if line.wantClosed != gotClosed {
				t.Errorf("want %d closing curly brackets, got %d", line.wantClosed, gotClosed)
			}
		})
	}
}

func BenchmarkScanSnapshot_Guess(b *testing.B) {
	b.ReportAllocs()
	data := internaltest.StaticPanicwebOutput()
	opts := defaultOpts()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		s, _, err := ScanSnapshot(bytes.NewReader(data), ioutil.Discard, opts)
		if err != io.EOF {
			b.Fatal(err)
		}
		if s == nil {
			b.Fatal("missing context")
		}
	}
}

func BenchmarkScanSnapshot_NoGuess(b *testing.B) {
	b.ReportAllocs()
	data := internaltest.StaticPanicwebOutput()
	opts := defaultOpts()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		s, _, err := ScanSnapshot(bytes.NewReader(data), ioutil.Discard, opts)
		if err != io.EOF {
			b.Fatal(err)
		}
		if s == nil {
			b.Fatal("missing context")
		}
	}
}

func BenchmarkScanSnapshot_Passthru(b *testing.B) {
	b.ReportAllocs()
	buf := make([]byte, b.N)
	for i := range buf {
		buf[i] = 'i'
		if i%16 == 0 {
			buf[i] = '\n'
		}
	}
	prefix := bytes.Buffer{}
	prefix.Grow(len(buf))
	r := bytes.NewReader(buf)
	opts := defaultOpts()
	b.ResetTimer()
	s, suffix, err := ScanSnapshot(r, &prefix, opts)
	if err != io.EOF {
		b.Fatal(err)
	}
	if s != nil {
		b.Fatalf("unexpected %v", s)
	}
	b.StopTimer()
	if !bytes.Equal(prefix.Bytes(), buf) {
		b.Fatal("unexpected prefix")
	}
	if len(suffix) != 0 {
		b.Fatal("unexpected suffix")
	}
}

//

type panicwebSignatureType int

const (
	pstUnknown panicwebSignatureType = iota
	pstMain
	pstURL1handler
	pstURL2handler
	pstClient
	pstServe
	pstColorable
	pstStdlib
)

func pstCount(s []panicwebSignatureType, t panicwebSignatureType) int {
	i := 0
	for _, v := range s {
		if v == t {
			i++
		}
	}
	return i
}

// identifyPanicwebSignature tries to assign one of the predefined signature to
// the bucket provided.
//
// One challenge is that the path will be different depending if this test is
// run within GOPATH or outside.
func identifyPanicwebSignature(t *testing.T, b *Bucket, pwebDir string) panicwebSignatureType {
	ver := ""
	if !isInGOPATH {
		ver = "/v2"
	}

	// The first bucket (the one calling panic()) is deterministic.
	if b.First {
		if len(b.IDs) != 1 {
			t.Fatal("first bucket is not correct")
			return pstUnknown
		}
		want := Signature{
			State: "running",
			Stack: Stack{
				Calls: []Call{
					newCallLocal("main.main", Args{}, pathJoin(pwebDir, "main.go"), 80),
				},
			},
		}
		similarSignatures(t, &want, &b.Signature)
		return pstMain
	}

	// We should find exactly 10 sleeping routines in the URL1Handler handler
	// signature and 3 in URL2Handler.
	if s := b.Stack.Calls[0].Func.Name; s == "URL1Handler" || s == "URL2Handler" {
		if b.State != "chan receive" {
			t.Fatalf("suspicious: %#v", b)
			return pstUnknown
		}
		if b.Stack.Calls[0].ImportPath != "github.com/maruel/panicparse"+ver+"/cmd/panicweb/internal" {
			t.Fatalf("suspicious: %q\n%#v", b.Stack.Calls[0].ImportPath, b)
			return pstUnknown
		}
		if b.Stack.Calls[0].SrcName != "internal.go" {
			t.Fatalf("suspicious: %#v", b)
			return pstUnknown
		}
		if b.CreatedBy.Calls[0].SrcName != "server.go" {
			t.Fatalf("suspicious: %#v", b)
			return pstUnknown
		}
		if b.CreatedBy.Calls[0].ImportPath != "net/http" {
			t.Fatalf("suspicious: %#v", b)
			return pstUnknown
		}
		if b.CreatedBy.Calls[0].Func.Name != "(*Server).Serve" {
			t.Fatalf("suspicious: %#v", b)
			return pstUnknown
		}
		if s == "URL1Handler" {
			return pstURL1handler
		}
		return pstURL2handler
	}

	// Find the client goroutine signatures. For the client, it is likely that
	// they haven't all bucketed perfectly.
	if b.CreatedBy.Calls[0].ImportPath == "github.com/maruel/panicparse"+ver+"/cmd/panicweb/internal" && b.CreatedBy.Calls[0].Func.Name == "GetAsync" {
		// TODO(maruel): More checks.
		return pstClient
	}

	// Now find the two goroutine started by main.
	if b.CreatedBy.Calls[0].ImportPath == "github.com/maruel/panicparse"+ver+"/cmd/panicweb" && b.CreatedBy.Calls[0].Func.ImportPath == "main" && b.CreatedBy.Calls[0].Func.Name == "main" {
		if b.State == "IO wait" {
			return pstServe
		}
		if b.State == "chan receive" {
			// Warning: This is brittle and will fail whenever go-colorable is
			// updated so only check the string minimum here.
			if !b.Signature.Locked {
				t.Fatal("expected Locked")
			}
			// This is a change detector on internal/main.go.
			want := Stack{Calls: []Call{newCallLocal("main.main", Args{}, pathJoin(pwebDir, "main.go"), 145)}}
			compareStacks(t, &b.Signature.CreatedBy, &want)
			for i := range b.Signature.Stack.Calls {
				if strings.HasPrefix(b.Signature.Stack.Calls[i].ImportPath, "github.com/mattn/go-colorable") {
					return pstColorable
				}
			}
			t.Fatalf("failed to find go-colorable\n%# v", b.Signature.Stack.Calls)
		}
		// That's the unix.Nanosleep() or windows.SleepEx() call.
		if b.State == "syscall" {
			{
				want := Stack{
					Calls: []Call{
						newCallLocal("main.main", Args{}, pathJoin(pwebDir, "main.go"), 63),
					},
				}
				zapStacks(t, &want, &b.CreatedBy)
				compareStacks(t, &want, &b.CreatedBy)
			}
			if l := len(b.IDs); l != 1 {
				t.Fatalf("expected 1 goroutine for the signature, got %d", l)
			}
			if runtime.GOOS == "windows" {
				if l := len(b.Stack.Calls); l != 5 {
					t.Fatalf("expected %d calls, got %d", 5, l)
				}
				if s := b.Stack.Calls[0].RelSrcPath; s != "runtime/syscall_windows.go" {
					t.Fatalf("expected %q file, got %q", "runtime/syscall_windows.go", s)
				}
			} else {
				if l := len(b.Stack.Calls); l != 4 {
					t.Fatalf("expected %d calls, got %d", 4, l)
				}
				// The first item shall be an assembly file independent of the OS on Go
				// < 1.19.
				s := b.Stack.Calls[0].RelSrcPath
				if ver := internaltest.GetGoMinorVersion(); ver > 0 && ver < 19 {
					if !strings.HasSuffix(s, ".s") {
						t.Fatalf("expected assembly file, got %q", s)
					}
				} else if !strings.HasPrefix(s, "syscall") && !strings.HasSuffix(s, ".go") {
					t.Fatalf("expected syscall go file, got %q", s)
				}
			}
			// Process the golang.org/x/sys call specifically.
			path := "golang.org/x/sys/unix"
			fn := "Nanosleep"
			mainOS := "main_unix.go"
			prefix := "golang.org/x/sys@v0.0.0-"
			expectDate := true
			usingModules := internaltest.IsUsingModules()
			if !usingModules {
				// Assert that there's no version by including the trailing /.
				prefix = "golang.org/x/sys/"
				expectDate = false
			}
			if runtime.GOOS == "windows" {
				// This changes across Go version, this check is super fragile.
				path = "syscall"
				fn = "Syscall"
				mainOS = "main_windows.go"
				prefix = "runtime/syscall_windows.go"
				expectDate = false
			}
			if b.Stack.Calls[1].Func.ImportPath != path || b.Stack.Calls[1].Func.Name != fn {
				t.Fatalf("expected %q & %q, got %#v", path, fn, b.Stack.Calls[1].Func)
			}
			if !strings.HasPrefix(b.Stack.Calls[1].RelSrcPath, prefix) {
				t.Fatalf("expected %q, got %q", prefix, b.Stack.Calls[1].RelSrcPath)
			}
			if expectDate {
				// Assert that it's using @v0-0-0.<date>-<commit> format.
				ver := strings.SplitN(b.Stack.Calls[1].RelSrcPath[len(prefix):], "/", 2)[0]
				re := regexp.MustCompile(`^\d{14}-[a-f0-9]{12}$`)
				if !re.MatchString(ver) {
					t.Fatalf("unexpected version string %q", ver)
				}
			}
			{
				want := []Call{
					newCallLocal("main.sysHang", Args{}, pathJoin(pwebDir, mainOS), 12),
					newCallLocal(
						"main.main.func3",
						ifCombinedAggregateArgs(
							Args{},
							// else
							Args{Values: []Arg{{Value: 0xc000140720, Name: "#135", IsPtr: true}}},
						),
						pathJoin(pwebDir, "main.go"),
						65),
				}
				got := b.Stack.Calls[2:]
				if ver := internaltest.GetGoMinorVersion(); (ver > 0 && ver < 18 && !is64Bit) || runtime.GOOS == "windows" {
					// TODO(maruel): Fix check on Windows.
					// On go1.17 on 32 bits this is failing but not on go1.18, so only
					// skip in that case.
					t.Log("skipping some checks on <go1.18 on 32 bits")
				} else {
					for i := range want {
						zapCalls(t, &want[i], &got[i])
					}
					if diff := cmp.Diff(want, got); diff != "" {
						t.Fatalf("rest of stack mismatch (-want +got):\n%s", diff)
					}
				}
			}
			return pstStdlib
		}
		t.Fatalf("suspicious: %# v", b)
		return pstUnknown
	}

	// The rest should all be created with internal threads.
	if b.CreatedBy.Calls[0].Location == Stdlib {
		return pstStdlib
	}

	// On older Go version, there's often an assembly stack in asm_amd64.s.
	if b.CreatedBy.Calls[0].Func.Complete == "" {
		if len(b.Stack.Calls) == 1 && b.Stack.Calls[0].Func.Complete == "runtime.goexit" {
			return pstStdlib
		}
	}
	t.Logf("CreatedBy import: %s", b.CreatedBy.Calls[0].ImportPath)
	t.Logf("CreatedBy:\n%#v", b.CreatedBy)
	t.Fatalf("unexpected thread started by non-stdlib:\n%#v", b.Stack.Calls)
	return pstUnknown
}

//

func defaultOpts() *Opts {
	o := DefaultOpts()
	o.GuessPaths = false
	o.AnalyzeSources = false
	return o
}

// getPanicParseDir returns the path to the root directory of panicparse
// package, using "/" as path separator.
func getPanicParseDir(t *testing.T) string {
	// We assume that the working directory is the directory containing this
	// source. In Go test framework, this normally holds true. If this ever
	// becomes false, let's fix this.
	thisDir, err := os.Getwd()
	if err != nil {
		t.Fatal(err)
	}
	// "/" is used even on Windows in the stack trace, return in this format to
	// simply our life.
	return strings.Replace(filepath.Dir(thisDir), "\\", "/", -1)
}

func createTree(t *testing.T, root string, tree map[string]string) {
	for path, content := range tree {
		p := filepath.Join(root, strings.Replace(path, "/", pathSeparator, -1))
		b := filepath.Dir(p)
		if err := os.MkdirAll(b, 0700); err != nil {
			t.Fatal(err)
		}
		if err := ioutil.WriteFile(p, []byte(content), 0600); err != nil {
			t.Fatal(err)
		}
	}
}

// ifCombinedAggregateArgs returns one of the two provided Args structs, based
// on the go compiler version. For 1.17 and above, combined is returned. For pre
// 1.17, separate is returned.
func ifCombinedAggregateArgs(combined, separate Args) Args {
	if combinedAggregateArgs {
		return combined
	}
	return separate
}
