// Copyright 2017 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 pkgtree

import (
	"fmt"
	"go/build"
	"go/scanner"
	"go/token"
	"io/ioutil"
	"os"
	"path"
	"path/filepath"
	"reflect"
	"runtime"
	"strings"
	"testing"

	"github.com/golang/dep/gps/paths"
	"github.com/golang/dep/internal/fs"
	_ "github.com/golang/dep/internal/test" // DO NOT REMOVE, allows go test ./... -update to work
	"github.com/google/go-cmp/cmp"
)

// PackageTree.ToReachMap() uses an easily separable algorithm, wmToReach(),
// to turn a discovered set of packages and their imports into a proper pair of
// internal and external reach maps.
//
// That algorithm is purely symbolic (no filesystem interaction), and thus is
// easy to test. This is that test.
func TestWorkmapToReach(t *testing.T) {
	empty := func() map[string]bool {
		return make(map[string]bool)
	}

	table := map[string]struct {
		workmap  map[string]wm
		rm       ReachMap
		em       map[string]*ProblemImportError
		backprop bool
	}{
		"single": {
			workmap: map[string]wm{
				"foo": {
					ex: empty(),
					in: empty(),
				},
			},
			rm: ReachMap{
				"foo": {},
			},
		},
		"no external": {
			workmap: map[string]wm{
				"foo": {
					ex: empty(),
					in: empty(),
				},
				"foo/bar": {
					ex: empty(),
					in: empty(),
				},
			},
			rm: ReachMap{
				"foo":     {},
				"foo/bar": {},
			},
		},
		"no external with subpkg": {
			workmap: map[string]wm{
				"foo": {
					ex: empty(),
					in: map[string]bool{
						"foo/bar": true,
					},
				},
				"foo/bar": {
					ex: empty(),
					in: empty(),
				},
			},
			rm: ReachMap{
				"foo": {
					Internal: []string{"foo/bar"},
				},
				"foo/bar": {},
			},
		},
		"simple base transitive": {
			workmap: map[string]wm{
				"foo": {
					ex: empty(),
					in: map[string]bool{
						"foo/bar": true,
					},
				},
				"foo/bar": {
					ex: map[string]bool{
						"baz": true,
					},
					in: empty(),
				},
			},
			rm: ReachMap{
				"foo": {
					External: []string{"baz"},
					Internal: []string{"foo/bar"},
				},
				"foo/bar": {
					External: []string{"baz"},
				},
			},
		},
		"missing package is poison": {
			workmap: map[string]wm{
				"A": {
					ex: map[string]bool{
						"B/foo": true,
					},
					in: map[string]bool{
						"A/foo": true, // missing
						"A/bar": true,
					},
				},
				"A/bar": {
					ex: map[string]bool{
						"B/baz": true,
					},
					in: empty(),
				},
			},
			rm: ReachMap{
				"A/bar": {
					External: []string{"B/baz"},
				},
			},
			em: map[string]*ProblemImportError{
				"A": {
					ImportPath: "A",
					Cause:      []string{"A/foo"},
					Err:        missingPkgErr("A/foo"),
				},
			},
			backprop: true,
		},
		"transitive missing package is poison": {
			workmap: map[string]wm{
				"A": {
					ex: map[string]bool{
						"B/foo": true,
					},
					in: map[string]bool{
						"A/foo":  true, // transitively missing
						"A/quux": true,
					},
				},
				"A/foo": {
					ex: map[string]bool{
						"C/flugle": true,
					},
					in: map[string]bool{
						"A/bar": true, // missing
					},
				},
				"A/quux": {
					ex: map[string]bool{
						"B/baz": true,
					},
					in: empty(),
				},
			},
			rm: ReachMap{
				"A/quux": {
					External: []string{"B/baz"},
				},
			},
			em: map[string]*ProblemImportError{
				"A": {
					ImportPath: "A",
					Cause:      []string{"A/foo", "A/bar"},
					Err:        missingPkgErr("A/bar"),
				},
				"A/foo": {
					ImportPath: "A/foo",
					Cause:      []string{"A/bar"},
					Err:        missingPkgErr("A/bar"),
				},
			},
			backprop: true,
		},
		"err'd package is poison": {
			workmap: map[string]wm{
				"A": {
					ex: map[string]bool{
						"B/foo": true,
					},
					in: map[string]bool{
						"A/foo": true, // err'd
						"A/bar": true,
					},
				},
				"A/foo": {
					err: fmt.Errorf("err pkg"),
				},
				"A/bar": {
					ex: map[string]bool{
						"B/baz": true,
					},
					in: empty(),
				},
			},
			rm: ReachMap{
				"A/bar": {
					External: []string{"B/baz"},
				},
			},
			em: map[string]*ProblemImportError{
				"A": {
					ImportPath: "A",
					Cause:      []string{"A/foo"},
					Err:        fmt.Errorf("err pkg"),
				},
				"A/foo": {
					ImportPath: "A/foo",
					Err:        fmt.Errorf("err pkg"),
				},
			},
			backprop: true,
		},
		"transitive err'd package is poison": {
			workmap: map[string]wm{
				"A": {
					ex: map[string]bool{
						"B/foo": true,
					},
					in: map[string]bool{
						"A/foo":  true, // transitively err'd
						"A/quux": true,
					},
				},
				"A/foo": {
					ex: map[string]bool{
						"C/flugle": true,
					},
					in: map[string]bool{
						"A/bar": true, // err'd
					},
				},
				"A/bar": {
					err: fmt.Errorf("err pkg"),
				},
				"A/quux": {
					ex: map[string]bool{
						"B/baz": true,
					},
					in: empty(),
				},
			},
			rm: ReachMap{
				"A/quux": {
					External: []string{"B/baz"},
				},
			},
			em: map[string]*ProblemImportError{
				"A": {
					ImportPath: "A",
					Cause:      []string{"A/foo", "A/bar"},
					Err:        fmt.Errorf("err pkg"),
				},
				"A/foo": {
					ImportPath: "A/foo",
					Cause:      []string{"A/bar"},
					Err:        fmt.Errorf("err pkg"),
				},
				"A/bar": {
					ImportPath: "A/bar",
					Err:        fmt.Errorf("err pkg"),
				},
			},
			backprop: true,
		},
		"transitive err'd package no backprop": {
			workmap: map[string]wm{
				"A": {
					ex: map[string]bool{
						"B/foo": true,
					},
					in: map[string]bool{
						"A/foo":  true, // transitively err'd
						"A/quux": true,
					},
				},
				"A/foo": {
					ex: map[string]bool{
						"C/flugle": true,
					},
					in: map[string]bool{
						"A/bar": true, // err'd
					},
				},
				"A/bar": {
					err: fmt.Errorf("err pkg"),
				},
				"A/quux": {
					ex: map[string]bool{
						"B/baz": true,
					},
					in: empty(),
				},
			},
			rm: ReachMap{
				"A": {
					Internal: []string{"A/bar", "A/foo", "A/quux"},
					//Internal: []string{"A/foo", "A/quux"},
					External: []string{"B/baz", "B/foo", "C/flugle"},
				},
				"A/foo": {
					Internal: []string{"A/bar"},
					External: []string{"C/flugle"},
				},
				"A/quux": {
					External: []string{"B/baz"},
				},
			},
			em: map[string]*ProblemImportError{
				"A/bar": {
					ImportPath: "A/bar",
					Err:        fmt.Errorf("err pkg"),
				},
			},
		},
		// The following tests are mostly about regressions and weeding out
		// weird assumptions
		"internal diamond": {
			workmap: map[string]wm{
				"A": {
					ex: map[string]bool{
						"B/foo": true,
					},
					in: map[string]bool{
						"A/foo": true,
						"A/bar": true,
					},
				},
				"A/foo": {
					ex: map[string]bool{
						"C": true,
					},
					in: map[string]bool{
						"A/quux": true,
					},
				},
				"A/bar": {
					ex: map[string]bool{
						"D": true,
					},
					in: map[string]bool{
						"A/quux": true,
					},
				},
				"A/quux": {
					ex: map[string]bool{
						"B/baz": true,
					},
					in: empty(),
				},
			},
			rm: ReachMap{
				"A": {
					External: []string{
						"B/baz",
						"B/foo",
						"C",
						"D",
					},
					Internal: []string{
						"A/bar",
						"A/foo",
						"A/quux",
					},
				},
				"A/foo": {
					External: []string{
						"B/baz",
						"C",
					},
					Internal: []string{
						"A/quux",
					},
				},
				"A/bar": {
					External: []string{
						"B/baz",
						"D",
					},
					Internal: []string{
						"A/quux",
					},
				},
				"A/quux": {
					External: []string{"B/baz"},
				},
			},
		},
		"rootmost gets imported": {
			workmap: map[string]wm{
				"A": {
					ex: map[string]bool{
						"B": true,
					},
					in: empty(),
				},
				"A/foo": {
					ex: map[string]bool{
						"C": true,
					},
					in: map[string]bool{
						"A": true,
					},
				},
			},
			rm: ReachMap{
				"A": {
					External: []string{"B"},
				},
				"A/foo": {
					External: []string{
						"B",
						"C",
					},
					Internal: []string{
						"A",
					},
				},
			},
		},
		"self cycle": {
			workmap: map[string]wm{
				"A": {in: map[string]bool{"A": true}},
			},
			rm: ReachMap{
				"A": {Internal: []string{"A"}},
			},
		},
		"simple cycle": {
			workmap: map[string]wm{
				"A": {in: map[string]bool{"B": true}},
				"B": {in: map[string]bool{"A": true}},
			},
			rm: ReachMap{
				"A": {Internal: []string{"A", "B"}},
				"B": {Internal: []string{"A", "B"}},
			},
		},
		"cycle with external dependency": {
			workmap: map[string]wm{
				"A": {
					in: map[string]bool{"B": true},
				},
				"B": {
					ex: map[string]bool{"C": true},
					in: map[string]bool{"A": true},
				},
			},
			rm: ReachMap{
				"A": {
					External: []string{"C"},
					Internal: []string{"A", "B"},
				},
				"B": {
					External: []string{"C"},
					Internal: []string{"A", "B"},
				},
			},
		},
		"cycle with transitive external dependency": {
			workmap: map[string]wm{
				"A": {
					in: map[string]bool{"B": true},
				},
				"B": {
					in: map[string]bool{"A": true, "C": true},
				},
				"C": {
					ex: map[string]bool{"D": true},
				},
			},
			rm: ReachMap{
				"A": {
					External: []string{"D"},
					Internal: []string{"A", "B", "C"},
				},
				"B": {
					External: []string{"D"},
					Internal: []string{"A", "B", "C"},
				},
				"C": {
					External: []string{"D"},
				},
			},
		},
		"internal cycle": {
			workmap: map[string]wm{
				"A": {
					ex: map[string]bool{"B": true},
					in: map[string]bool{"C": true},
				},
				"C": {
					in: map[string]bool{"D": true},
				},
				"D": {
					in: map[string]bool{"E": true},
				},
				"E": {
					in: map[string]bool{"C": true},
				},
			},
			rm: ReachMap{
				"A": {
					External: []string{"B"},
					Internal: []string{"C", "D", "E"},
				},
				"C": {
					Internal: []string{"C", "D", "E"},
				},
				"D": {
					Internal: []string{"C", "D", "E"},
				},
				"E": {
					Internal: []string{"C", "D", "E"},
				},
			},
		},
	}

	for name, fix := range table {
		name, fix := name, fix
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			// Avoid erroneous errors by initializing the fixture's error map if
			// needed
			if fix.em == nil {
				fix.em = make(map[string]*ProblemImportError)
			}

			rm, em := wmToReach(fix.workmap, fix.backprop)
			if diff := cmp.Diff(rm, fix.rm); diff != "" {
				//t.Error(pretty.Sprintf("wmToReach(%q): Did not get expected reach map:\n\t(GOT): %s\n\t(WNT): %s", name, rm, fix.rm))
				t.Errorf("Did not get expected reach map:\n\t(GOT): %s\n\t(WNT): %s", rm, fix.rm)
			}
			if diff := cmp.Diff(em, fix.em, cmp.Comparer(func(x error, y error) bool {
				return x.Error() == y.Error()
			})); diff != "" {
				//t.Error(pretty.Sprintf("wmToReach(%q): Did not get expected error map:\n\t(GOT): %# v\n\t(WNT): %# v", name, em, fix.em))
				t.Errorf("Did not get expected error map:\n\t(GOT): %v\n\t(WNT): %v", em, fix.em)
			}
		})
	}
}

