github.com/sdboyer/gps@v0.16.3/pkgtree/pkgtree_test.go (about)

     1  package pkgtree
     2  
     3  import (
     4  	"fmt"
     5  	"go/build"
     6  	"go/scanner"
     7  	"go/token"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"reflect"
    12  	"runtime"
    13  	"strings"
    14  	"testing"
    15  
    16  	"github.com/sdboyer/gps/internal"
    17  	"github.com/sdboyer/gps/internal/fs"
    18  )
    19  
    20  // Stores a reference to original IsStdLib, so we could restore overridden version.
    21  var doIsStdLib = internal.IsStdLib
    22  
    23  func init() {
    24  	overrideIsStdLib()
    25  }
    26  
    27  // sets the IsStdLib func to always return false, otherwise it would identify
    28  // pretty much all of our fixtures as being stdlib and skip everything.
    29  func overrideIsStdLib() {
    30  	internal.IsStdLib = func(path string) bool {
    31  		return false
    32  	}
    33  }
    34  
    35  // PackageTree.ToReachMap() uses an easily separable algorithm, wmToReach(),
    36  // to turn a discovered set of packages and their imports into a proper pair of
    37  // internal and external reach maps.
    38  //
    39  // That algorithm is purely symbolic (no filesystem interaction), and thus is
    40  // easy to test. This is that test.
    41  func TestWorkmapToReach(t *testing.T) {
    42  	empty := func() map[string]bool {
    43  		return make(map[string]bool)
    44  	}
    45  
    46  	e := struct {
    47  		Internal, External []string
    48  	}{}
    49  	table := map[string]struct {
    50  		workmap  map[string]wm
    51  		rm       ReachMap
    52  		em       map[string]*ProblemImportError
    53  		backprop bool
    54  	}{
    55  		"single": {
    56  			workmap: map[string]wm{
    57  				"foo": {
    58  					ex: empty(),
    59  					in: empty(),
    60  				},
    61  			},
    62  			rm: ReachMap{
    63  				"foo": e,
    64  			},
    65  		},
    66  		"no external": {
    67  			workmap: map[string]wm{
    68  				"foo": {
    69  					ex: empty(),
    70  					in: empty(),
    71  				},
    72  				"foo/bar": {
    73  					ex: empty(),
    74  					in: empty(),
    75  				},
    76  			},
    77  			rm: ReachMap{
    78  				"foo":     e,
    79  				"foo/bar": e,
    80  			},
    81  		},
    82  		"no external with subpkg": {
    83  			workmap: map[string]wm{
    84  				"foo": {
    85  					ex: empty(),
    86  					in: map[string]bool{
    87  						"foo/bar": true,
    88  					},
    89  				},
    90  				"foo/bar": {
    91  					ex: empty(),
    92  					in: empty(),
    93  				},
    94  			},
    95  			rm: ReachMap{
    96  				"foo": {
    97  					Internal: []string{"foo/bar"},
    98  				},
    99  				"foo/bar": e,
   100  			},
   101  		},
   102  		"simple base transitive": {
   103  			workmap: map[string]wm{
   104  				"foo": {
   105  					ex: empty(),
   106  					in: map[string]bool{
   107  						"foo/bar": true,
   108  					},
   109  				},
   110  				"foo/bar": {
   111  					ex: map[string]bool{
   112  						"baz": true,
   113  					},
   114  					in: empty(),
   115  				},
   116  			},
   117  			rm: ReachMap{
   118  				"foo": {
   119  					External: []string{"baz"},
   120  					Internal: []string{"foo/bar"},
   121  				},
   122  				"foo/bar": {
   123  					External: []string{"baz"},
   124  				},
   125  			},
   126  		},
   127  		"missing package is poison": {
   128  			workmap: map[string]wm{
   129  				"A": {
   130  					ex: map[string]bool{
   131  						"B/foo": true,
   132  					},
   133  					in: map[string]bool{
   134  						"A/foo": true, // missing
   135  						"A/bar": true,
   136  					},
   137  				},
   138  				"A/bar": {
   139  					ex: map[string]bool{
   140  						"B/baz": true,
   141  					},
   142  					in: empty(),
   143  				},
   144  			},
   145  			rm: ReachMap{
   146  				"A/bar": {
   147  					External: []string{"B/baz"},
   148  				},
   149  			},
   150  			em: map[string]*ProblemImportError{
   151  				"A": &ProblemImportError{
   152  					ImportPath: "A",
   153  					Cause:      []string{"A/foo"},
   154  					Err:        missingPkgErr("A/foo"),
   155  				},
   156  			},
   157  			backprop: true,
   158  		},
   159  		"transitive missing package is poison": {
   160  			workmap: map[string]wm{
   161  				"A": {
   162  					ex: map[string]bool{
   163  						"B/foo": true,
   164  					},
   165  					in: map[string]bool{
   166  						"A/foo":  true, // transitively missing
   167  						"A/quux": true,
   168  					},
   169  				},
   170  				"A/foo": {
   171  					ex: map[string]bool{
   172  						"C/flugle": true,
   173  					},
   174  					in: map[string]bool{
   175  						"A/bar": true, // missing
   176  					},
   177  				},
   178  				"A/quux": {
   179  					ex: map[string]bool{
   180  						"B/baz": true,
   181  					},
   182  					in: empty(),
   183  				},
   184  			},
   185  			rm: ReachMap{
   186  				"A/quux": {
   187  					External: []string{"B/baz"},
   188  				},
   189  			},
   190  			em: map[string]*ProblemImportError{
   191  				"A": &ProblemImportError{
   192  					ImportPath: "A",
   193  					Cause:      []string{"A/foo", "A/bar"},
   194  					Err:        missingPkgErr("A/bar"),
   195  				},
   196  				"A/foo": &ProblemImportError{
   197  					ImportPath: "A/foo",
   198  					Cause:      []string{"A/bar"},
   199  					Err:        missingPkgErr("A/bar"),
   200  				},
   201  			},
   202  			backprop: true,
   203  		},
   204  		"err'd package is poison": {
   205  			workmap: map[string]wm{
   206  				"A": {
   207  					ex: map[string]bool{
   208  						"B/foo": true,
   209  					},
   210  					in: map[string]bool{
   211  						"A/foo": true, // err'd
   212  						"A/bar": true,
   213  					},
   214  				},
   215  				"A/foo": {
   216  					err: fmt.Errorf("err pkg"),
   217  				},
   218  				"A/bar": {
   219  					ex: map[string]bool{
   220  						"B/baz": true,
   221  					},
   222  					in: empty(),
   223  				},
   224  			},
   225  			rm: ReachMap{
   226  				"A/bar": {
   227  					External: []string{"B/baz"},
   228  				},
   229  			},
   230  			em: map[string]*ProblemImportError{
   231  				"A": &ProblemImportError{
   232  					ImportPath: "A",
   233  					Cause:      []string{"A/foo"},
   234  					Err:        fmt.Errorf("err pkg"),
   235  				},
   236  				"A/foo": &ProblemImportError{
   237  					ImportPath: "A/foo",
   238  					Err:        fmt.Errorf("err pkg"),
   239  				},
   240  			},
   241  			backprop: true,
   242  		},
   243  		"transitive err'd package is poison": {
   244  			workmap: map[string]wm{
   245  				"A": {
   246  					ex: map[string]bool{
   247  						"B/foo": true,
   248  					},
   249  					in: map[string]bool{
   250  						"A/foo":  true, // transitively err'd
   251  						"A/quux": true,
   252  					},
   253  				},
   254  				"A/foo": {
   255  					ex: map[string]bool{
   256  						"C/flugle": true,
   257  					},
   258  					in: map[string]bool{
   259  						"A/bar": true, // err'd
   260  					},
   261  				},
   262  				"A/bar": {
   263  					err: fmt.Errorf("err pkg"),
   264  				},
   265  				"A/quux": {
   266  					ex: map[string]bool{
   267  						"B/baz": true,
   268  					},
   269  					in: empty(),
   270  				},
   271  			},
   272  			rm: ReachMap{
   273  				"A/quux": {
   274  					External: []string{"B/baz"},
   275  				},
   276  			},
   277  			em: map[string]*ProblemImportError{
   278  				"A": &ProblemImportError{
   279  					ImportPath: "A",
   280  					Cause:      []string{"A/foo", "A/bar"},
   281  					Err:        fmt.Errorf("err pkg"),
   282  				},
   283  				"A/foo": &ProblemImportError{
   284  					ImportPath: "A/foo",
   285  					Cause:      []string{"A/bar"},
   286  					Err:        fmt.Errorf("err pkg"),
   287  				},
   288  				"A/bar": &ProblemImportError{
   289  					ImportPath: "A/bar",
   290  					Err:        fmt.Errorf("err pkg"),
   291  				},
   292  			},
   293  			backprop: true,
   294  		},
   295  		"transitive err'd package no backprop": {
   296  			workmap: map[string]wm{
   297  				"A": {
   298  					ex: map[string]bool{
   299  						"B/foo": true,
   300  					},
   301  					in: map[string]bool{
   302  						"A/foo":  true, // transitively err'd
   303  						"A/quux": true,
   304  					},
   305  				},
   306  				"A/foo": {
   307  					ex: map[string]bool{
   308  						"C/flugle": true,
   309  					},
   310  					in: map[string]bool{
   311  						"A/bar": true, // err'd
   312  					},
   313  				},
   314  				"A/bar": {
   315  					err: fmt.Errorf("err pkg"),
   316  				},
   317  				"A/quux": {
   318  					ex: map[string]bool{
   319  						"B/baz": true,
   320  					},
   321  					in: empty(),
   322  				},
   323  			},
   324  			rm: ReachMap{
   325  				"A": {
   326  					Internal: []string{"A/bar", "A/foo", "A/quux"},
   327  					//Internal: []string{"A/foo", "A/quux"},
   328  					External: []string{"B/baz", "B/foo", "C/flugle"},
   329  				},
   330  				"A/foo": {
   331  					Internal: []string{"A/bar"},
   332  					External: []string{"C/flugle"},
   333  				},
   334  				"A/quux": {
   335  					External: []string{"B/baz"},
   336  				},
   337  			},
   338  			em: map[string]*ProblemImportError{
   339  				"A/bar": &ProblemImportError{
   340  					ImportPath: "A/bar",
   341  					Err:        fmt.Errorf("err pkg"),
   342  				},
   343  			},
   344  		},
   345  		// The following tests are mostly about regressions and weeding out
   346  		// weird assumptions
   347  		"internal diamond": {
   348  			workmap: map[string]wm{
   349  				"A": {
   350  					ex: map[string]bool{
   351  						"B/foo": true,
   352  					},
   353  					in: map[string]bool{
   354  						"A/foo": true,
   355  						"A/bar": true,
   356  					},
   357  				},
   358  				"A/foo": {
   359  					ex: map[string]bool{
   360  						"C": true,
   361  					},
   362  					in: map[string]bool{
   363  						"A/quux": true,
   364  					},
   365  				},
   366  				"A/bar": {
   367  					ex: map[string]bool{
   368  						"D": true,
   369  					},
   370  					in: map[string]bool{
   371  						"A/quux": true,
   372  					},
   373  				},
   374  				"A/quux": {
   375  					ex: map[string]bool{
   376  						"B/baz": true,
   377  					},
   378  					in: empty(),
   379  				},
   380  			},
   381  			rm: ReachMap{
   382  				"A": {
   383  					External: []string{
   384  						"B/baz",
   385  						"B/foo",
   386  						"C",
   387  						"D",
   388  					},
   389  					Internal: []string{
   390  						"A/bar",
   391  						"A/foo",
   392  						"A/quux",
   393  					},
   394  				},
   395  				"A/foo": {
   396  					External: []string{
   397  						"B/baz",
   398  						"C",
   399  					},
   400  					Internal: []string{
   401  						"A/quux",
   402  					},
   403  				},
   404  				"A/bar": {
   405  					External: []string{
   406  						"B/baz",
   407  						"D",
   408  					},
   409  					Internal: []string{
   410  						"A/quux",
   411  					},
   412  				},
   413  				"A/quux": {
   414  					External: []string{"B/baz"},
   415  				},
   416  			},
   417  		},
   418  		"rootmost gets imported": {
   419  			workmap: map[string]wm{
   420  				"A": {
   421  					ex: map[string]bool{
   422  						"B": true,
   423  					},
   424  					in: empty(),
   425  				},
   426  				"A/foo": {
   427  					ex: map[string]bool{
   428  						"C": true,
   429  					},
   430  					in: map[string]bool{
   431  						"A": true,
   432  					},
   433  				},
   434  			},
   435  			rm: ReachMap{
   436  				"A": {
   437  					External: []string{"B"},
   438  				},
   439  				"A/foo": {
   440  					External: []string{
   441  						"B",
   442  						"C",
   443  					},
   444  					Internal: []string{
   445  						"A",
   446  					},
   447  				},
   448  			},
   449  		},
   450  	}
   451  
   452  	for name, fix := range table {
   453  		name, fix := name, fix
   454  		t.Run(name, func(t *testing.T) {
   455  			t.Parallel()
   456  
   457  			// Avoid erroneous errors by initializing the fixture's error map if
   458  			// needed
   459  			if fix.em == nil {
   460  				fix.em = make(map[string]*ProblemImportError)
   461  			}
   462  
   463  			rm, em := wmToReach(fix.workmap, fix.backprop)
   464  			if !reflect.DeepEqual(rm, fix.rm) {
   465  				//t.Error(pretty.Sprintf("wmToReach(%q): Did not get expected reach map:\n\t(GOT): %s\n\t(WNT): %s", name, rm, fix.rm))
   466  				t.Errorf("Did not get expected reach map:\n\t(GOT): %s\n\t(WNT): %s", rm, fix.rm)
   467  			}
   468  			if !reflect.DeepEqual(em, fix.em) {
   469  				//t.Error(pretty.Sprintf("wmToReach(%q): Did not get expected error map:\n\t(GOT): %# v\n\t(WNT): %# v", name, em, fix.em))
   470  				t.Errorf("Did not get expected error map:\n\t(GOT): %v\n\t(WNT): %v", em, fix.em)
   471  			}
   472  		})
   473  	}
   474  }
   475  
   476  func TestListPackagesNoDir(t *testing.T) {
   477  	out, err := ListPackages(filepath.Join(getTestdataRootDir(t), "notexist"), "notexist")
   478  	if err == nil {
   479  		t.Error("ListPackages should have errored on pointing to a nonexistent dir")
   480  	}
   481  	if !reflect.DeepEqual(PackageTree{}, out) {
   482  		t.Error("should've gotten back an empty PackageTree")
   483  	}
   484  }
   485  
   486  func TestListPackages(t *testing.T) {
   487  	srcdir := filepath.Join(getTestdataRootDir(t), "src")
   488  	j := func(s ...string) string {
   489  		return filepath.Join(srcdir, filepath.Join(s...))
   490  	}
   491  
   492  	table := map[string]struct {
   493  		fileRoot   string
   494  		importRoot string
   495  		out        PackageTree
   496  		err        error
   497  	}{
   498  		"empty": {
   499  			fileRoot:   j("empty"),
   500  			importRoot: "empty",
   501  			out: PackageTree{
   502  				ImportRoot: "empty",
   503  				Packages: map[string]PackageOrErr{
   504  					"empty": {
   505  						Err: &build.NoGoError{
   506  							Dir: j("empty"),
   507  						},
   508  					},
   509  				},
   510  			},
   511  		},
   512  		"code only": {
   513  			fileRoot:   j("simple"),
   514  			importRoot: "simple",
   515  			out: PackageTree{
   516  				ImportRoot: "simple",
   517  				Packages: map[string]PackageOrErr{
   518  					"simple": {
   519  						P: Package{
   520  							ImportPath:  "simple",
   521  							CommentPath: "",
   522  							Name:        "simple",
   523  							Imports: []string{
   524  								"github.com/sdboyer/gps",
   525  								"sort",
   526  							},
   527  						},
   528  					},
   529  				},
   530  			},
   531  		},
   532  		"impose import path": {
   533  			fileRoot:   j("simple"),
   534  			importRoot: "arbitrary",
   535  			out: PackageTree{
   536  				ImportRoot: "arbitrary",
   537  				Packages: map[string]PackageOrErr{
   538  					"arbitrary": {
   539  						P: Package{
   540  							ImportPath:  "arbitrary",
   541  							CommentPath: "",
   542  							Name:        "simple",
   543  							Imports: []string{
   544  								"github.com/sdboyer/gps",
   545  								"sort",
   546  							},
   547  						},
   548  					},
   549  				},
   550  			},
   551  		},
   552  		"test only": {
   553  			fileRoot:   j("t"),
   554  			importRoot: "simple",
   555  			out: PackageTree{
   556  				ImportRoot: "simple",
   557  				Packages: map[string]PackageOrErr{
   558  					"simple": {
   559  						P: Package{
   560  							ImportPath:  "simple",
   561  							CommentPath: "",
   562  							Name:        "simple",
   563  							Imports:     []string{},
   564  							TestImports: []string{
   565  								"math/rand",
   566  								"strconv",
   567  							},
   568  						},
   569  					},
   570  				},
   571  			},
   572  		},
   573  		"xtest only": {
   574  			fileRoot:   j("xt"),
   575  			importRoot: "simple",
   576  			out: PackageTree{
   577  				ImportRoot: "simple",
   578  				Packages: map[string]PackageOrErr{
   579  					"simple": {
   580  						P: Package{
   581  							ImportPath:  "simple",
   582  							CommentPath: "",
   583  							Name:        "simple",
   584  							Imports:     []string{},
   585  							TestImports: []string{
   586  								"sort",
   587  								"strconv",
   588  							},
   589  						},
   590  					},
   591  				},
   592  			},
   593  		},
   594  		"code and test": {
   595  			fileRoot:   j("simplet"),
   596  			importRoot: "simple",
   597  			out: PackageTree{
   598  				ImportRoot: "simple",
   599  				Packages: map[string]PackageOrErr{
   600  					"simple": {
   601  						P: Package{
   602  							ImportPath:  "simple",
   603  							CommentPath: "",
   604  							Name:        "simple",
   605  							Imports: []string{
   606  								"github.com/sdboyer/gps",
   607  								"sort",
   608  							},
   609  							TestImports: []string{
   610  								"math/rand",
   611  								"strconv",
   612  							},
   613  						},
   614  					},
   615  				},
   616  			},
   617  		},
   618  		"code and xtest": {
   619  			fileRoot:   j("simplext"),
   620  			importRoot: "simple",
   621  			out: PackageTree{
   622  				ImportRoot: "simple",
   623  				Packages: map[string]PackageOrErr{
   624  					"simple": {
   625  						P: Package{
   626  							ImportPath:  "simple",
   627  							CommentPath: "",
   628  							Name:        "simple",
   629  							Imports: []string{
   630  								"github.com/sdboyer/gps",
   631  								"sort",
   632  							},
   633  							TestImports: []string{
   634  								"sort",
   635  								"strconv",
   636  							},
   637  						},
   638  					},
   639  				},
   640  			},
   641  		},
   642  		"code, test, xtest": {
   643  			fileRoot:   j("simpleallt"),
   644  			importRoot: "simple",
   645  			out: PackageTree{
   646  				ImportRoot: "simple",
   647  				Packages: map[string]PackageOrErr{
   648  					"simple": {
   649  						P: Package{
   650  							ImportPath:  "simple",
   651  							CommentPath: "",
   652  							Name:        "simple",
   653  							Imports: []string{
   654  								"github.com/sdboyer/gps",
   655  								"sort",
   656  							},
   657  							TestImports: []string{
   658  								"math/rand",
   659  								"sort",
   660  								"strconv",
   661  							},
   662  						},
   663  					},
   664  				},
   665  			},
   666  		},
   667  		"one pkg multifile": {
   668  			fileRoot:   j("m1p"),
   669  			importRoot: "m1p",
   670  			out: PackageTree{
   671  				ImportRoot: "m1p",
   672  				Packages: map[string]PackageOrErr{
   673  					"m1p": {
   674  						P: Package{
   675  							ImportPath:  "m1p",
   676  							CommentPath: "",
   677  							Name:        "m1p",
   678  							Imports: []string{
   679  								"github.com/sdboyer/gps",
   680  								"os",
   681  								"sort",
   682  							},
   683  						},
   684  					},
   685  				},
   686  			},
   687  		},
   688  		"one nested below": {
   689  			fileRoot:   j("nest"),
   690  			importRoot: "nest",
   691  			out: PackageTree{
   692  				ImportRoot: "nest",
   693  				Packages: map[string]PackageOrErr{
   694  					"nest": {
   695  						P: Package{
   696  							ImportPath:  "nest",
   697  							CommentPath: "",
   698  							Name:        "simple",
   699  							Imports: []string{
   700  								"github.com/sdboyer/gps",
   701  								"sort",
   702  							},
   703  						},
   704  					},
   705  					"nest/m1p": {
   706  						P: Package{
   707  							ImportPath:  "nest/m1p",
   708  							CommentPath: "",
   709  							Name:        "m1p",
   710  							Imports: []string{
   711  								"github.com/sdboyer/gps",
   712  								"os",
   713  								"sort",
   714  							},
   715  						},
   716  					},
   717  				},
   718  			},
   719  		},
   720  		"malformed go file": {
   721  			fileRoot:   j("bad"),
   722  			importRoot: "bad",
   723  			out: PackageTree{
   724  				ImportRoot: "bad",
   725  				Packages: map[string]PackageOrErr{
   726  					"bad": {
   727  						Err: scanner.ErrorList{
   728  							&scanner.Error{
   729  								Pos: token.Position{
   730  									Filename: j("bad", "bad.go"),
   731  									Offset:   113,
   732  									Line:     2,
   733  									Column:   43,
   734  								},
   735  								Msg: "expected 'package', found 'EOF'",
   736  							},
   737  						},
   738  					},
   739  				},
   740  			},
   741  		},
   742  		"two nested under empty root": {
   743  			fileRoot:   j("ren"),
   744  			importRoot: "ren",
   745  			out: PackageTree{
   746  				ImportRoot: "ren",
   747  				Packages: map[string]PackageOrErr{
   748  					"ren": {
   749  						Err: &build.NoGoError{
   750  							Dir: j("ren"),
   751  						},
   752  					},
   753  					"ren/m1p": {
   754  						P: Package{
   755  							ImportPath:  "ren/m1p",
   756  							CommentPath: "",
   757  							Name:        "m1p",
   758  							Imports: []string{
   759  								"github.com/sdboyer/gps",
   760  								"os",
   761  								"sort",
   762  							},
   763  						},
   764  					},
   765  					"ren/simple": {
   766  						P: Package{
   767  							ImportPath:  "ren/simple",
   768  							CommentPath: "",
   769  							Name:        "simple",
   770  							Imports: []string{
   771  								"github.com/sdboyer/gps",
   772  								"sort",
   773  							},
   774  						},
   775  					},
   776  				},
   777  			},
   778  		},
   779  		"internal name mismatch": {
   780  			fileRoot:   j("doublenest"),
   781  			importRoot: "doublenest",
   782  			out: PackageTree{
   783  				ImportRoot: "doublenest",
   784  				Packages: map[string]PackageOrErr{
   785  					"doublenest": {
   786  						P: Package{
   787  							ImportPath:  "doublenest",
   788  							CommentPath: "",
   789  							Name:        "base",
   790  							Imports: []string{
   791  								"github.com/sdboyer/gps",
   792  								"go/parser",
   793  							},
   794  						},
   795  					},
   796  					"doublenest/namemismatch": {
   797  						P: Package{
   798  							ImportPath:  "doublenest/namemismatch",
   799  							CommentPath: "",
   800  							Name:        "nm",
   801  							Imports: []string{
   802  								"github.com/Masterminds/semver",
   803  								"os",
   804  							},
   805  						},
   806  					},
   807  					"doublenest/namemismatch/m1p": {
   808  						P: Package{
   809  							ImportPath:  "doublenest/namemismatch/m1p",
   810  							CommentPath: "",
   811  							Name:        "m1p",
   812  							Imports: []string{
   813  								"github.com/sdboyer/gps",
   814  								"os",
   815  								"sort",
   816  							},
   817  						},
   818  					},
   819  				},
   820  			},
   821  		},
   822  		"file and importroot mismatch": {
   823  			fileRoot:   j("doublenest"),
   824  			importRoot: "other",
   825  			out: PackageTree{
   826  				ImportRoot: "other",
   827  				Packages: map[string]PackageOrErr{
   828  					"other": {
   829  						P: Package{
   830  							ImportPath:  "other",
   831  							CommentPath: "",
   832  							Name:        "base",
   833  							Imports: []string{
   834  								"github.com/sdboyer/gps",
   835  								"go/parser",
   836  							},
   837  						},
   838  					},
   839  					"other/namemismatch": {
   840  						P: Package{
   841  							ImportPath:  "other/namemismatch",
   842  							CommentPath: "",
   843  							Name:        "nm",
   844  							Imports: []string{
   845  								"github.com/Masterminds/semver",
   846  								"os",
   847  							},
   848  						},
   849  					},
   850  					"other/namemismatch/m1p": {
   851  						P: Package{
   852  							ImportPath:  "other/namemismatch/m1p",
   853  							CommentPath: "",
   854  							Name:        "m1p",
   855  							Imports: []string{
   856  								"github.com/sdboyer/gps",
   857  								"os",
   858  								"sort",
   859  							},
   860  						},
   861  					},
   862  				},
   863  			},
   864  		},
   865  		"code and ignored main": {
   866  			fileRoot:   j("igmain"),
   867  			importRoot: "simple",
   868  			out: PackageTree{
   869  				ImportRoot: "simple",
   870  				Packages: map[string]PackageOrErr{
   871  					"simple": {
   872  						P: Package{
   873  							ImportPath:  "simple",
   874  							CommentPath: "",
   875  							Name:        "simple",
   876  							Imports: []string{
   877  								"github.com/sdboyer/gps",
   878  								"sort",
   879  								"unicode",
   880  							},
   881  						},
   882  					},
   883  				},
   884  			},
   885  		},
   886  		"code and ignored main, order check": {
   887  			fileRoot:   j("igmainfirst"),
   888  			importRoot: "simple",
   889  			out: PackageTree{
   890  				ImportRoot: "simple",
   891  				Packages: map[string]PackageOrErr{
   892  					"simple": {
   893  						P: Package{
   894  							ImportPath:  "simple",
   895  							CommentPath: "",
   896  							Name:        "simple",
   897  							Imports: []string{
   898  								"github.com/sdboyer/gps",
   899  								"sort",
   900  								"unicode",
   901  							},
   902  						},
   903  					},
   904  				},
   905  			},
   906  		},
   907  		"code and ignored main with comment leader": {
   908  			fileRoot:   j("igmainlong"),
   909  			importRoot: "simple",
   910  			out: PackageTree{
   911  				ImportRoot: "simple",
   912  				Packages: map[string]PackageOrErr{
   913  					"simple": {
   914  						P: Package{
   915  							ImportPath:  "simple",
   916  							CommentPath: "",
   917  							Name:        "simple",
   918  							Imports: []string{
   919  								"github.com/sdboyer/gps",
   920  								"sort",
   921  								"unicode",
   922  							},
   923  						},
   924  					},
   925  				},
   926  			},
   927  		},
   928  		"code, tests, and ignored main": {
   929  			fileRoot:   j("igmaint"),
   930  			importRoot: "simple",
   931  			out: PackageTree{
   932  				ImportRoot: "simple",
   933  				Packages: map[string]PackageOrErr{
   934  					"simple": {
   935  						P: Package{
   936  							ImportPath:  "simple",
   937  							CommentPath: "",
   938  							Name:        "simple",
   939  							Imports: []string{
   940  								"github.com/sdboyer/gps",
   941  								"sort",
   942  								"unicode",
   943  							},
   944  							TestImports: []string{
   945  								"math/rand",
   946  								"strconv",
   947  							},
   948  						},
   949  					},
   950  				},
   951  			},
   952  		},
   953  		// New code allows this because it doesn't care if the code compiles (kinda) or not,
   954  		// so maybe this is actually not an error anymore?
   955  		//
   956  		// TODO re-enable this case after the full and proper ListPackages()
   957  		// refactor in #99
   958  		/*"two pkgs": {
   959  			fileRoot:   j("twopkgs"),
   960  			importRoot: "twopkgs",
   961  			out: PackageTree{
   962  				ImportRoot: "twopkgs",
   963  				Packages: map[string]PackageOrErr{
   964  					"twopkgs": {
   965  						Err: &build.MultiplePackageError{
   966  							Dir:      j("twopkgs"),
   967  							Packages: []string{"simple", "m1p"},
   968  							Files:    []string{"a.go", "b.go"},
   969  						},
   970  					},
   971  				},
   972  			},
   973  		}, */
   974  		// imports a missing pkg
   975  		"missing import": {
   976  			fileRoot:   j("missing"),
   977  			importRoot: "missing",
   978  			out: PackageTree{
   979  				ImportRoot: "missing",
   980  				Packages: map[string]PackageOrErr{
   981  					"missing": {
   982  						P: Package{
   983  							ImportPath:  "missing",
   984  							CommentPath: "",
   985  							Name:        "simple",
   986  							Imports: []string{
   987  								"github.com/sdboyer/gps",
   988  								"missing/missing",
   989  								"sort",
   990  							},
   991  						},
   992  					},
   993  					"missing/m1p": {
   994  						P: Package{
   995  							ImportPath:  "missing/m1p",
   996  							CommentPath: "",
   997  							Name:        "m1p",
   998  							Imports: []string{
   999  								"github.com/sdboyer/gps",
  1000  								"os",
  1001  								"sort",
  1002  							},
  1003  						},
  1004  					},
  1005  				},
  1006  			},
  1007  		},
  1008  		// import cycle of three packages. ListPackages doesn't do anything
  1009  		// special with cycles - that's the reach calculator's job - so this is
  1010  		// error-free
  1011  		"import cycle, len 3": {
  1012  			fileRoot:   j("cycle"),
  1013  			importRoot: "cycle",
  1014  			out: PackageTree{
  1015  				ImportRoot: "cycle",
  1016  				Packages: map[string]PackageOrErr{
  1017  					"cycle": {
  1018  						P: Package{
  1019  							ImportPath:  "cycle",
  1020  							CommentPath: "",
  1021  							Name:        "cycle",
  1022  							Imports: []string{
  1023  								"cycle/one",
  1024  								"github.com/sdboyer/gps",
  1025  							},
  1026  						},
  1027  					},
  1028  					"cycle/one": {
  1029  						P: Package{
  1030  							ImportPath:  "cycle/one",
  1031  							CommentPath: "",
  1032  							Name:        "one",
  1033  							Imports: []string{
  1034  								"cycle/two",
  1035  								"github.com/sdboyer/gps",
  1036  							},
  1037  						},
  1038  					},
  1039  					"cycle/two": {
  1040  						P: Package{
  1041  							ImportPath:  "cycle/two",
  1042  							CommentPath: "",
  1043  							Name:        "two",
  1044  							Imports: []string{
  1045  								"cycle",
  1046  								"github.com/sdboyer/gps",
  1047  							},
  1048  						},
  1049  					},
  1050  				},
  1051  			},
  1052  		},
  1053  		// has disallowed dir names
  1054  		"disallowed dirs": {
  1055  			fileRoot:   j("disallow"),
  1056  			importRoot: "disallow",
  1057  			out: PackageTree{
  1058  				ImportRoot: "disallow",
  1059  				Packages: map[string]PackageOrErr{
  1060  					"disallow": {
  1061  						P: Package{
  1062  							ImportPath:  "disallow",
  1063  							CommentPath: "",
  1064  							Name:        "disallow",
  1065  							Imports: []string{
  1066  								"disallow/testdata",
  1067  								"github.com/sdboyer/gps",
  1068  								"sort",
  1069  							},
  1070  						},
  1071  					},
  1072  					// disallow/.m1p is ignored by listPackages...for now. Kept
  1073  					// here commented because this might change again...
  1074  					//"disallow/.m1p": {
  1075  					//P: Package{
  1076  					//ImportPath:  "disallow/.m1p",
  1077  					//CommentPath: "",
  1078  					//Name:        "m1p",
  1079  					//Imports: []string{
  1080  					//"github.com/sdboyer/gps",
  1081  					//"os",
  1082  					//"sort",
  1083  					//},
  1084  					//},
  1085  					//},
  1086  					"disallow/testdata": {
  1087  						P: Package{
  1088  							ImportPath:  "disallow/testdata",
  1089  							CommentPath: "",
  1090  							Name:        "testdata",
  1091  							Imports: []string{
  1092  								"hash",
  1093  							},
  1094  						},
  1095  					},
  1096  				},
  1097  			},
  1098  		},
  1099  		"relative imports": {
  1100  			fileRoot:   j("relimport"),
  1101  			importRoot: "relimport",
  1102  			out: PackageTree{
  1103  				ImportRoot: "relimport",
  1104  				Packages: map[string]PackageOrErr{
  1105  					"relimport": {
  1106  						P: Package{
  1107  							ImportPath:  "relimport",
  1108  							CommentPath: "",
  1109  							Name:        "relimport",
  1110  							Imports: []string{
  1111  								"sort",
  1112  							},
  1113  						},
  1114  					},
  1115  					"relimport/dot": {
  1116  						P: Package{
  1117  							ImportPath:  "relimport/dot",
  1118  							CommentPath: "",
  1119  							Name:        "dot",
  1120  							Imports: []string{
  1121  								".",
  1122  								"sort",
  1123  							},
  1124  						},
  1125  					},
  1126  					"relimport/dotdot": {
  1127  						Err: &LocalImportsError{
  1128  							Dir:        j("relimport/dotdot"),
  1129  							ImportPath: "relimport/dotdot",
  1130  							LocalImports: []string{
  1131  								"..",
  1132  							},
  1133  						},
  1134  					},
  1135  					"relimport/dotslash": {
  1136  						Err: &LocalImportsError{
  1137  							Dir:        j("relimport/dotslash"),
  1138  							ImportPath: "relimport/dotslash",
  1139  							LocalImports: []string{
  1140  								"./simple",
  1141  							},
  1142  						},
  1143  					},
  1144  					"relimport/dotdotslash": {
  1145  						Err: &LocalImportsError{
  1146  							Dir:        j("relimport/dotdotslash"),
  1147  							ImportPath: "relimport/dotdotslash",
  1148  							LocalImports: []string{
  1149  								"../github.com/sdboyer/gps",
  1150  							},
  1151  						},
  1152  					},
  1153  				},
  1154  			},
  1155  		},
  1156  		"skip underscore": {
  1157  			fileRoot:   j("skip_"),
  1158  			importRoot: "skip_",
  1159  			out: PackageTree{
  1160  				ImportRoot: "skip_",
  1161  				Packages: map[string]PackageOrErr{
  1162  					"skip_": {
  1163  						P: Package{
  1164  							ImportPath:  "skip_",
  1165  							CommentPath: "",
  1166  							Name:        "skip",
  1167  							Imports: []string{
  1168  								"github.com/sdboyer/gps",
  1169  								"sort",
  1170  							},
  1171  						},
  1172  					},
  1173  				},
  1174  			},
  1175  		},
  1176  		// This case mostly exists for the PackageTree methods, but it does
  1177  		// cover a bit of range
  1178  		"varied": {
  1179  			fileRoot:   j("varied"),
  1180  			importRoot: "varied",
  1181  			out: PackageTree{
  1182  				ImportRoot: "varied",
  1183  				Packages: map[string]PackageOrErr{
  1184  					"varied": {
  1185  						P: Package{
  1186  							ImportPath:  "varied",
  1187  							CommentPath: "",
  1188  							Name:        "main",
  1189  							Imports: []string{
  1190  								"net/http",
  1191  								"varied/namemismatch",
  1192  								"varied/otherpath",
  1193  								"varied/simple",
  1194  							},
  1195  						},
  1196  					},
  1197  					"varied/otherpath": {
  1198  						P: Package{
  1199  							ImportPath:  "varied/otherpath",
  1200  							CommentPath: "",
  1201  							Name:        "otherpath",
  1202  							Imports:     []string{},
  1203  							TestImports: []string{
  1204  								"varied/m1p",
  1205  							},
  1206  						},
  1207  					},
  1208  					"varied/simple": {
  1209  						P: Package{
  1210  							ImportPath:  "varied/simple",
  1211  							CommentPath: "",
  1212  							Name:        "simple",
  1213  							Imports: []string{
  1214  								"github.com/sdboyer/gps",
  1215  								"go/parser",
  1216  								"varied/simple/another",
  1217  							},
  1218  						},
  1219  					},
  1220  					"varied/simple/another": {
  1221  						P: Package{
  1222  							ImportPath:  "varied/simple/another",
  1223  							CommentPath: "",
  1224  							Name:        "another",
  1225  							Imports: []string{
  1226  								"hash",
  1227  								"varied/m1p",
  1228  							},
  1229  							TestImports: []string{
  1230  								"encoding/binary",
  1231  							},
  1232  						},
  1233  					},
  1234  					"varied/namemismatch": {
  1235  						P: Package{
  1236  							ImportPath:  "varied/namemismatch",
  1237  							CommentPath: "",
  1238  							Name:        "nm",
  1239  							Imports: []string{
  1240  								"github.com/Masterminds/semver",
  1241  								"os",
  1242  							},
  1243  						},
  1244  					},
  1245  					"varied/m1p": {
  1246  						P: Package{
  1247  							ImportPath:  "varied/m1p",
  1248  							CommentPath: "",
  1249  							Name:        "m1p",
  1250  							Imports: []string{
  1251  								"github.com/sdboyer/gps",
  1252  								"os",
  1253  								"sort",
  1254  							},
  1255  						},
  1256  					},
  1257  				},
  1258  			},
  1259  		},
  1260  		"invalid buildtag like comments should be ignored": {
  1261  			fileRoot:   j("buildtag"),
  1262  			importRoot: "buildtag",
  1263  			out: PackageTree{
  1264  				ImportRoot: "buildtag",
  1265  				Packages: map[string]PackageOrErr{
  1266  					"buildtag": {
  1267  						P: Package{
  1268  							ImportPath:  "buildtag",
  1269  							CommentPath: "",
  1270  							Name:        "buildtag",
  1271  							Imports: []string{
  1272  								"sort",
  1273  							},
  1274  						},
  1275  					},
  1276  				},
  1277  			},
  1278  		},
  1279  	}
  1280  
  1281  	for name, fix := range table {
  1282  		t.Run(name, func(t *testing.T) {
  1283  			if _, err := os.Stat(fix.fileRoot); err != nil {
  1284  				t.Errorf("error on fileRoot %s: %s", fix.fileRoot, err)
  1285  			}
  1286  
  1287  			out, err := ListPackages(fix.fileRoot, fix.importRoot)
  1288  
  1289  			if err != nil && fix.err == nil {
  1290  				t.Errorf("Received error but none expected: %s", err)
  1291  			} else if fix.err != nil && err == nil {
  1292  				t.Errorf("Error expected but none received")
  1293  			} else if fix.err != nil && err != nil {
  1294  				if !reflect.DeepEqual(fix.err, err) {
  1295  					t.Errorf("Did not receive expected error:\n\t(GOT): %s\n\t(WNT): %s", err, fix.err)
  1296  				}
  1297  			}
  1298  
  1299  			if fix.out.ImportRoot != "" && fix.out.Packages != nil {
  1300  				if !reflect.DeepEqual(out, fix.out) {
  1301  					if fix.out.ImportRoot != out.ImportRoot {
  1302  						t.Errorf("Expected ImportRoot %s, got %s", fix.out.ImportRoot, out.ImportRoot)
  1303  					}
  1304  
  1305  					// overwrite the out one to see if we still have a real problem
  1306  					out.ImportRoot = fix.out.ImportRoot
  1307  
  1308  					if !reflect.DeepEqual(out, fix.out) {
  1309  						if len(fix.out.Packages) < 2 {
  1310  							t.Errorf("Did not get expected PackageOrErrs:\n\t(GOT): %#v\n\t(WNT): %#v", out, fix.out)
  1311  						} else {
  1312  							seen := make(map[string]bool)
  1313  							for path, perr := range fix.out.Packages {
  1314  								seen[path] = true
  1315  								if operr, exists := out.Packages[path]; !exists {
  1316  									t.Errorf("Expected PackageOrErr for path %s was missing from output:\n\t%s", path, perr)
  1317  								} else {
  1318  									if !reflect.DeepEqual(perr, operr) {
  1319  										t.Errorf("PkgOrErr for path %s was not as expected:\n\t(GOT): %#v\n\t(WNT): %#v", path, operr, perr)
  1320  									}
  1321  								}
  1322  							}
  1323  
  1324  							for path, operr := range out.Packages {
  1325  								if seen[path] {
  1326  									continue
  1327  								}
  1328  
  1329  								t.Errorf("Got PackageOrErr for path %s, but none was expected:\n\t%s", path, operr)
  1330  							}
  1331  						}
  1332  					}
  1333  				}
  1334  			}
  1335  		})
  1336  	}
  1337  }
  1338  
  1339  // Test that ListPackages skips directories for which it lacks permissions to
  1340  // enter and files it lacks permissions to read.
  1341  func TestListPackagesNoPerms(t *testing.T) {
  1342  	if runtime.GOOS == "windows" {
  1343  		// TODO This test doesn't work on windows because I wasn't able to easily
  1344  		// figure out how to chmod a dir in a way that made it untraversable.
  1345  		//
  1346  		// It's not a big deal, though, because the os.IsPermission() call we
  1347  		// use in the real code is effectively what's being tested here, and
  1348  		// that's designed to be cross-platform. So, if the unix tests pass, we
  1349  		// have every reason to believe windows tests would to, if the situation
  1350  		// arises.
  1351  		t.Skip()
  1352  	}
  1353  	tmp, err := ioutil.TempDir("", "listpkgsnp")
  1354  	if err != nil {
  1355  		t.Fatalf("Failed to create temp dir: %s", err)
  1356  	}
  1357  	defer os.RemoveAll(tmp)
  1358  
  1359  	srcdir := filepath.Join(getTestdataRootDir(t), "src", "ren")
  1360  	workdir := filepath.Join(tmp, "ren")
  1361  	fs.CopyDir(srcdir, workdir)
  1362  
  1363  	// chmod the simple dir and m1p/b.go file so they can't be read
  1364  	err = os.Chmod(filepath.Join(workdir, "simple"), 0)
  1365  	if err != nil {
  1366  		t.Fatalf("Error while chmodding simple dir: %s", err)
  1367  	}
  1368  	os.Chmod(filepath.Join(workdir, "m1p", "b.go"), 0)
  1369  	if err != nil {
  1370  		t.Fatalf("Error while chmodding b.go file: %s", err)
  1371  	}
  1372  
  1373  	want := PackageTree{
  1374  		ImportRoot: "ren",
  1375  		Packages: map[string]PackageOrErr{
  1376  			"ren": {
  1377  				Err: &build.NoGoError{
  1378  					Dir: workdir,
  1379  				},
  1380  			},
  1381  			"ren/m1p": {
  1382  				P: Package{
  1383  					ImportPath:  "ren/m1p",
  1384  					CommentPath: "",
  1385  					Name:        "m1p",
  1386  					Imports: []string{
  1387  						"github.com/sdboyer/gps",
  1388  						"sort",
  1389  					},
  1390  				},
  1391  			},
  1392  		},
  1393  	}
  1394  
  1395  	got, err := ListPackages(workdir, "ren")
  1396  
  1397  	if err != nil {
  1398  		t.Fatalf("Unexpected err from ListPackages: %s", err)
  1399  	}
  1400  	if want.ImportRoot != got.ImportRoot {
  1401  		t.Fatalf("Expected ImportRoot %s, got %s", want.ImportRoot, got.ImportRoot)
  1402  	}
  1403  
  1404  	if !reflect.DeepEqual(got, want) {
  1405  		t.Errorf("Did not get expected PackageOrErrs:\n\t(GOT): %#v\n\t(WNT): %#v", got, want)
  1406  		if len(got.Packages) != 2 {
  1407  			if len(got.Packages) == 3 {
  1408  				t.Error("Wrong number of PackageOrErrs - did 'simple' subpackage make it into results somehow?")
  1409  			} else {
  1410  				t.Error("Wrong number of PackageOrErrs")
  1411  			}
  1412  		}
  1413  
  1414  		if got.Packages["ren"].Err == nil {
  1415  			t.Error("Should have gotten error on empty root directory")
  1416  		}
  1417  
  1418  		if !reflect.DeepEqual(got.Packages["ren/m1p"].P.Imports, want.Packages["ren/m1p"].P.Imports) {
  1419  			t.Error("Mismatch between imports in m1p")
  1420  		}
  1421  	}
  1422  }
  1423  
  1424  func TestToReachMap(t *testing.T) {
  1425  	// There's enough in the 'varied' test case to test most of what matters
  1426  	vptree, err := ListPackages(filepath.Join(getTestdataRootDir(t), "src", "github.com", "example", "varied"), "github.com/example/varied")
  1427  	if err != nil {
  1428  		t.Fatalf("ListPackages failed on varied test case: %s", err)
  1429  	}
  1430  
  1431  	// Helper to add github.com/varied/example prefix
  1432  	b := func(s string) string {
  1433  		if s == "" {
  1434  			return "github.com/example/varied"
  1435  		}
  1436  		return "github.com/example/varied/" + s
  1437  	}
  1438  	bl := func(parts ...string) string {
  1439  		for k, s := range parts {
  1440  			parts[k] = b(s)
  1441  		}
  1442  		return strings.Join(parts, " ")
  1443  	}
  1444  
  1445  	// Set up vars for validate closure
  1446  	var want ReachMap
  1447  	var name string
  1448  	var main, tests bool
  1449  	var ignore map[string]bool
  1450  
  1451  	validate := func() {
  1452  		got, em := vptree.ToReachMap(main, tests, true, ignore)
  1453  		if len(em) != 0 {
  1454  			t.Errorf("Should not have any error packages from ToReachMap, got %s", em)
  1455  		}
  1456  		if !reflect.DeepEqual(want, got) {
  1457  			seen := make(map[string]bool)
  1458  			for ip, wantie := range want {
  1459  				seen[ip] = true
  1460  				if gotie, exists := got[ip]; !exists {
  1461  					t.Errorf("ver(%q): expected import path %s was not present in result", name, ip)
  1462  				} else {
  1463  					if !reflect.DeepEqual(wantie, gotie) {
  1464  						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)
  1465  					}
  1466  				}
  1467  			}
  1468  
  1469  			for ip, ie := range got {
  1470  				if seen[ip] {
  1471  					continue
  1472  				}
  1473  				t.Errorf("ver(%q): Got packages for import path %s, but none were expected:\n\t%s", name, ip, ie)
  1474  			}
  1475  		}
  1476  	}
  1477  
  1478  	// maps of each internal package, and their expected external and internal
  1479  	// imports in the maximal case.
  1480  	allex := map[string][]string{
  1481  		b(""):               {"encoding/binary", "github.com/Masterminds/semver", "github.com/sdboyer/gps", "go/parser", "hash", "net/http", "os", "sort"},
  1482  		b("m1p"):            {"github.com/sdboyer/gps", "os", "sort"},
  1483  		b("namemismatch"):   {"github.com/Masterminds/semver", "os"},
  1484  		b("otherpath"):      {"github.com/sdboyer/gps", "os", "sort"},
  1485  		b("simple"):         {"encoding/binary", "github.com/sdboyer/gps", "go/parser", "hash", "os", "sort"},
  1486  		b("simple/another"): {"encoding/binary", "github.com/sdboyer/gps", "hash", "os", "sort"},
  1487  	}
  1488  
  1489  	allin := map[string][]string{
  1490  		b(""):               {b("m1p"), b("namemismatch"), b("otherpath"), b("simple"), b("simple/another")},
  1491  		b("m1p"):            {},
  1492  		b("namemismatch"):   {},
  1493  		b("otherpath"):      {b("m1p")},
  1494  		b("simple"):         {b("m1p"), b("simple/another")},
  1495  		b("simple/another"): {b("m1p")},
  1496  	}
  1497  
  1498  	// build a map to validate the exception inputs. do this because shit is
  1499  	// hard enough to keep track of that it's preferable not to have silent
  1500  	// success if a typo creeps in and we're trying to except an import that
  1501  	// isn't in a pkg in the first place
  1502  	valid := make(map[string]map[string]bool)
  1503  	for ip, expkgs := range allex {
  1504  		m := make(map[string]bool)
  1505  		for _, pkg := range expkgs {
  1506  			m[pkg] = true
  1507  		}
  1508  		valid[ip] = m
  1509  	}
  1510  	validin := make(map[string]map[string]bool)
  1511  	for ip, inpkgs := range allin {
  1512  		m := make(map[string]bool)
  1513  		for _, pkg := range inpkgs {
  1514  			m[pkg] = true
  1515  		}
  1516  		validin[ip] = m
  1517  	}
  1518  
  1519  	// helper to compose want, excepting specific packages
  1520  	//
  1521  	// this makes it easier to see what we're taking out on each test
  1522  	except := func(pkgig ...string) {
  1523  		// reinit expect with everything from all
  1524  		want = make(ReachMap)
  1525  		for ip, expkgs := range allex {
  1526  			var ie struct{ Internal, External []string }
  1527  
  1528  			inpkgs := allin[ip]
  1529  			lenex, lenin := len(expkgs), len(inpkgs)
  1530  			if lenex > 0 {
  1531  				ie.External = make([]string, len(expkgs))
  1532  				copy(ie.External, expkgs)
  1533  			}
  1534  
  1535  			if lenin > 0 {
  1536  				ie.Internal = make([]string, len(inpkgs))
  1537  				copy(ie.Internal, inpkgs)
  1538  			}
  1539  
  1540  			want[ip] = ie
  1541  		}
  1542  
  1543  		// now build the dropmap
  1544  		drop := make(map[string]map[string]bool)
  1545  		for _, igstr := range pkgig {
  1546  			// split on space; first elem is import path to pkg, the rest are
  1547  			// the imports to drop.
  1548  			not := strings.Split(igstr, " ")
  1549  			var ip string
  1550  			ip, not = not[0], not[1:]
  1551  			if _, exists := valid[ip]; !exists {
  1552  				t.Fatalf("%s is not a package name we're working with, doofus", ip)
  1553  			}
  1554  
  1555  			// if only a single elem was passed, though, drop the whole thing
  1556  			if len(not) == 0 {
  1557  				delete(want, ip)
  1558  				continue
  1559  			}
  1560  
  1561  			m := make(map[string]bool)
  1562  			for _, imp := range not {
  1563  				if strings.HasPrefix(imp, "github.com/example/varied") {
  1564  					if !validin[ip][imp] {
  1565  						t.Fatalf("%s is not a reachable import of %s, even in the all case", imp, ip)
  1566  					}
  1567  				} else {
  1568  					if !valid[ip][imp] {
  1569  						t.Fatalf("%s is not a reachable import of %s, even in the all case", imp, ip)
  1570  					}
  1571  				}
  1572  				m[imp] = true
  1573  			}
  1574  
  1575  			drop[ip] = m
  1576  		}
  1577  
  1578  		for ip, ie := range want {
  1579  			var nie struct{ Internal, External []string }
  1580  			for _, imp := range ie.Internal {
  1581  				if !drop[ip][imp] {
  1582  					nie.Internal = append(nie.Internal, imp)
  1583  				}
  1584  			}
  1585  
  1586  			for _, imp := range ie.External {
  1587  				if !drop[ip][imp] {
  1588  					nie.External = append(nie.External, imp)
  1589  				}
  1590  			}
  1591  
  1592  			want[ip] = nie
  1593  		}
  1594  	}
  1595  
  1596  	/* PREP IS DONE, BEGIN ACTUAL TESTING */
  1597  
  1598  	// first, validate all
  1599  	name = "all"
  1600  	main, tests = true, true
  1601  	except()
  1602  	validate()
  1603  
  1604  	// turn off main pkgs, which necessarily doesn't affect anything else
  1605  	name = "no main"
  1606  	main = false
  1607  	except(b(""))
  1608  	validate()
  1609  
  1610  	// ignoring the "varied" pkg has same effect as disabling main pkgs
  1611  	name = "ignore root"
  1612  	ignore = map[string]bool{
  1613  		b(""): true,
  1614  	}
  1615  	main = true
  1616  	validate()
  1617  
  1618  	// when we drop tests, varied/otherpath loses its link to varied/m1p and
  1619  	// varied/simple/another loses its test import, which has a fairly big
  1620  	// cascade
  1621  	name = "no tests"
  1622  	tests = false
  1623  	ignore = nil
  1624  	except(
  1625  		b("")+" encoding/binary",
  1626  		b("simple")+" encoding/binary",
  1627  		b("simple/another")+" encoding/binary",
  1628  		b("otherpath")+" github.com/sdboyer/gps os sort",
  1629  	)
  1630  
  1631  	// almost the same as previous, but varied just goes away completely
  1632  	name = "no main or tests"
  1633  	main = false
  1634  	except(
  1635  		b(""),
  1636  		b("simple")+" encoding/binary",
  1637  		b("simple/another")+" encoding/binary",
  1638  		bl("otherpath", "m1p")+" github.com/sdboyer/gps os sort",
  1639  	)
  1640  	validate()
  1641  
  1642  	// focus on ignores now, so reset main and tests
  1643  	main, tests = true, true
  1644  
  1645  	// now, the fun stuff. punch a hole in the middle by cutting out
  1646  	// varied/simple
  1647  	name = "ignore varied/simple"
  1648  	ignore = map[string]bool{
  1649  		b("simple"): true,
  1650  	}
  1651  	except(
  1652  		// root pkg loses on everything in varied/simple/another
  1653  		// FIXME this is a bit odd, but should probably exclude m1p as well,
  1654  		// because it actually shouldn't be valid to import a package that only
  1655  		// has tests. This whole model misses that nuance right now, though.
  1656  		bl("", "simple", "simple/another")+" hash encoding/binary go/parser",
  1657  		b("simple"),
  1658  	)
  1659  	validate()
  1660  
  1661  	// widen the hole by excluding otherpath
  1662  	name = "ignore varied/{otherpath,simple}"
  1663  	ignore = map[string]bool{
  1664  		b("otherpath"): true,
  1665  		b("simple"):    true,
  1666  	}
  1667  	except(
  1668  		// root pkg loses on everything in varied/simple/another and varied/m1p
  1669  		bl("", "simple", "simple/another", "m1p", "otherpath")+" hash encoding/binary go/parser github.com/sdboyer/gps sort",
  1670  		b("otherpath"),
  1671  		b("simple"),
  1672  	)
  1673  	validate()
  1674  
  1675  	// remove namemismatch, though we're mostly beating a dead horse now
  1676  	name = "ignore varied/{otherpath,simple,namemismatch}"
  1677  	ignore[b("namemismatch")] = true
  1678  	except(
  1679  		// root pkg loses on everything in varied/simple/another and varied/m1p
  1680  		bl("", "simple", "simple/another", "m1p", "otherpath", "namemismatch")+" hash encoding/binary go/parser github.com/sdboyer/gps sort os github.com/Masterminds/semver",
  1681  		b("otherpath"),
  1682  		b("simple"),
  1683  		b("namemismatch"),
  1684  	)
  1685  	validate()
  1686  }
  1687  
  1688  func TestFlattenReachMap(t *testing.T) {
  1689  	// There's enough in the 'varied' test case to test most of what matters
  1690  	vptree, err := ListPackages(filepath.Join(getTestdataRootDir(t), "src", "github.com", "example", "varied"), "github.com/example/varied")
  1691  	if err != nil {
  1692  		t.Fatalf("listPackages failed on varied test case: %s", err)
  1693  	}
  1694  
  1695  	var expect []string
  1696  	var name string
  1697  	var ignore map[string]bool
  1698  	var stdlib, main, tests bool
  1699  
  1700  	validate := func() {
  1701  		rm, em := vptree.ToReachMap(main, tests, true, ignore)
  1702  		if len(em) != 0 {
  1703  			t.Errorf("Should not have any error pkgs from ToReachMap, got %s", em)
  1704  		}
  1705  		result := rm.Flatten(stdlib)
  1706  		if !reflect.DeepEqual(expect, result) {
  1707  			t.Errorf("Wrong imports in %q case:\n\t(GOT): %s\n\t(WNT): %s", name, result, expect)
  1708  		}
  1709  	}
  1710  
  1711  	all := []string{
  1712  		"encoding/binary",
  1713  		"github.com/Masterminds/semver",
  1714  		"github.com/sdboyer/gps",
  1715  		"go/parser",
  1716  		"hash",
  1717  		"net/http",
  1718  		"os",
  1719  		"sort",
  1720  	}
  1721  
  1722  	// helper to rewrite expect, except for a couple packages
  1723  	//
  1724  	// this makes it easier to see what we're taking out on each test
  1725  	except := func(not ...string) {
  1726  		expect = make([]string, len(all)-len(not))
  1727  
  1728  		drop := make(map[string]bool)
  1729  		for _, npath := range not {
  1730  			drop[npath] = true
  1731  		}
  1732  
  1733  		k := 0
  1734  		for _, path := range all {
  1735  			if !drop[path] {
  1736  				expect[k] = path
  1737  				k++
  1738  			}
  1739  		}
  1740  	}
  1741  
  1742  	// everything on
  1743  	name = "simple"
  1744  	except()
  1745  	stdlib, main, tests = true, true, true
  1746  	validate()
  1747  
  1748  	// turning off stdlib should cut most things, but we need to override the
  1749  	// function
  1750  	internal.IsStdLib = doIsStdLib
  1751  	name = "no stdlib"
  1752  	stdlib = false
  1753  	except("encoding/binary", "go/parser", "hash", "net/http", "os", "sort")
  1754  	validate()
  1755  	// restore stdlib func override
  1756  	overrideIsStdLib()
  1757  
  1758  	// stdlib back in; now exclude tests, which should just cut one
  1759  	name = "no tests"
  1760  	stdlib, tests = true, false
  1761  	except("encoding/binary")
  1762  	validate()
  1763  
  1764  	// Now skip main, which still just cuts out one
  1765  	name = "no main"
  1766  	main, tests = false, true
  1767  	except("net/http")
  1768  	validate()
  1769  
  1770  	// No test and no main, which should be additive
  1771  	name = "no test, no main"
  1772  	main, tests = false, false
  1773  	except("net/http", "encoding/binary")
  1774  	validate()
  1775  
  1776  	// now, the ignore tests. turn main and tests back on
  1777  	main, tests = true, true
  1778  
  1779  	// start with non-matching
  1780  	name = "non-matching ignore"
  1781  	ignore = map[string]bool{
  1782  		"nomatch": true,
  1783  	}
  1784  	except()
  1785  	validate()
  1786  
  1787  	// should have the same effect as ignoring main
  1788  	name = "ignore the root"
  1789  	ignore = map[string]bool{
  1790  		"github.com/example/varied": true,
  1791  	}
  1792  	except("net/http")
  1793  	validate()
  1794  
  1795  	// now drop a more interesting one
  1796  	name = "ignore simple"
  1797  	ignore = map[string]bool{
  1798  		"github.com/example/varied/simple": true,
  1799  	}
  1800  	// we get github.com/sdboyer/gps from m1p, too, so it should still be there
  1801  	except("go/parser")
  1802  	validate()
  1803  
  1804  	// now drop two
  1805  	name = "ignore simple and namemismatch"
  1806  	ignore = map[string]bool{
  1807  		"github.com/example/varied/simple":       true,
  1808  		"github.com/example/varied/namemismatch": true,
  1809  	}
  1810  	except("go/parser", "github.com/Masterminds/semver")
  1811  	validate()
  1812  
  1813  	// make sure tests and main play nice with ignore
  1814  	name = "ignore simple and namemismatch, and no tests"
  1815  	tests = false
  1816  	except("go/parser", "github.com/Masterminds/semver", "encoding/binary")
  1817  	validate()
  1818  	name = "ignore simple and namemismatch, and no main"
  1819  	main, tests = false, true
  1820  	except("go/parser", "github.com/Masterminds/semver", "net/http")
  1821  	validate()
  1822  	name = "ignore simple and namemismatch, and no main or tests"
  1823  	main, tests = false, false
  1824  	except("go/parser", "github.com/Masterminds/semver", "net/http", "encoding/binary")
  1825  	validate()
  1826  
  1827  	main, tests = true, true
  1828  
  1829  	// ignore two that should knock out gps
  1830  	name = "ignore both importers"
  1831  	ignore = map[string]bool{
  1832  		"github.com/example/varied/simple": true,
  1833  		"github.com/example/varied/m1p":    true,
  1834  	}
  1835  	except("sort", "github.com/sdboyer/gps", "go/parser")
  1836  	validate()
  1837  
  1838  	// finally, directly ignore some external packages
  1839  	name = "ignore external"
  1840  	ignore = map[string]bool{
  1841  		"github.com/sdboyer/gps": true,
  1842  		"go/parser":              true,
  1843  		"sort":                   true,
  1844  	}
  1845  	except("sort", "github.com/sdboyer/gps", "go/parser")
  1846  	validate()
  1847  
  1848  	// The only thing varied *doesn't* cover is disallowed path patterns
  1849  	ptree, err := ListPackages(filepath.Join(getTestdataRootDir(t), "src", "disallow"), "disallow")
  1850  	if err != nil {
  1851  		t.Fatalf("ListPackages failed on disallow test case: %s", err)
  1852  	}
  1853  
  1854  	rm, em := ptree.ToReachMap(false, false, true, nil)
  1855  	if len(em) != 0 {
  1856  		t.Errorf("Should not have any error packages from ToReachMap, got %s", em)
  1857  	}
  1858  	result := rm.Flatten(true)
  1859  	expect = []string{"github.com/sdboyer/gps", "hash", "sort"}
  1860  	if !reflect.DeepEqual(expect, result) {
  1861  		t.Errorf("Wrong imports in %q case:\n\t(GOT): %s\n\t(WNT): %s", name, result, expect)
  1862  	}
  1863  }
  1864  
  1865  // Verify that we handle import cycles correctly - drop em all
  1866  func TestToReachMapCycle(t *testing.T) {
  1867  	ptree, err := ListPackages(filepath.Join(getTestdataRootDir(t), "src", "cycle"), "cycle")
  1868  	if err != nil {
  1869  		t.Fatalf("ListPackages failed on cycle test case: %s", err)
  1870  	}
  1871  
  1872  	rm, em := ptree.ToReachMap(true, true, false, nil)
  1873  	if len(em) != 0 {
  1874  		t.Errorf("Should not have any error packages from ToReachMap, got %s", em)
  1875  	}
  1876  
  1877  	// FIXME TEMPORARILY COMMENTED UNTIL WE CREATE A BETTER LISTPACKAGES MODEL -
  1878  	//if len(rm) > 0 {
  1879  	//t.Errorf("should be empty reachmap when all packages are in a cycle, got %v", rm)
  1880  	//}
  1881  
  1882  	if len(rm) == 0 {
  1883  		t.Error("TEMPORARY: should ignore import cycles, but cycle was eliminated")
  1884  	}
  1885  }
  1886  
  1887  func getTestdataRootDir(t *testing.T) string {
  1888  	cwd, err := os.Getwd()
  1889  	if err != nil {
  1890  		t.Fatal(err)
  1891  	}
  1892  	return filepath.Join(cwd, "..", "_testdata")
  1893  }