github.com/thockin/go2make@v0.0.0-20221008213743-c1956c0434a7/go2make_test.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package main
    18  
    19  import (
    20  	"bytes"
    21  	"io/ioutil"
    22  	"os"
    23  	"path/filepath"
    24  	"strings"
    25  	"testing"
    26  
    27  	"github.com/google/go-cmp/cmp"
    28  	"github.com/lithammer/dedent"
    29  	"golang.org/x/tools/go/packages"
    30  )
    31  
    32  func initModule(t *testing.T, name string, files map[string]string) string {
    33  	dir := t.TempDir()
    34  	writeFile(t, dir, "go.mod", dedent.Dedent(`
    35  		module example.com/mod
    36  		go 1.18
    37  	`))
    38  	for path, content := range files {
    39  		writeFile(t, dir, path, content)
    40  	}
    41  	return dir
    42  }
    43  
    44  func writeFile(t *testing.T, dir, path, content string) {
    45  	path = filepath.Join(dir, path)
    46  	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
    47  		t.Fatal(err)
    48  	}
    49  	if err := ioutil.WriteFile(path, []byte(content), 0644); err != nil {
    50  		t.Fatal(err)
    51  	}
    52  }
    53  
    54  // build a map of pkg -> filenames
    55  func pkgFiles(pkgs []*packages.Package) map[string][]string {
    56  	out := make(map[string][]string, len(pkgs))
    57  	for _, p := range pkgs {
    58  		skip := false
    59  		for _, e := range p.Errors {
    60  			// Some test cases try bad patterns on purpose.
    61  			if e.Kind == packages.ListError {
    62  				skip = true
    63  				break
    64  			}
    65  		}
    66  		if skip {
    67  			continue
    68  		}
    69  		files := make([]string, 0, len(p.GoFiles))
    70  		for _, f := range p.GoFiles {
    71  			files = append(files, filepath.Base(f))
    72  		}
    73  		out[p.PkgPath] = files
    74  	}
    75  	return out
    76  }
    77  
    78  func TestLoadPackages(t *testing.T) {
    79  	cases := []struct {
    80  		name       string
    81  		files      map[string]string
    82  		tags       []string
    83  		expectPkgs map[string][]string
    84  	}{{
    85  		name: "one_pkg_no_imports",
    86  		files: map[string]string{
    87  			"file.go": dedent.Dedent(`
    88  				package p
    89  				var V string
    90  			`),
    91  		},
    92  		expectPkgs: map[string][]string{
    93  			"example.com/mod": {"file.go"},
    94  		},
    95  	}, {
    96  		name: "one_pkg_with_other_files",
    97  		files: map[string]string{
    98  			"README": "",
    99  			"file.go": dedent.Dedent(`
   100  				package p
   101  				var V string
   102  			`),
   103  			"file_test.go": dedent.Dedent(`
   104  				package p
   105  				var T string
   106  			`),
   107  			"_ignore.go": dedent.Dedent(`
   108  				package p
   109  				var I string
   110  			`),
   111  		},
   112  		expectPkgs: map[string][]string{
   113  			"example.com/mod": {"file.go"},
   114  		},
   115  	}, {
   116  		name: "one_pkg_with_imports",
   117  		files: map[string]string{
   118  			"file.go": dedent.Dedent(`
   119  				package p
   120  				import "io"
   121  				import "os"
   122  				func init() { io.WriteString(os.Stdout, "") }
   123  			`),
   124  		},
   125  		expectPkgs: map[string][]string{
   126  			"example.com/mod": {"file.go"},
   127  		},
   128  	}, {
   129  		name: "multi_pkg_no_imports",
   130  		files: map[string]string{
   131  			"p1/file1.go": dedent.Dedent(`
   132  				package p1
   133  				var V string
   134  			`),
   135  			"p2/file2.go": dedent.Dedent(`
   136  				package p2
   137  				var V string
   138  			`),
   139  			"p3/file3.go": dedent.Dedent(`
   140  				package p3
   141  				var V string
   142  			`),
   143  		},
   144  		expectPkgs: map[string][]string{
   145  			"example.com/mod/p1": {"file1.go"},
   146  			"example.com/mod/p2": {"file2.go"},
   147  			"example.com/mod/p3": {"file3.go"},
   148  		},
   149  	}, {
   150  		name: "multi_pkg_with_imports",
   151  		files: map[string]string{
   152  			"p1/file1.go": dedent.Dedent(`
   153  				package p1
   154  				import "io"
   155  				import "os"
   156  				func init() { io.WriteString(os.Stdout, "") }
   157  			`),
   158  			"p2/file2.go": dedent.Dedent(`
   159  				package p2
   160  				import "io"
   161  				import "os"
   162  				func init() { io.WriteString(os.Stdout, "") }
   163  			`),
   164  			"p3/file3.go": dedent.Dedent(`
   165  				package p3
   166  				import "io"
   167  				import "os"
   168  				func init() { io.WriteString(os.Stdout, "") }
   169  			`),
   170  		},
   171  		expectPkgs: map[string][]string{
   172  			"example.com/mod/p1": {"file1.go"},
   173  			"example.com/mod/p2": {"file2.go"},
   174  			"example.com/mod/p3": {"file3.go"},
   175  		},
   176  	}, {
   177  		name: "multi_pkg_with_tags",
   178  		tags: []string{"foo"},
   179  		files: map[string]string{
   180  			"p1/file1.go": dedent.Dedent(`
   181  				package p1
   182  				var V string
   183  			`),
   184  			"p2/file2.go": dedent.Dedent(`
   185  				//go:build !foo
   186  				// +build !foo
   187  				package p2
   188  				var V string
   189  			`),
   190  			"p3/file3.go": dedent.Dedent(`
   191  				//go:build foo
   192  				// +build foo
   193  				package p3
   194  				var V string
   195  			`),
   196  		},
   197  		expectPkgs: map[string][]string{
   198  			"example.com/mod/p1": {"file1.go"},
   199  			"example.com/mod/p3": {"file3.go"},
   200  		},
   201  	}, {
   202  		name: "multi_pkg_multi_file_with_tags",
   203  		tags: []string{"foo", "bar"},
   204  		files: map[string]string{
   205  			"p1/file1a.go": dedent.Dedent(`
   206  				package p1
   207  				var Va string
   208  			`),
   209  			"p1/file1b.go": dedent.Dedent(`
   210  				package p1
   211  				var Vb string
   212  			`),
   213  			"p2/file2a.go": dedent.Dedent(`
   214  				//go:build !foo
   215  				// +build !foo
   216  				package p2
   217  				var Va string
   218  			`),
   219  			"p2/file2b.go": dedent.Dedent(`
   220  				package p2
   221  				var Vb string
   222  			`),
   223  			"p3/file3a.go": dedent.Dedent(`
   224  				//go:build bar
   225  				// +build bar
   226  				package p3
   227  				var Va string
   228  			`),
   229  			"p3/file3b.go": dedent.Dedent(`
   230  				package p3
   231  				var Vb string
   232  			`),
   233  		},
   234  		expectPkgs: map[string][]string{
   235  			"example.com/mod/p1": {"file1a.go", "file1b.go"},
   236  			"example.com/mod/p2": {"file2b.go"},
   237  			"example.com/mod/p3": {"file3a.go", "file3b.go"},
   238  		},
   239  	}}
   240  
   241  	wd, err := os.Getwd()
   242  	if err != nil {
   243  		t.Fatal(err)
   244  	}
   245  
   246  	for _, tc := range cases {
   247  		t.Run(tc.name, func(t *testing.T) {
   248  			dir := initModule(t, "example.com/mod", tc.files)
   249  
   250  			emit := emitter{
   251  				tags: tc.tags,
   252  			}
   253  
   254  			// pushd
   255  			if err := os.Chdir(dir); err != nil {
   256  				t.Fatal(err)
   257  			}
   258  
   259  			for _, pattern := range []string{"example.com/mod/...", "./..."} {
   260  				pkgs, err := emit.loadPackages(pattern)
   261  				if err != nil {
   262  					t.Errorf("unexpected error: %v", err)
   263  				}
   264  				if want, got := tc.expectPkgs, pkgFiles(pkgs); !cmp.Equal(want, got) {
   265  					t.Errorf("wrong result for pattern %q:\n\twant: %v\n\t got: %v", pattern, want, got)
   266  				}
   267  			}
   268  
   269  			// popd
   270  			if err := os.Chdir(wd); err != nil {
   271  				t.Fatal(err)
   272  			}
   273  		})
   274  	}
   275  }
   276  
   277  func TestLoadPackagesMultiModule(t *testing.T) {
   278  	cases := []struct {
   279  		name          string
   280  		files         map[string]string
   281  		tags          []string
   282  		expectPkgs    map[string][]string
   283  		testSubByPath bool
   284  	}{{
   285  		name: "one_pkg",
   286  		files: map[string]string{
   287  			"file.go": dedent.Dedent(`
   288  				package p
   289  				var V string
   290  			`),
   291  		},
   292  		expectPkgs: map[string][]string{
   293  			"example.com/mod": {"file.go"},
   294  		},
   295  	}, {
   296  		name: "multi_pkg",
   297  		files: map[string]string{
   298  			"p1/file1.go": dedent.Dedent(`
   299  				package p1
   300  				var V string
   301  			`),
   302  			"p2/file2.go": dedent.Dedent(`
   303  				package p2
   304  				var V string
   305  			`),
   306  			"p3/file3.go": dedent.Dedent(`
   307  				package p3
   308  				var V string
   309  			`),
   310  		},
   311  		expectPkgs: map[string][]string{
   312  			"example.com/mod/p1": {"file1.go"},
   313  			"example.com/mod/p2": {"file2.go"},
   314  			"example.com/mod/p3": {"file3.go"},
   315  		},
   316  	}, {
   317  		name: "multi_module_no_workspace",
   318  		files: map[string]string{
   319  			"p1/file1.go": dedent.Dedent(`
   320  				package p1
   321  				var V string
   322  			`),
   323  			"m2/go.mod": dedent.Dedent(`
   324  				module example.com/m2
   325  				go 1.18
   326  			`),
   327  			"m2/file2.go": dedent.Dedent(`
   328  				package m2
   329  				var V string
   330  			`),
   331  			"m3/go.mod": dedent.Dedent(`
   332  				module example.com/m3
   333  				go 1.18
   334  			`),
   335  			"m3/file3.go": dedent.Dedent(`
   336  				package m3
   337  				var V string
   338  			`),
   339  		},
   340  		expectPkgs: map[string][]string{
   341  			"example.com/mod/p1": {"file1.go"},
   342  		},
   343  	}, {
   344  		name: "multi_module_workspace",
   345  		files: map[string]string{
   346  			"go.work": dedent.Dedent(`
   347  				go 1.18
   348  				use (
   349  					.
   350  					./m2
   351  				)
   352  				replace (
   353  					example.com/m2 v0.0.0 => ./m2
   354  				)
   355  			`),
   356  			"p1/file1.go": dedent.Dedent(`
   357  				package p1
   358  				var V string
   359  			`),
   360  			"m2/go.mod": dedent.Dedent(`
   361  				module example.com/m2
   362  				go 1.18
   363  			`),
   364  			"m2/file2.go": dedent.Dedent(`
   365  				package m2
   366  				var V string
   367  			`),
   368  			"m3/go.mod": dedent.Dedent(`
   369  				module example.com/m3
   370  				go 1.18
   371  			`),
   372  			"m3/file3.go": dedent.Dedent(`
   373  				package m3
   374  				var V string
   375  			`),
   376  		},
   377  		expectPkgs: map[string][]string{
   378  			"example.com/mod/p1": {"file1.go"},
   379  			"example.com/m2":     {"file2.go"},
   380  		},
   381  	}}
   382  
   383  	wd, err := os.Getwd()
   384  	if err != nil {
   385  		t.Fatal(err)
   386  	}
   387  
   388  	for _, tc := range cases {
   389  		t.Run(tc.name, func(t *testing.T) {
   390  			emit := emitter{
   391  				tags: tc.tags,
   392  			}
   393  
   394  			// pushd
   395  			dir := initModule(t, "example.com/mod", tc.files)
   396  			if err := os.Chdir(dir); err != nil {
   397  				t.Fatal(err)
   398  			}
   399  
   400  			for _, pattern := range [][]string{{"example.com/mod/...", "example.com/m2/..."}, {"./...", "./m2/..."}, {"all"}} {
   401  				pkgs, err := emit.loadPackages(pattern...)
   402  				if err != nil {
   403  					t.Errorf("unexpected error: %v", err)
   404  				}
   405  				if want, got := tc.expectPkgs, pkgFiles(pkgs); !cmp.Equal(want, got) {
   406  					t.Errorf("wrong result for pattern(s) %q:\n\twant: %v\n\t got: %v", pattern, want, got)
   407  				}
   408  			}
   409  
   410  			// popd
   411  			if err := os.Chdir(wd); err != nil {
   412  				t.Fatal(err)
   413  			}
   414  		})
   415  	}
   416  }
   417  
   418  func TestVisitPackage(t *testing.T) {
   419  	pkgpath := "example.com/mod/pkg"
   420  
   421  	cases := []struct {
   422  		name       string
   423  		pkg        packages.Package
   424  		initMap    func(pkgMap map[string]*packages.Package, pkg *packages.Package) // optional
   425  		expectErrs bool
   426  	}{{
   427  		name: "already_present",
   428  		pkg: packages.Package{
   429  			PkgPath: pkgpath,
   430  		},
   431  		initMap: func(pkgMap map[string]*packages.Package, pkg *packages.Package) {
   432  			pkgMap[pkgpath] = pkg
   433  		},
   434  	}, {
   435  		name: "success",
   436  		pkg: packages.Package{
   437  			PkgPath: pkgpath,
   438  		},
   439  	}, {
   440  		name: "list_error",
   441  		pkg: packages.Package{
   442  			PkgPath: pkgpath,
   443  			Errors:  []packages.Error{{Kind: packages.ListError}},
   444  		},
   445  		expectErrs: true,
   446  	}, {
   447  		name: "parse_error",
   448  		pkg: packages.Package{
   449  			PkgPath: pkgpath,
   450  			Errors: []packages.Error{{
   451  				Kind: packages.ParseError,
   452  			}},
   453  		},
   454  		expectErrs: true,
   455  	}}
   456  
   457  	for _, tc := range cases {
   458  		t.Run(tc.name, func(t *testing.T) {
   459  			pkgMap := map[string]*packages.Package{}
   460  			if tc.initMap != nil {
   461  				tc.initMap(pkgMap, &tc.pkg)
   462  			}
   463  			emit := emitter{}
   464  
   465  			ok := emit.visitPackage(&tc.pkg, pkgMap)
   466  			if ok && tc.expectErrs {
   467  				t.Errorf("unexpected success")
   468  			}
   469  			if !ok && !tc.expectErrs {
   470  				t.Errorf("unexpected failure")
   471  			}
   472  			if want, got := 1, len(pkgMap); want != got {
   473  				t.Errorf("unexpected number of packages: want %d, got %d, %v", want, got, pkgMap)
   474  			}
   475  			if p, found := pkgMap[pkgpath]; !found {
   476  				t.Errorf("package %q not found in map: %v", pkgpath, pkgMap)
   477  			} else if p != &tc.pkg {
   478  				t.Errorf("package %q in map is different pointer: %v", pkgpath, p)
   479  			}
   480  		})
   481  	}
   482  }
   483  
   484  func TestEmitMake(t *testing.T) {
   485  	cases := []struct {
   486  		name   string
   487  		files  map[string]string
   488  		tags   []string
   489  		expect string
   490  	}{{
   491  		name: "one_pkg_no_imports",
   492  		files: map[string]string{
   493  			"file.go": dedent.Dedent(`
   494  				package p
   495  				var V string
   496  			`),
   497  		},
   498  		expect: dedent.Dedent(`
   499  			.go2make/by-pkg/./m2/.../_pkg:
   500  				@mkdir -p $(@D)
   501  				@touch $@
   502  
   503  			.go2make/by-pkg/./m3/.../_pkg:
   504  				@mkdir -p $(@D)
   505  				@touch $@
   506  
   507  			.go2make/by-pkg/example.com/mod/_files: ./
   508  				@mkdir -p $(@D)
   509  				@ls $</*.go | LC_ALL=C sort > $@.tmp
   510  				@if ! cmp -s $@.tmp $@; then \
   511  				    cat $@.tmp > $@; \
   512  				fi
   513  				@rm -f $@.tmp
   514  
   515  			.go2make/by-pkg/example.com/mod/_pkg: .go2make/by-pkg/example.com/mod/_files \
   516  			  ./file.go
   517  				@mkdir -p $(@D)
   518  				@touch $@
   519  
   520  			.go2make/by-path/./_pkg: .go2make/by-pkg/example.com/mod/_pkg
   521  				@mkdir -p $(@D)
   522  				@touch $@
   523  		`),
   524  	}, {
   525  		name: "one_pkg_with_other_files",
   526  		files: map[string]string{
   527  			"README": "",
   528  			"file.go": dedent.Dedent(`
   529  				package p
   530  				var V string
   531  			`),
   532  			"file_test.go": dedent.Dedent(`
   533  				package p
   534  				import "io"
   535  				import "os"
   536  				func init() { io.WriteString(os.Stdout, "") }
   537  			`),
   538  			"_ignore.go": dedent.Dedent(`
   539  				package p
   540  				import "io"
   541  				import "os"
   542  				func init() { io.WriteString(os.Stdout, "") }
   543  			`),
   544  		},
   545  		expect: dedent.Dedent(`
   546  			.go2make/by-pkg/./m2/.../_pkg:
   547  				@mkdir -p $(@D)
   548  				@touch $@
   549  
   550  			.go2make/by-pkg/./m3/.../_pkg:
   551  				@mkdir -p $(@D)
   552  				@touch $@
   553  
   554  			.go2make/by-pkg/example.com/mod/_files: ./
   555  				@mkdir -p $(@D)
   556  				@ls $</*.go | LC_ALL=C sort > $@.tmp
   557  				@if ! cmp -s $@.tmp $@; then \
   558  				    cat $@.tmp > $@; \
   559  				fi
   560  				@rm -f $@.tmp
   561  
   562  			.go2make/by-pkg/example.com/mod/_pkg: .go2make/by-pkg/example.com/mod/_files \
   563  			  ./file.go
   564  				@mkdir -p $(@D)
   565  				@touch $@
   566  
   567  			.go2make/by-path/./_pkg: .go2make/by-pkg/example.com/mod/_pkg
   568  				@mkdir -p $(@D)
   569  				@touch $@
   570  		`),
   571  	}, {
   572  		name: "one_pkg_with_imports",
   573  		files: map[string]string{
   574  			"file.go": dedent.Dedent(`
   575  				package p
   576  				import "io"
   577  				import "os"
   578  				func init() { io.WriteString(os.Stdout, "") }
   579  			`),
   580  		},
   581  		expect: dedent.Dedent(`
   582  			.go2make/by-pkg/./m2/.../_pkg:
   583  				@mkdir -p $(@D)
   584  				@touch $@
   585  
   586  			.go2make/by-pkg/./m3/.../_pkg:
   587  				@mkdir -p $(@D)
   588  				@touch $@
   589  
   590  			.go2make/by-pkg/example.com/mod/_files: ./
   591  				@mkdir -p $(@D)
   592  				@ls $</*.go | LC_ALL=C sort > $@.tmp
   593  				@if ! cmp -s $@.tmp $@; then \
   594  				    cat $@.tmp > $@; \
   595  				fi
   596  				@rm -f $@.tmp
   597  
   598  			.go2make/by-pkg/example.com/mod/_pkg: .go2make/by-pkg/example.com/mod/_files \
   599  			  ./file.go
   600  				@mkdir -p $(@D)
   601  				@touch $@
   602  
   603  			.go2make/by-path/./_pkg: .go2make/by-pkg/example.com/mod/_pkg
   604  				@mkdir -p $(@D)
   605  				@touch $@
   606  		`),
   607  	}, {
   608  		name: "multi_pkg_no_imports",
   609  		files: map[string]string{
   610  			"p1/file1.go": dedent.Dedent(`
   611  				package p1
   612  				var V string
   613  			`),
   614  			"p2/file2.go": dedent.Dedent(`
   615  				package p2
   616  				import "example.com/mod/p1"
   617  				var V = p1.V
   618  			`),
   619  			"p3/file3.go": dedent.Dedent(`
   620  				package p3
   621  				import "example.com/mod/p1"
   622  				import "example.com/mod/p2"
   623  				var V = p1.V + p2.V
   624  			`),
   625  		},
   626  		expect: dedent.Dedent(`
   627  			.go2make/by-pkg/./m2/.../_pkg:
   628  				@mkdir -p $(@D)
   629  				@touch $@
   630  
   631  			.go2make/by-pkg/./m3/.../_pkg:
   632  				@mkdir -p $(@D)
   633  				@touch $@
   634  
   635  			.go2make/by-pkg/example.com/mod/p1/_files: ./p1/
   636  				@mkdir -p $(@D)
   637  				@ls $</*.go | LC_ALL=C sort > $@.tmp
   638  				@if ! cmp -s $@.tmp $@; then \
   639  				    cat $@.tmp > $@; \
   640  				fi
   641  				@rm -f $@.tmp
   642  
   643  			.go2make/by-pkg/example.com/mod/p1/_pkg: .go2make/by-pkg/example.com/mod/p1/_files \
   644  			  ./p1/file1.go
   645  				@mkdir -p $(@D)
   646  				@touch $@
   647  
   648  			.go2make/by-path/./p1/_pkg: .go2make/by-pkg/example.com/mod/p1/_pkg
   649  				@mkdir -p $(@D)
   650  				@touch $@
   651  
   652  			.go2make/by-pkg/example.com/mod/p2/_files: ./p2/
   653  				@mkdir -p $(@D)
   654  				@ls $</*.go | LC_ALL=C sort > $@.tmp
   655  				@if ! cmp -s $@.tmp $@; then \
   656  				    cat $@.tmp > $@; \
   657  				fi
   658  				@rm -f $@.tmp
   659  
   660  			.go2make/by-pkg/example.com/mod/p2/_pkg: .go2make/by-pkg/example.com/mod/p2/_files \
   661  			  ./p2/file2.go \
   662  			  .go2make/by-pkg/example.com/mod/p1/_pkg
   663  				@mkdir -p $(@D)
   664  				@touch $@
   665  
   666  			.go2make/by-path/./p2/_pkg: .go2make/by-pkg/example.com/mod/p2/_pkg
   667  				@mkdir -p $(@D)
   668  				@touch $@
   669  
   670  			.go2make/by-pkg/example.com/mod/p3/_files: ./p3/
   671  				@mkdir -p $(@D)
   672  				@ls $</*.go | LC_ALL=C sort > $@.tmp
   673  				@if ! cmp -s $@.tmp $@; then \
   674  				    cat $@.tmp > $@; \
   675  				fi
   676  				@rm -f $@.tmp
   677  
   678  			.go2make/by-pkg/example.com/mod/p3/_pkg: .go2make/by-pkg/example.com/mod/p3/_files \
   679  			  ./p3/file3.go \
   680  			  .go2make/by-pkg/example.com/mod/p1/_pkg \
   681  			  .go2make/by-pkg/example.com/mod/p2/_pkg
   682  				@mkdir -p $(@D)
   683  				@touch $@
   684  
   685  			.go2make/by-path/./p3/_pkg: .go2make/by-pkg/example.com/mod/p3/_pkg
   686  				@mkdir -p $(@D)
   687  				@touch $@
   688  		`),
   689  	}, {
   690  		name: "multi_module_workspace",
   691  		files: map[string]string{
   692  			"go.work": dedent.Dedent(`
   693  				go 1.18
   694  				use (
   695  					.
   696  					./m2
   697  				)
   698  				replace (
   699  					example.com/m2 v0.0.0 => ./m2
   700  				)
   701  			`),
   702  			"p1/file1.go": dedent.Dedent(`
   703  				package p1
   704  				import "example.com/m2"
   705  				var V = m2.V
   706  			`),
   707  			"m2/go.mod": dedent.Dedent(`
   708  				module example.com/m2
   709  				go 1.18
   710  			`),
   711  			"m2/file2.go": dedent.Dedent(`
   712  				package m2
   713  				var V string
   714  			`),
   715  			"m3/go.mod": dedent.Dedent(`
   716  				module example.com/m3
   717  				go 1.18
   718  			`),
   719  			"m3/file3.go": dedent.Dedent(`
   720  				package m3
   721  				var V string
   722  			`),
   723  		},
   724  		expect: dedent.Dedent(`
   725  			.go2make/by-pkg/./m3/.../_pkg:
   726  				@mkdir -p $(@D)
   727  				@touch $@
   728  
   729  			.go2make/by-pkg/example.com/m2/_files: ./m2/
   730  				@mkdir -p $(@D)
   731  				@ls $</*.go | LC_ALL=C sort > $@.tmp
   732  				@if ! cmp -s $@.tmp $@; then \
   733  				    cat $@.tmp > $@; \
   734  				fi
   735  				@rm -f $@.tmp
   736  
   737  			.go2make/by-pkg/example.com/m2/_pkg: .go2make/by-pkg/example.com/m2/_files \
   738  			  ./m2/file2.go
   739  				@mkdir -p $(@D)
   740  				@touch $@
   741  
   742  			.go2make/by-path/./m2/_pkg: .go2make/by-pkg/example.com/m2/_pkg
   743  				@mkdir -p $(@D)
   744  				@touch $@
   745  
   746  			.go2make/by-pkg/example.com/mod/p1/_files: ./p1/
   747  				@mkdir -p $(@D)
   748  				@ls $</*.go | LC_ALL=C sort > $@.tmp
   749  				@if ! cmp -s $@.tmp $@; then \
   750  				    cat $@.tmp > $@; \
   751  				fi
   752  				@rm -f $@.tmp
   753  
   754  			.go2make/by-pkg/example.com/mod/p1/_pkg: .go2make/by-pkg/example.com/mod/p1/_files \
   755  			  ./p1/file1.go \
   756  			  .go2make/by-pkg/example.com/m2/_pkg
   757  				@mkdir -p $(@D)
   758  				@touch $@
   759  
   760  			.go2make/by-path/./p1/_pkg: .go2make/by-pkg/example.com/mod/p1/_pkg
   761  				@mkdir -p $(@D)
   762  				@touch $@
   763  		`),
   764  	}}
   765  
   766  	wd, err := os.Getwd()
   767  	if err != nil {
   768  		t.Fatal(err)
   769  	}
   770  
   771  	for _, tc := range cases {
   772  		t.Run(tc.name, func(t *testing.T) {
   773  			dir := initModule(t, "example.com/mod", tc.files)
   774  
   775  			emit := emitter{
   776  				stateDir:     ".go2make",
   777  				relPath:      dir,
   778  				ignoreErrors: true, // easier output comparison
   779  			}
   780  
   781  			// pushd
   782  			if err := os.Chdir(dir); err != nil {
   783  				t.Fatal(err)
   784  			}
   785  
   786  			pkgs, err := emit.loadPackages("./...", "./m2/...", "./m3/...")
   787  			if err != nil {
   788  				t.Errorf("unexpected error: %v", err)
   789  			}
   790  			pkgMap := emit.visitPackages(pkgs)
   791  			if pkgMap == nil {
   792  				t.Errorf("unexpected error")
   793  			}
   794  			buf := bytes.Buffer{}
   795  			emit.emitMake(&buf, pkgMap)
   796  			if want, got := strings.Trim(tc.expect, "\n"), strings.Trim(buf.String(), "\n"); want != got {
   797  				t.Errorf("wrong result:\n%s", cmp.Diff(want, got))
   798  			}
   799  
   800  			// popd
   801  			if err := os.Chdir(wd); err != nil {
   802  				t.Fatal(err)
   803  			}
   804  		})
   805  	}
   806  }