func TestListPackagesNoDir(t *testing.T) {
	out, err := ListPackages(filepath.Join(getTestdataRootDir(t), "notexist"), "notexist")
	if err == nil {
		t.Error("ListPackages should have errored on pointing to a nonexistent dir")
	}
	if !reflect.DeepEqual(PackageTree{}, out) {
		t.Error("should've gotten back an empty PackageTree")
	}
}

func TestListPackages(t *testing.T) {
	srcdir := filepath.Join(getTestdataRootDir(t), "src")
	j := func(s ...string) string {
		return filepath.Join(srcdir, filepath.Join(s...))
	}

	table := map[string]struct {
		fileRoot   string
		importRoot string
		out        PackageTree
		err        error
	}{
		"empty": {
			fileRoot:   j("empty"),
			importRoot: "empty",
			out: PackageTree{
				ImportRoot: "empty",
				Packages: map[string]PackageOrErr{
					"empty": {
						Err: &build.NoGoError{
							Dir: j("empty"),
						},
					},
				},
			},
		},
		"code only": {
			fileRoot:   j("simple"),
			importRoot: "simple",
			out: PackageTree{
				ImportRoot: "simple",
				Packages: map[string]PackageOrErr{
					"simple": {
						P: Package{
							ImportPath:  "simple",
							CommentPath: "",
							Name:        "simple",
							Imports: []string{
								"github.com/golang/dep/gps",
								"sort",
							},
						},
					},
				},
			},
		},
		"impose import path": {
			fileRoot:   j("simple"),
			importRoot: "arbitrary",
			out: PackageTree{
				ImportRoot: "arbitrary",
				Packages: map[string]PackageOrErr{
					"arbitrary": {
						P: Package{
							ImportPath:  "arbitrary",
							CommentPath: "",
							Name:        "simple",
							Imports: []string{
								"github.com/golang/dep/gps",
								"sort",
							},
						},
					},
				},
			},
		},
		"test only": {
			fileRoot:   j("t"),
			importRoot: "simple",
			out: PackageTree{
				ImportRoot: "simple",
				Packages: map[string]PackageOrErr{
					"simple": {
						P: Package{
							ImportPath:  "simple",
							CommentPath: "",
							Name:        "simple",
							Imports:     []string{},
							TestImports: []string{
								"math/rand",
								"strconv",
							},
						},
					},
				},
			},
		},
		"xtest only": {
			fileRoot:   j("xt"),
			importRoot: "simple",
			out: PackageTree{
				ImportRoot: "simple",
				Packages: map[string]PackageOrErr{
					"simple": {
						P: Package{
							ImportPath:  "simple",
							CommentPath: "",
							Name:        "simple",
							Imports:     []string{},
							TestImports: []string{
								"sort",
								"strconv",
							},
						},
					},
				},
			},
		},
		"code and test": {
			fileRoot:   j("simplet"),
			importRoot: "simple",
			out: PackageTree{
				ImportRoot: "simple",
				Packages: map[string]PackageOrErr{
					"simple": {
						P: Package{
							ImportPath:  "simple",
							CommentPath: "",
							Name:        "simple",
							Imports: []string{
								"github.com/golang/dep/gps",
								"sort",
							},
							TestImports: []string{
								"math/rand",
								"strconv",
							},
						},
					},
				},
			},
		},
		"code and xtest": {
			fileRoot:   j("simplext"),
			importRoot: "simple",
			out: PackageTree{
				ImportRoot: "simple",
				Packages: map[string]PackageOrErr{
					"simple": {
						P: Package{
							ImportPath:  "simple",
							CommentPath: "",
							Name:        "simple",
							Imports: []string{
								"github.com/golang/dep/gps",
								"sort",
							},
							TestImports: []string{
								"sort",
								"strconv",
							},
						},
					},
				},
			},
		},
		"code, test, xtest": {
			fileRoot:   j("simpleallt"),
			importRoot: "simple",
			out: PackageTree{
				ImportRoot: "simple",
				Packages: map[string]PackageOrErr{
					"simple": {
						P: Package{
							ImportPath:  "simple",
							CommentPath: "",
							Name:        "simple",
							Imports: []string{
								"github.com/golang/dep/gps",
								"sort",
							},
							TestImports: []string{
								"math/rand",
								"sort",
								"strconv",
							},
						},
					},
				},
			},
		},
		"one pkg multifile": {
			fileRoot:   j("m1p"),
			importRoot: "m1p",
			out: PackageTree{
				ImportRoot: "m1p",
				Packages: map[string]PackageOrErr{
					"m1p": {
						P: Package{
							ImportPath:  "m1p",
							CommentPath: "",
							Name:        "m1p",
							Imports: []string{
								"github.com/golang/dep/gps",
								"os",
								"sort",
							},
						},
					},
				},
			},
		},
		"one nested below": {
			fileRoot:   j("nest"),
			importRoot: "nest",
			out: PackageTree{
				ImportRoot: "nest",
				Packages: map[string]PackageOrErr{
					"nest": {
						P: Package{
							ImportPath:  "nest",
							CommentPath: "",
							Name:        "simple",
							Imports: []string{
								"github.com/golang/dep/gps",
								"sort",
							},
						},
					},
					"nest/m1p": {
						P: Package{
							ImportPath:  "nest/m1p",
							CommentPath: "",
							Name:        "m1p",
							Imports: []string{
								"github.com/golang/dep/gps",
								"os",
								"sort",
							},
						},
					},
				},
			},
		},
		"malformed go file": {
			fileRoot:   j("bad"),
			importRoot: "bad",
			out: PackageTree{
				ImportRoot: "bad",
				Packages: map[string]PackageOrErr{
					"bad": {
						Err: scanner.ErrorList{
							&scanner.Error{
								Pos: token.Position{
									Filename: j("bad", "bad.go"),
									Offset:   273,
									Line:     6,
									Column:   43,
								},
								Msg: "expected 'package', found 'EOF'",
							},
						},
					},
				},
			},
		},
		"two nested under empty root": {
			fileRoot:   j("ren"),
			importRoot: "ren",
			out: PackageTree{
				ImportRoot: "ren",
				Packages: map[string]PackageOrErr{
					"ren": {
						Err: &build.NoGoError{
							Dir: j("ren"),
						},
					},
					"ren/m1p": {
						P: Package{
							ImportPath:  "ren/m1p",
							CommentPath: "",
							Name:        "m1p",
							Imports: []string{
								"github.com/golang/dep/gps",
								"os",
								"sort",
							},
						},
					},
					"ren/simple": {
						P: Package{
							ImportPath:  "ren/simple",
							CommentPath: "",
							Name:        "simple",
							Imports: []string{
								"github.com/golang/dep/gps",
								"sort",
							},
						},
					},
				},
			},
		},
		"internal name mismatch": {
			fileRoot:   j("doublenest"),
			importRoot: "doublenest",
			out: PackageTree{
				ImportRoot: "doublenest",
				Packages: map[string]PackageOrErr{
					"doublenest": {
						P: Package{
							ImportPath:  "doublenest",
							CommentPath: "",
							Name:        "base",
							Imports: []string{
								"github.com/golang/dep/gps",
								"go/parser",
							},
						},
					},
					"doublenest/namemismatch": {
						P: Package{
							ImportPath:  "doublenest/namemismatch",
							CommentPath: "",
							Name:        "nm",
							Imports: []string{
								"github.com/Masterminds/semver",
								"os",
							},
						},
					},
					"doublenest/namemismatch/m1p": {
						P: Package{
							ImportPath:  "doublenest/namemismatch/m1p",
							CommentPath: "",
							Name:        "m1p",
							Imports: []string{
								"github.com/golang/dep/gps",
								"os",
								"sort",
							},
						},
					},
				},
			},
		},
		"file and importroot mismatch": {
			fileRoot:   j("doublenest"),
			importRoot: "other",
			out: PackageTree{
				ImportRoot: "other",
				Packages: map[string]PackageOrErr{
					"other": {
						P: Package{
							ImportPath:  "other",
							CommentPath: "",
							Name:        "base",
							Imports: []string{
								"github.com/golang/dep/gps",
								"go/parser",
							},
						},
					},
					"other/namemismatch": {
						P: Package{
							ImportPath:  "other/namemismatch",
							CommentPath: "",
							Name:        "nm",
							Imports: []string{
								"github.com/Masterminds/semver",
								"os",
							},
						},
					},
					"other/namemismatch/m1p": {
						P: Package{
							ImportPath:  "other/namemismatch/m1p",
							CommentPath: "",
							Name:        "m1p",
							Imports: []string{
								"github.com/golang/dep/gps",
								"os",
								"sort",
							},
						},
					},
				},
			},
		},
		"code and ignored main": {
			fileRoot:   j("igmain"),
			importRoot: "simple",
			out: PackageTree{
				ImportRoot: "simple",
				Packages: map[string]PackageOrErr{
					"simple": {
						P: Package{
							ImportPath:  "simple",
							CommentPath: "",
							Name:        "simple",
							Imports: []string{
								"github.com/golang/dep/gps",
								"sort",
								"unicode",
							},
						},
					},
				},
			},
		},
		"code and ignored main, order check": {
			fileRoot:   j("igmainfirst"),
			importRoot: "simple",
			out: PackageTree{
				ImportRoot: "simple",
				Packages: map[string]PackageOrErr{
					"simple": {
						P: Package{
							ImportPath:  "simple",
							CommentPath: "",
							Name:        "simple",
							Imports: []string{
								"github.com/golang/dep/gps",
								"sort",
								"unicode",
							},
						},
					},
				},
			},
		},
		"code and ignored main with comment leader": {
			fileRoot:   j("igmainlong"),
			importRoot: "simple",
			out: PackageTree{
				ImportRoot: "simple",
				Packages: map[string]PackageOrErr{
					"simple": {
						P: Package{
							ImportPath:  "simple",
							CommentPath: "",
							Name:        "simple",
							Imports: []string{
								"github.com/golang/dep/gps",
								"sort",
								"unicode",
							},
						},
					},
				},
			},
		},
		"code, tests, and ignored main": {
			fileRoot:   j("igmaint"),
			importRoot: "simple",
			out: PackageTree{
				ImportRoot: "simple",
				Packages: map[string]PackageOrErr{
					"simple": {
						P: Package{
							ImportPath:  "simple",
							CommentPath: "",
							Name:        "simple",
							Imports: []string{
								"github.com/golang/dep/gps",
								"sort",
								"unicode",
							},
							TestImports: []string{
								"math/rand",
								"strconv",
							},
						},
					},
				},
			},
		},
		// imports a missing pkg
		"missing import": {
			fileRoot:   j("missing"),
			importRoot: "missing",
			out: PackageTree{
				ImportRoot: "missing",
				Packages: map[string]PackageOrErr{
					"missing": {
						P: Package{
							ImportPath:  "missing",
							CommentPath: "",
							Name:        "simple",
							Imports: []string{
								"github.com/golang/dep/gps",
								"missing/missing",
								"sort",
							},
						},
					},
					"missing/m1p": {
						P: Package{
							ImportPath:  "missing/m1p",
							CommentPath: "",
							Name:        "m1p",
							Imports: []string{
								"github.com/golang/dep/gps",
								"os",
								"sort",
							},
						},
					},
				},
			},
		},
		// import cycle of three packages. ListPackages doesn't do anything
		// special with cycles - that's the reach calculator's job - so this is
		// error-free
		"import cycle, len 3": {
			fileRoot:   j("cycle"),
			importRoot: "cycle",
			out: PackageTree{
				ImportRoot: "cycle",
				Packages: map[string]PackageOrErr{
					"cycle": {
						P: Package{
							ImportPath:  "cycle",
							CommentPath: "",
							Name:        "cycle",
							Imports: []string{
								"cycle/one",
								"github.com/golang/dep/gps",
							},
						},
					},
					"cycle/one": {
						P: Package{
							ImportPath:  "cycle/one",
							CommentPath: "",
							Name:        "one",
							Imports: []string{
								"cycle/two",
								"github.com/golang/dep/gps",
							},
						},
					},
					"cycle/two": {
						P: Package{
							ImportPath:  "cycle/two",
							CommentPath: "",
							Name:        "two",
							Imports: []string{
								"cycle",
								"github.com/golang/dep/gps",
							},
						},
					},
				},
			},
		},
		// has disallowed dir names
		"disallowed dirs": {
			fileRoot:   j("disallow"),
			importRoot: "disallow",
			out: PackageTree{
				ImportRoot: "disallow",
				Packages: map[string]PackageOrErr{
					"disallow": {
						P: Package{
							ImportPath:  "disallow",
							CommentPath: "",
							Name:        "disallow",
							Imports: []string{
								"disallow/testdata",
								"github.com/golang/dep/gps",
								"sort",
							},
						},
					},
					"disallow/testdata": {
						P: Package{
							ImportPath:  "disallow/testdata",
							CommentPath: "",
							Name:        "testdata",
							Imports: []string{
								"hash",
							},
						},
					},
				},
			},
		},
		"relative imports": {
			fileRoot:   j("relimport"),
			importRoot: "relimport",
			out: PackageTree{
				ImportRoot: "relimport",
				Packages: map[string]PackageOrErr{
					"relimport": {
						P: Package{
							ImportPath:  "relimport",
							CommentPath: "",
							Name:        "relimport",
							Imports: []string{
								"sort",
							},
						},
					},
					"relimport/dot": {
						P: Package{
							ImportPath:  "relimport/dot",
							CommentPath: "",
							Name:        "dot",
							Imports: []string{
								".",
								"sort",
							},
						},
					},
					"relimport/dotdot": {
						Err: &LocalImportsError{
							Dir:        j("relimport/dotdot"),
							ImportPath: "relimport/dotdot",
							LocalImports: []string{
								"..",
							},
						},
					},
					"relimport/dotslash": {
						Err: &LocalImportsError{
							Dir:        j("relimport/dotslash"),
							ImportPath: "relimport/dotslash",
							LocalImports: []string{
								"./simple",
							},
						},
					},
					"relimport/dotdotslash": {
						Err: &LocalImportsError{
							Dir:        j("relimport/dotdotslash"),
							ImportPath: "relimport/dotdotslash",
							LocalImports: []string{
								"../github.com/golang/dep/gps",
							},
						},
					},
				},
			},
		},
		"skip underscore": {
			fileRoot:   j("skip_"),
			importRoot: "skip_",
			out: PackageTree{
				ImportRoot: "skip_",
				Packages: map[string]PackageOrErr{
					"skip_": {
						P: Package{
							ImportPath:  "skip_",
							CommentPath: "",
							Name:        "skip",
							Imports: []string{
								"github.com/golang/dep/gps",
								"sort",
							},
						},
					},
				},
			},
		},
		// This case mostly exists for the PackageTree methods, but it does
		// cover a bit of range
		"varied": {
			fileRoot:   j("varied"),
			importRoot: "varied",
			out: PackageTree{
				ImportRoot: "varied",
				Packages: map[string]PackageOrErr{
					"varied": {
						P: Package{
							ImportPath:  "varied",
							CommentPath: "",
							Name:        "main",
							Imports: []string{
								"net/http",
								"varied/namemismatch",
								"varied/otherpath",
								"varied/simple",
							},
						},
					},
					"varied/otherpath": {
						P: Package{
							ImportPath:  "varied/otherpath",
							CommentPath: "",
							Name:        "otherpath",
							Imports:     []string{},
							TestImports: []string{
								"varied/m1p",
							},
						},
					},
					"varied/simple": {
						P: Package{
							ImportPath:  "varied/simple",
							CommentPath: "",
							Name:        "simple",
							Imports: []string{
								"github.com/golang/dep/gps",
								"go/parser",
								"varied/simple/another",
							},
						},
					},
					"varied/simple/another": {
						P: Package{
							ImportPath:  "varied/simple/another",
							CommentPath: "",
							Name:        "another",
							Imports: []string{
								"hash",
								"varied/m1p",
							},
							TestImports: []string{
								"encoding/binary",
							},
						},
					},
					"varied/namemismatch": {
						P: Package{
							ImportPath:  "varied/namemismatch",
							CommentPath: "",
							Name:        "nm",
							Imports: []string{
								"github.com/Masterminds/semver",
								"os",
							},
						},
					},
					"varied/m1p": {
						P: Package{
							ImportPath:  "varied/m1p",
							CommentPath: "",
							Name:        "m1p",
							Imports: []string{
								"github.com/golang/dep/gps",
								"os",
								"sort",
							},
						},
					},
				},
			},
		},
		"varied with hidden dirs": {
			fileRoot:   j("varied_hidden"),
			importRoot: "varied",
			out: PackageTree{
				ImportRoot: "varied",
				Packages: map[string]PackageOrErr{
					"varied": {
						P: Package{
							ImportPath:  "varied",
							CommentPath: "",
							Name:        "main",
							Imports: []string{
								"net/http",
								"varied/_frommain",
								"varied/simple",
							},
						},
					},
					"varied/always": {
						P: Package{
							ImportPath:  "varied/always",
							CommentPath: "",
							Name:        "always",
							Imports:     []string{},
							TestImports: []string{
								"varied/.onlyfromtests",
							},
						},
					},
					"varied/.onlyfromtests": {
						P: Package{
							ImportPath:  "varied/.onlyfromtests",
							CommentPath: "",
							Name:        "onlyfromtests",
							Imports: []string{
								"github.com/golang/dep/gps",
								"os",
								"sort",
								"varied/_secondorder",
							},
						},
					},
					"varied/simple": {
						P: Package{
							ImportPath:  "varied/simple",
							CommentPath: "",
							Name:        "simple",
							Imports: []string{
								"github.com/golang/dep/gps",
								"go/parser",
								"varied/simple/testdata",
							},
						},
					},
					"varied/simple/testdata": {
						P: Package{
							ImportPath:  "varied/simple/testdata",
							CommentPath: "",
							Name:        "testdata",
							Imports: []string{
								"varied/dotdotslash",
							},
						},
					},
					"varied/_secondorder": {
						P: Package{
							ImportPath:  "varied/_secondorder",
							CommentPath: "",
							Name:        "secondorder",
							Imports: []string{
								"hash",
							},
						},
					},
					"varied/_never": {
						P: Package{
							ImportPath:  "varied/_never",
							CommentPath: "",
							Name:        "never",
							Imports: []string{
								"github.com/golang/dep/gps",
								"sort",
							},
						},
					},
					"varied/_frommain": {
						P: Package{
							ImportPath:  "varied/_frommain",
							CommentPath: "",
							Name:        "frommain",
							Imports: []string{
								"github.com/golang/dep/gps",
								"sort",
							},
						},
					},
					"varied/dotdotslash": {
						Err: &LocalImportsError{
							Dir:        j("varied_hidden/dotdotslash"),
							ImportPath: "varied/dotdotslash",
							LocalImports: []string{
								"../github.com/golang/dep/gps",
							},
						},
					},
				},
			},
		},
		"invalid buildtag like comments should be ignored": {
			fileRoot:   j("buildtag"),
			importRoot: "buildtag",
			out: PackageTree{
				ImportRoot: "buildtag",
				Packages: map[string]PackageOrErr{
					"buildtag": {
						P: Package{
							ImportPath:  "buildtag",
							CommentPath: "",
							Name:        "buildtag",
							Imports: []string{
								"sort",
							},
						},
					},
				},
			},
		},
		"does not skip directories starting with '.'": {
			fileRoot:   j("dotgodir"),
			importRoot: "dotgodir",
			out: PackageTree{
				ImportRoot: "dotgodir",
				Packages: map[string]PackageOrErr{
					"dotgodir": {
						P: Package{
							ImportPath: "dotgodir",
							Imports:    []string{},
						},
					},
					"dotgodir/.go": {
						P: Package{
							ImportPath: "dotgodir/.go",
							Name:       "dot",
							Imports:    []string{},
						},
					},
					"dotgodir/foo.go": {
						P: Package{
							ImportPath: "dotgodir/foo.go",
							Name:       "foo",
							Imports:    []string{"sort"},
						},
					},
					"dotgodir/.m1p": {
						P: Package{
							ImportPath:  "dotgodir/.m1p",
							CommentPath: "",
							Name:        "m1p",
							Imports: []string{
								"github.com/golang/dep/gps",
								"os",
								"sort",
							},
						},
					},
				},
			},
		},
		"canonical": {
			fileRoot:   j("canonical"),
			importRoot: "canonical",
			out: PackageTree{
				ImportRoot: "canonical",
				Packages: map[string]PackageOrErr{
					"canonical": {
						P: Package{
							ImportPath:  "canonical",
							CommentPath: "canonical",
							Name:        "pkg",
							Imports:     []string{},
						},
					},
					"canonical/sub": {
						P: Package{
							ImportPath:  "canonical/sub",
							CommentPath: "canonical/subpackage",
							Name:        "sub",
							Imports:     []string{},
						},
					},
				},
			},
		},
		"conflicting canonical comments": {
			fileRoot:   j("canon_confl"),
			importRoot: "canon_confl",
			out: PackageTree{
				ImportRoot: "canon_confl",
				Packages: map[string]PackageOrErr{
					"canon_confl": {
						Err: &ConflictingImportComments{
							ImportPath: "canon_confl",
							ConflictingImportComments: []string{
								"vanity1",
								"vanity2",
							},
						},
					},
				},
			},
		},
		"non-canonical": {
			fileRoot:   j("canonical"),
			importRoot: "noncanonical",
			out: PackageTree{
				ImportRoot: "noncanonical",
				Packages: map[string]PackageOrErr{
					"noncanonical": {
						Err: &NonCanonicalImportRoot{
							ImportRoot: "noncanonical",
							Canonical:  "canonical",
						},
					},
					"noncanonical/sub": {
						Err: &NonCanonicalImportRoot{
							ImportRoot: "noncanonical",
							Canonical:  "canonical/subpackage",
						},
					},
				},
			},
		},
		"slash-star": {
			fileRoot:   j("slash-star_confl"),
			importRoot: "slash-star_confl",
			out: PackageTree{
				ImportRoot: "slash-star_confl",
				Packages: map[string]PackageOrErr{
					"slash-star_confl": {
						Err: &ConflictingImportComments{
							ImportPath: "slash-star_confl",
							ConflictingImportComments: []string{
								"vanity1",
								"vanity2",
							},
						},
					},
				},
			},
		},
	}

	for name, fix := range table {
		t.Run(name, func(t *testing.T) {
			if _, err := os.Stat(fix.fileRoot); err != nil {
				t.Errorf("error on fileRoot %s: %s", fix.fileRoot, err)
			}

			out, err := ListPackages(fix.fileRoot, fix.importRoot)

			if err != nil && fix.err == nil {
				t.Errorf("Received error but none expected: %s", err)
			} else if fix.err != nil && err == nil {
				t.Errorf("Error expected but none received")
			} else if fix.err != nil && err != nil {
				if !reflect.DeepEqual(fix.err, err) {
					t.Errorf("Did not receive expected error:\n\t(GOT): %s\n\t(WNT): %s", err, fix.err)
				}
			}

			if fix.out.ImportRoot != "" && fix.out.Packages != nil {
				if !reflect.DeepEqual(out, fix.out) {
					if fix.out.ImportRoot != out.ImportRoot {
						t.Errorf("Expected ImportRoot %s, got %s", fix.out.ImportRoot, out.ImportRoot)
					}

					// overwrite the out one to see if we still have a real problem
					out.ImportRoot = fix.out.ImportRoot

					if !reflect.DeepEqual(out, fix.out) {
						// TODO (kris-nova) We need to disable this bypass here, and in the .travis.yml
						// as soon as dep#501 is fixed
						bypass := os.Getenv("DEPTESTBYPASS501")
						if bypass != "" {
							t.Log("bypassing fix.out.Packages check < 2")
						} else {
							if len(fix.out.Packages) < 2 {
								t.Errorf("Did not get expected PackageOrErrs:\n\t(GOT): %#v\n\t(WNT): %#v", out, fix.out)
							} else {
								seen := make(map[string]bool)
								for path, perr := range fix.out.Packages {
									seen[path] = true
									if operr, exists := out.Packages[path]; !exists {
										t.Errorf("Expected PackageOrErr for path %s was missing from output:\n\t%s", path, perr)
									} else {
										if !reflect.DeepEqual(perr, operr) {
											t.Errorf("PkgOrErr for path %s was not as expected:\n\t(GOT): %#v\n\t(WNT): %#v", path, operr, perr)
										}
									}
								}

								for path, operr := range out.Packages {
									if seen[path] {
										continue
									}

									t.Errorf("Got PackageOrErr for path %s, but none was expected:\n\t%s", path, operr)
								}
							}
						}
					}
				}
			}
		})
	}
}

// Transform Table Test that operates solely on the varied_hidden fixture.
func TestTrimHiddenPackages(t *testing.T) {
	base, err := ListPackages(filepath.Join(getTestdataRootDir(t), "src", "varied_hidden"), "varied")
	if err != nil {
		panic(err)
	}

	table := map[string]struct {
		main, tests bool     // literal params to TrimHiddenPackages
		ignore      []string // transformed into IgnoredRuleset param to TrimHiddenPackages
		trimmed     []string // list of packages that should be trimmed in result PackageTree
	}{
		// All of these implicitly verify that the varied/_never pkg is always
		// trimmed, and that the varied/dotdotslash pkg is not trimmed even
		// though it has errors.
		"minimal trim": {
			main:  true,
			tests: true,
		},
		"ignore simple, lose testdata": {
			main:    true,
			tests:   true,
			ignore:  []string{"simple"},
			trimmed: []string{"simple", "simple/testdata"},
		},
		"no tests": {
			main:    true,
			tests:   false,
			trimmed: []string{".onlyfromtests", "_secondorder"},
		},
		"ignore a reachable hidden": {
			main:    true,
			tests:   true,
			ignore:  []string{"_secondorder"},
			trimmed: []string{"_secondorder"},
		},
		"ignore a reachable hidden with another hidden solely reachable through it": {
			main:    true,
			tests:   true,
			ignore:  []string{".onlyfromtests"},
			trimmed: []string{".onlyfromtests", "_secondorder"},
		},
		"no main": {
			main:    false,
			tests:   true,
			trimmed: []string{"", "_frommain"},
		},
		"no main or tests": {
			main:    false,
			tests:   false,
			trimmed: []string{"", "_frommain", ".onlyfromtests", "_secondorder"},
		},
		"no main or tests and ignore simple": {
			main:    false,
			tests:   false,
			ignore:  []string{"simple"},
			trimmed: []string{"", "_frommain", ".onlyfromtests", "_secondorder", "simple", "simple/testdata"},
		},
	}

	for name, fix := range table {
		t.Run(name, func(t *testing.T) {
			want := base.Copy()

			var ig []string
			for _, v := range fix.ignore {
				ig = append(ig, path.Join("varied", v))
			}
			got := base.TrimHiddenPackages(fix.main, fix.tests, NewIgnoredRuleset(ig))

			for _, ip := range append(fix.trimmed, "_never") {
				ip = path.Join("varied", ip)
				if _, has := want.Packages[ip]; !has {
					panic(fmt.Sprintf("bad input, %s does not exist in fixture ptree", ip))
				}
				delete(want.Packages, ip)
			}

			if !reflect.DeepEqual(want, got) {
				if len(want.Packages) < 2 {
					t.Errorf("Did not get expected PackageOrErrs:\n\t(GOT): %#v\n\t(WNT): %#v", got, want)
				} else {
					seen := make(map[string]bool)
					for path, perr := range want.Packages {
						seen[path] = true
						if operr, exists := got.Packages[path]; !exists {
							t.Errorf("Expected PackageOrErr for path %s was missing from output:\n\t%s", path, perr)
						} else {
							if !reflect.DeepEqual(perr, operr) {
								t.Errorf("PkgOrErr for path %s was not as expected:\n\t(GOT): %#v\n\t(WNT): %#v", path, operr, perr)
							}
						}
					}

					for path, operr := range got.Packages {
						if seen[path] {
							continue
						}

						t.Errorf("Got PackageOrErr for path %s, but none was expected:\n\t%s", path, operr)
					}
				}
			}
		})
	}
}

// Test that ListPackages skips directories for which it lacks permissions to
// enter and files it lacks permissions to read.
func TestListPackagesNoPerms(t *testing.T) {
	if runtime.GOOS == "windows" {
		// TODO This test doesn't work on windows because I wasn't able to easily
		// figure out how to chmod a dir in a way that made it untraversable.
		//
		// It's not a big deal, though, because the os.IsPermission() call we
		// use in the real code is effectively what's being tested here, and
		// that's designed to be cross-platform. So, if the unix tests pass, we
		// have every reason to believe windows tests would too, if the situation
		// arises.
		t.Skip()
	}
	tmp, err := ioutil.TempDir("", "listpkgsnp")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %s", err)
	}
	defer os.RemoveAll(tmp)

	srcdir := filepath.Join(getTestdataRootDir(t), "src", "ren")
	workdir := filepath.Join(tmp, "ren")
	fs.CopyDir(srcdir, workdir)

	// chmod the simple dir and m1p/b.go file so they can't be read
	err = os.Chmod(filepath.Join(workdir, "simple"), 0)
	if err != nil {
		t.Fatalf("Error while chmodding simple dir: %s", err)
	}
	os.Chmod(filepath.Join(workdir, "m1p", "b.go"), 0)
	if err != nil {
		t.Fatalf("Error while chmodding b.go file: %s", err)
	}

	want := PackageTree{
		ImportRoot: "ren",
		Packages: map[string]PackageOrErr{
			"ren": {
				Err: &build.NoGoError{
					Dir: workdir,
				},
			},
			"ren/m1p": {
				P: Package{
					ImportPath:  "ren/m1p",
					CommentPath: "",
					Name:        "m1p",
					Imports: []string{
						"github.com/golang/dep/gps",
						"sort",
					},
				},
			},
		},
	}

	got, err := ListPackages(workdir, "ren")

	if err != nil {
		t.Fatalf("Unexpected err from ListPackages: %s", err)
	}
	if want.ImportRoot != got.ImportRoot {
		t.Fatalf("Expected ImportRoot %s, got %s", want.ImportRoot, got.ImportRoot)
	}

	if !reflect.DeepEqual(got, want) {
		t.Errorf("Did not get expected PackageOrErrs:\n\t(GOT): %#v\n\t(WNT): %#v", got, want)
		if len(got.Packages) != 2 {
			if len(got.Packages) == 3 {
				t.Error("Wrong number of PackageOrErrs - did 'simple' subpackage make it into results somehow?")
			} else {
				t.Error("Wrong number of PackageOrErrs")
			}
		}

		if got.Packages["ren"].Err == nil {
			t.Error("Should have gotten error on empty root directory")
		}

		if !reflect.DeepEqual(got.Packages["ren/m1p"].P.Imports, want.Packages["ren/m1p"].P.Imports) {
			t.Error("Mismatch between imports in m1p")
		}
	}
}

func TestToReachMap(t *testing.T) {
	// There's enough in the 'varied' test case to test most of what matters
	vptree, err := ListPackages(filepath.Join(getTestdataRootDir(t), "src", "github.com", "example", "varied"), "github.com/example/varied")
	if err != nil {
		t.Fatalf("ListPackages failed on varied test case: %s", err)
	}

	// Helper to add github.com/varied/example prefix
	b := func(s string) string {
		if s == "" {
			return "github.com/example/varied"
		}
		return "github.com/example/varied/" + s
	}
	bl := func(parts ...string) string {
		for k, s := range parts {
			parts[k] = b(s)
		}
		return strings.Join(parts, " ")
	}

	// Set up vars for validate closure
	var want ReachMap
	var name string
	var main, tests bool
	var ignore []string

	validate := func() {
		got, em := vptree.ToReachMap(main, tests, true, NewIgnoredRuleset(ignore))
		if len(em) != 0 {
			t.Errorf("Should not have any error packages from ToReachMap, got %s", em)
		}
		if !reflect.DeepEqual(want, got) {
			seen := make(map[string]bool)
			for ip, wantie := range want {
				seen[ip] = true
				if gotie, exists := got[ip]; !exists {
					t.Errorf("ver(%q): expected import path %s was not present in result", name, ip)
				} else {
					if !reflect.DeepEqual(wantie, gotie) {
						t.Errorf("ver(%q): did not get expected import set for pkg %s:\n\t(GOT): %#v\n\t(WNT): %#v", name, ip, gotie, wantie)
					}
				}
			}

			for ip, ie := range got {
				if seen[ip] {
					continue
				}
				t.Errorf("ver(%q): Got packages for import path %s, but none were expected:\n\t%s", name, ip, ie)
			}
		}
	}

	// maps of each internal package, and their expected external and internal
	// imports in the maximal case.
	allex := map[string][]string{
		b(""):               {"encoding/binary", "github.com/Masterminds/semver", "github.com/golang/dep/gps", "go/parser", "hash", "net/http", "os", "sort"},
		b("m1p"):            {"github.com/golang/dep/gps", "os", "sort"},
		b("namemismatch"):   {"github.com/Masterminds/semver", "os"},
		b("otherpath"):      {"github.com/golang/dep/gps", "os", "sort"},
		b("simple"):         {"encoding/binary", "github.com/golang/dep/gps", "go/parser", "hash", "os", "sort"},
		b("simple/another"): {"encoding/binary", "github.com/golang/dep/gps", "hash", "os", "sort"},
	}

	allin := map[string][]string{
		b(""):               {b("m1p"), b("namemismatch"), b("otherpath"), b("simple"), b("simple/another")},
		b("m1p"):            {},
		b("namemismatch"):   {},
		b("otherpath"):      {b("m1p")},
		b("simple"):         {b("m1p"), b("simple/another")},
		b("simple/another"): {b("m1p")},
	}

	// build a map to validate the exception inputs. do this because shit is
	// hard enough to keep track of that it's preferable not to have silent
	// success if a typo creeps in and we're trying to except an import that
	// isn't in a pkg in the first place
	valid := make(map[string]map[string]bool)
	for ip, expkgs := range allex {
		m := make(map[string]bool)
		for _, pkg := range expkgs {
			m[pkg] = true
		}
		valid[ip] = m
	}
	validin := make(map[string]map[string]bool)
	for ip, inpkgs := range allin {
		m := make(map[string]bool)
		for _, pkg := range inpkgs {
			m[pkg] = true
		}
		validin[ip] = m
	}

	// helper to compose want, excepting specific packages
	//
	// this makes it easier to see what we're taking out on each test
	except := func(pkgig ...string) {
		// reinit expect with everything from all
		want = make(ReachMap)
		for ip, expkgs := range allex {
			var ie struct{ Internal, External []string }

			inpkgs := allin[ip]
			lenex, lenin := len(expkgs), len(inpkgs)
			if lenex > 0 {
				ie.External = make([]string, len(expkgs))
				copy(ie.External, expkgs)
			}

			if lenin > 0 {
				ie.Internal = make([]string, len(inpkgs))
				copy(ie.Internal, inpkgs)
			}

			want[ip] = ie
		}

		// now build the dropmap
		drop := make(map[string]map[string]bool)
		for _, igstr := range pkgig {
			// split on space; first elem is import path to pkg, the rest are
			// the imports to drop.
			not := strings.Split(igstr, " ")
			var ip string
			ip, not = not[0], not[1:]
			if _, exists := valid[ip]; !exists {
				t.Fatalf("%s is not a package name we're working with, doofus", ip)
			}

			// if only a single elem was passed, though, drop the whole thing
			if len(not) == 0 {
				delete(want, ip)
				continue
			}

			m := make(map[string]bool)
			for _, imp := range not {
				if strings.HasPrefix(imp, "github.com/example/varied") {
					if !validin[ip][imp] {
						t.Fatalf("%s is not a reachable import of %s, even in the all case", imp, ip)
					}
				} else {
					if !valid[ip][imp] {
						t.Fatalf("%s is not a reachable import of %s, even in the all case", imp, ip)
					}
				}
				m[imp] = true
			}

			drop[ip] = m
		}

		for ip, ie := range want {
			var nie struct{ Internal, External []string }
			for _, imp := range ie.Internal {
				if !drop[ip][imp] {
					nie.Internal = append(nie.Internal, imp)
				}
			}

			for _, imp := range ie.External {
				if !drop[ip][imp] {
					nie.External = append(nie.External, imp)
				}
			}

			want[ip] = nie
		}
	}

	/* PREP IS DONE, BEGIN ACTUAL TESTING */

	// first, validate all
	name = "all"
	main, tests = true, true
	except()
	validate()

	// turn off main pkgs, which necessarily doesn't affect anything else
	name = "no main"
	main = false
	except(b(""))
	validate()

	// ignoring the "varied" pkg has same effect as disabling main pkgs
	name = "ignore root"
	ignore = []string{b("")}
	main = true
	validate()

	// when we drop tests, varied/otherpath loses its link to varied/m1p and
	// varied/simple/another loses its test import, which has a fairly big
	// cascade
	name = "no tests"
	tests = false
	ignore = nil
	except(
		b("")+" encoding/binary",
		b("simple")+" encoding/binary",
		b("simple/another")+" encoding/binary",
		b("otherpath")+" github.com/golang/dep/gps os sort",
	)

	// almost the same as previous, but varied just goes away completely
	name = "no main or tests"
	main = false
	except(
		b(""),
		b("simple")+" encoding/binary",
		b("simple/another")+" encoding/binary",
		bl("otherpath", "m1p")+" github.com/golang/dep/gps os sort",
	)
	validate()

	// focus on ignores now, so reset main and tests
	main, tests = true, true

	// now, the fun stuff. punch a hole in the middle by cutting out
	// varied/simple
	name = "ignore varied/simple"
	ignore = []string{b("simple")}
	except(
		// root pkg loses on everything in varied/simple/another
		// FIXME this is a bit odd, but should probably exclude m1p as well,
		// because it actually shouldn't be valid to import a package that only
		// has tests. This whole model misses that nuance right now, though.
		bl("", "simple", "simple/another")+" hash encoding/binary go/parser",
		b("simple"),
	)
	validate()

	// widen the hole by excluding otherpath
	name = "ignore varied/{otherpath,simple}"
	ignore = []string{
		b("otherpath"),
		b("simple"),
	}
	except(
		// root pkg loses on everything in varied/simple/another and varied/m1p
		bl("", "simple", "simple/another", "m1p", "otherpath")+" hash encoding/binary go/parser github.com/golang/dep/gps sort",
		b("otherpath"),
		b("simple"),
	)
	validate()

	// remove namemismatch, though we're mostly beating a dead horse now
	name = "ignore varied/{otherpath,simple,namemismatch}"
	ignore = append(ignore, b("namemismatch"))
	except(
		// root pkg loses on everything in varied/simple/another and varied/m1p
		bl("", "simple", "simple/another", "m1p", "otherpath", "namemismatch")+" hash encoding/binary go/parser github.com/golang/dep/gps sort os github.com/Masterminds/semver",
		b("otherpath"),
		b("simple"),
		b("namemismatch"),
	)
	validate()
}

func TestFlattenReachMap(t *testing.T) {
	// There's enough in the 'varied' test case to test most of what matters
	vptree, err := ListPackages(filepath.Join(getTestdataRootDir(t), "src", "github.com", "example", "varied"), "github.com/example/varied")
	if err != nil {
		t.Fatalf("listPackages failed on varied test case: %s", err)
	}

	all := []string{
		"encoding/binary",
		"github.com/Masterminds/semver",
		"github.com/golang/dep/gps",
		"go/parser",
		"hash",
		"net/http",
		"os",
		"sort",
	}

	// helper to generate testCase.expect as: all, except for a couple packages
	//
	// this makes it easier to see what we're taking out on each test
	except := func(not ...string) []string {
		expect := make([]string, len(all)-len(not))

		drop := make(map[string]bool)
		for _, npath := range not {
			drop[npath] = true
		}

		k := 0
		for _, path := range all {
			if !drop[path] {
				expect[k] = path
				k++
			}
		}
		return expect
	}

	for _, testCase := range []*flattenReachMapCase{
		// everything on
		{
			name:       "simple",
			expect:     except(),
			isStdLibFn: nil,
			main:       true,
			tests:      true,
		},
		// no stdlib
		{
			name:       "no stdlib",
			expect:     except("encoding/binary", "go/parser", "hash", "net/http", "os", "sort"),
			isStdLibFn: paths.IsStandardImportPath,
			main:       true,
			tests:      true,
		},
		// stdlib back in; now exclude tests, which should just cut one
		{
			name:       "no tests",
			expect:     except("encoding/binary"),
			isStdLibFn: nil,
			main:       true,
			tests:      false,
		},
		// Now skip main, which still just cuts out one
		{
			name:       "no main",
			expect:     except("net/http"),
			isStdLibFn: nil,
			main:       false,
			tests:      true,
		},
		// No test and no main, which should be additive
		{
			name:       "no tests, no main",
			expect:     except("net/http", "encoding/binary"),
			isStdLibFn: nil,
			main:       false,
			tests:      false,
		},
		// now, the ignore tests. turn main and tests back on
		// start with non-matching
		{
			name:       "non-matching ignore",
			expect:     except(),
			isStdLibFn: nil,
			main:       true,
			tests:      true,
			ignore: NewIgnoredRuleset([]string{
				"nomatch",
			}),
		},
		// should have the same effect as ignoring main
		{
			name:       "ignore the root",
			expect:     except("net/http"),
			isStdLibFn: nil,
			main:       true,
			tests:      true,
			ignore: NewIgnoredRuleset([]string{
				"github.com/example/varied",
			}),
		},
		// now drop a more interesting one
		// we get github.com/golang/dep/gps from m1p, too, so it should still be there
		{
			name:       "ignore simple",
			expect:     except("go/parser"),
			isStdLibFn: nil,
			main:       true,
			tests:      true,
			ignore: NewIgnoredRuleset([]string{
				"github.com/example/varied/simple",
			}),
		},
		// now drop two
		{
			name:       "ignore simple and nameismatch",
			expect:     except("go/parser", "github.com/Masterminds/semver"),
			isStdLibFn: nil,
			main:       true,
			tests:      true,
			ignore: NewIgnoredRuleset([]string{
				"github.com/example/varied/simple",
				"github.com/example/varied/namemismatch",
			}),
		},
		// make sure tests and main play nice with ignore
		{
			name:       "ignore simple and nameismatch, and no tests",
			expect:     except("go/parser", "github.com/Masterminds/semver", "encoding/binary"),
			isStdLibFn: nil,
			main:       true,
			tests:      false,
			ignore: NewIgnoredRuleset([]string{
				"github.com/example/varied/simple",
				"github.com/example/varied/namemismatch",
			}),
		},
		{
			name:       "ignore simple and namemismatch, and no main",
			expect:     except("go/parser", "github.com/Masterminds/semver", "net/http"),
			isStdLibFn: nil,
			main:       false,
			tests:      true,
			ignore: NewIgnoredRuleset([]string{
				"github.com/example/varied/simple",
				"github.com/example/varied/namemismatch",
			}),
		},
		{
			name:       "ignore simple and namemismatch, and no main or tests",
			expect:     except("go/parser", "github.com/Masterminds/semver", "net/http", "encoding/binary"),
			isStdLibFn: nil,
			main:       false,
			tests:      false,
			ignore: NewIgnoredRuleset([]string{
				"github.com/example/varied/simple",
				"github.com/example/varied/namemismatch",
			}),
		},
		// ignore two that should knock out gps
		{
			name:       "ignore both importers",
			expect:     except("sort", "github.com/golang/dep/gps", "go/parser"),
			isStdLibFn: nil,
			main:       true,
			tests:      true,
			ignore: NewIgnoredRuleset([]string{
				"github.com/example/varied/simple",
				"github.com/example/varied/m1p",
			}),
		},
		// finally, directly ignore some external packages
		{
			name:       "ignore external",
			expect:     except("sort", "github.com/golang/dep/gps", "go/parser"),
			isStdLibFn: nil,
			main:       true,
			tests:      true,
			ignore: NewIgnoredRuleset([]string{
				"github.com/golang/dep/gps",
				"go/parser",
				"sort",
			}),
		},
	} {
		t.Run(testCase.name, testFlattenReachMap(&vptree, testCase))
	}

	// The only thing varied *doesn't* cover is disallowed path patterns
	ptree, err := ListPackages(filepath.Join(getTestdataRootDir(t), "src", "disallow"), "disallow")
	if err != nil {
		t.Fatalf("ListPackages failed on disallow test case: %s", err)
	}

	t.Run("disallowed", testFlattenReachMap(&ptree, &flattenReachMapCase{
		name:       "disallowed",
		expect:     []string{"github.com/golang/dep/gps", "hash", "sort"},
		isStdLibFn: nil,
		main:       false,
		tests:      false,
	}))
}

type flattenReachMapCase struct {
	expect      []string
	name        string
	ignore      *IgnoredRuleset
	main, tests bool
	isStdLibFn  func(string) bool
}

func testFlattenReachMap(ptree *PackageTree, testCase *flattenReachMapCase) func(*testing.T) {
	return func(t *testing.T) {
		t.Parallel()
		rm, em := ptree.ToReachMap(testCase.main, testCase.tests, true, testCase.ignore)
		if len(em) != 0 {
			t.Errorf("Should not have any error pkgs from ToReachMap, got %s", em)
		}
		result := rm.FlattenFn(testCase.isStdLibFn)
		if !reflect.DeepEqual(testCase.expect, result) {
			t.Errorf("Wrong imports in %q case:\n\t(GOT): %s\n\t(WNT): %s", testCase.name, result, testCase.expect)
		}
	}
}

// Verify that we handle import cycles correctly - drop em all
func TestToReachMapCycle(t *testing.T) {
	ptree, err := ListPackages(filepath.Join(getTestdataRootDir(t), "src", "cycle"), "cycle")
	if err != nil {
		t.Fatalf("ListPackages failed on cycle test case: %s", err)
	}

	rm, em := ptree.ToReachMap(true, true, false, nil)
	if len(em) != 0 {
		t.Errorf("Should not have any error packages from ToReachMap, got %s", em)
	}

	// FIXME TEMPORARILY COMMENTED UNTIL WE CREATE A BETTER LISTPACKAGES MODEL -
	//if len(rm) > 0 {
	//t.Errorf("should be empty reachmap when all packages are in a cycle, got %v", rm)
	//}

	if len(rm) == 0 {
		t.Error("TEMPORARY: should ignore import cycles, but cycle was eliminated")
	}
}

func TestToReachMapFilterDot(t *testing.T) {
	ptree, err := ListPackages(filepath.Join(getTestdataRootDir(t), "src", "relimport"), "relimport")
	if err != nil {
		t.Fatalf("ListPackages failed on relimport test case: %s", err)
	}

	rm, _ := ptree.ToReachMap(true, true, false, nil)
	if _, has := rm["relimport/dot"]; !has {
		t.Fatal("relimport/dot should not have had errors")
	}

	imports := dedupeStrings(rm["relimport/dot"].External, rm["relimport/dot"].Internal)
	for _, imp := range imports {
		if imp == "." {
			t.Fatal("dot import should have been filtered by ToReachMap")
		}
	}
}

func getTestdataRootDir(t *testing.T) string {
	cwd, err := os.Getwd()
	if err != nil {
		t.Fatal(err)
	}
	return filepath.Join(cwd, "..", "_testdata")
}

// Canary regression test to make sure that if PackageTree ever gains new
// fields, we update the Copy method accordingly.
func TestCanaryPackageTreeCopy(t *testing.T) {
	ptreeFields := []string{
		"ImportRoot",
		"Packages",
	}
	packageFields := []string{
		"Name",
		"ImportPath",
		"CommentPath",
		"Imports",
		"TestImports",
	}

	fieldNames := func(typ reflect.Type) []string {
		var names []string
		for i := 0; i < typ.NumField(); i++ {
			names = append(names, typ.Field(i).Name)
		}
		return names
	}

	ptreeRefl := fieldNames(reflect.TypeOf(PackageTree{}))
	packageRefl := fieldNames(reflect.TypeOf(Package{}))

	if !reflect.DeepEqual(ptreeFields, ptreeRefl) {
		t.Errorf("PackageTree.Copy is designed to work with an exact set of fields in the PackageTree struct - make sure it (and this test) have been updated!\n\t(GOT):%s\n\t(WNT):%s", ptreeFields, ptreeRefl)
	}

	if !reflect.DeepEqual(packageFields, packageRefl) {
		t.Errorf("PackageTree.Copy is designed to work with an exact set of fields in the Package struct - make sure it (and this test) have been updated!\n\t(GOT):%s\n\t(WNT):%s", packageFields, packageRefl)
	}
}

func TestPackageTreeCopy(t *testing.T) {
	want := PackageTree{
		ImportRoot: "ren",
		Packages: map[string]PackageOrErr{
			"ren": {
				Err: &build.NoGoError{
					Dir: "some/dir",
				},
			},
			"ren/m1p": {
				P: Package{
					ImportPath:  "ren/m1p",
					CommentPath: "",
					Name:        "m1p",
					Imports: []string{
						"github.com/sdboyer/gps",
						"sort",
					},
				},
			},
		},
	}
	got := want.Copy()
	if !reflect.DeepEqual(want, got) {
		t.Errorf("Did not get expected PackageOrErrs:\n\t(GOT): %+v\n\t(WNT): %+v", got, want)
	}
}
