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

     1  package gps
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  	"runtime"
    11  	"sync"
    12  	"sync/atomic"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/Masterminds/semver"
    17  )
    18  
    19  var bd string
    20  
    21  // An analyzer that passes nothing back, but doesn't error. This is the naive
    22  // case - no constraints, no lock, and no errors. The SourceMgr will interpret
    23  // this as open/Any constraints on everything in the import graph.
    24  type naiveAnalyzer struct{}
    25  
    26  func (naiveAnalyzer) DeriveManifestAndLock(string, ProjectRoot) (Manifest, Lock, error) {
    27  	return nil, nil, nil
    28  }
    29  
    30  func (a naiveAnalyzer) Info() (name string, version int) {
    31  	return "naive-analyzer", 1
    32  }
    33  
    34  func sv(s string) *semver.Version {
    35  	sv, err := semver.NewVersion(s)
    36  	if err != nil {
    37  		panic(fmt.Sprintf("Error creating semver from %q: %s", s, err))
    38  	}
    39  
    40  	return sv
    41  }
    42  
    43  func mkNaiveSM(t *testing.T) (*SourceMgr, func()) {
    44  	cpath, err := ioutil.TempDir("", "smcache")
    45  	if err != nil {
    46  		t.Fatalf("Failed to create temp dir: %s", err)
    47  	}
    48  
    49  	sm, err := NewSourceManager(cpath)
    50  	if err != nil {
    51  		t.Fatalf("Unexpected error on SourceManager creation: %s", err)
    52  	}
    53  
    54  	return sm, func() {
    55  		sm.Release()
    56  		err := removeAll(cpath)
    57  		if err != nil {
    58  			t.Errorf("removeAll failed: %s", err)
    59  		}
    60  	}
    61  }
    62  
    63  func remakeNaiveSM(osm *SourceMgr, t *testing.T) (*SourceMgr, func()) {
    64  	cpath := osm.cachedir
    65  	osm.Release()
    66  
    67  	sm, err := NewSourceManager(cpath)
    68  	if err != nil {
    69  		t.Fatalf("unexpected error on SourceManager recreation: %s", err)
    70  	}
    71  
    72  	return sm, func() {
    73  		sm.Release()
    74  		err := removeAll(cpath)
    75  		if err != nil {
    76  			t.Errorf("removeAll failed: %s", err)
    77  		}
    78  	}
    79  }
    80  
    81  func init() {
    82  	_, filename, _, _ := runtime.Caller(1)
    83  	bd = path.Dir(filename)
    84  }
    85  
    86  func TestSourceManagerInit(t *testing.T) {
    87  	cpath, err := ioutil.TempDir("", "smcache")
    88  	if err != nil {
    89  		t.Errorf("Failed to create temp dir: %s", err)
    90  	}
    91  	sm, err := NewSourceManager(cpath)
    92  
    93  	if err != nil {
    94  		t.Errorf("Unexpected error on SourceManager creation: %s", err)
    95  	}
    96  
    97  	_, err = NewSourceManager(cpath)
    98  	if err == nil {
    99  		t.Errorf("Creating second SourceManager should have failed due to file lock contention")
   100  	} else if te, ok := err.(CouldNotCreateLockError); !ok {
   101  		t.Errorf("Should have gotten CouldNotCreateLockError error type, but got %T", te)
   102  	}
   103  
   104  	if _, err = os.Stat(path.Join(cpath, "sm.lock")); err != nil {
   105  		t.Errorf("Global cache lock file not created correctly")
   106  	}
   107  
   108  	sm.Release()
   109  	err = removeAll(cpath)
   110  	if err != nil {
   111  		t.Errorf("removeAll failed: %s", err)
   112  	}
   113  
   114  	if _, err = os.Stat(path.Join(cpath, "sm.lock")); !os.IsNotExist(err) {
   115  		t.Fatalf("Global cache lock file not cleared correctly on Release()")
   116  	}
   117  
   118  	// Set another one up at the same spot now, just to be sure
   119  	sm, err = NewSourceManager(cpath)
   120  	if err != nil {
   121  		t.Errorf("Creating a second SourceManager should have succeeded when the first was released, but failed with err %s", err)
   122  	}
   123  
   124  	sm.Release()
   125  	err = removeAll(cpath)
   126  	if err != nil {
   127  		t.Errorf("removeAll failed: %s", err)
   128  	}
   129  }
   130  
   131  func TestSourceInit(t *testing.T) {
   132  	// This test is a bit slow, skip it on -short
   133  	if testing.Short() {
   134  		t.Skip("Skipping project manager init test in short mode")
   135  	}
   136  
   137  	cpath, err := ioutil.TempDir("", "smcache")
   138  	if err != nil {
   139  		t.Fatalf("Failed to create temp dir: %s", err)
   140  	}
   141  
   142  	sm, err := NewSourceManager(cpath)
   143  	if err != nil {
   144  		t.Fatalf("Unexpected error on SourceManager creation: %s", err)
   145  	}
   146  
   147  	defer func() {
   148  		sm.Release()
   149  		err := removeAll(cpath)
   150  		if err != nil {
   151  			t.Errorf("removeAll failed: %s", err)
   152  		}
   153  	}()
   154  
   155  	id := mkPI("github.com/sdboyer/gpkt").normalize()
   156  	pvl, err := sm.ListVersions(id)
   157  	if err != nil {
   158  		t.Errorf("Unexpected error during initial project setup/fetching %s", err)
   159  	}
   160  
   161  	if len(pvl) != 7 {
   162  		t.Errorf("Expected seven version results from the test repo, got %v", len(pvl))
   163  	} else {
   164  		expected := []PairedVersion{
   165  			NewVersion("v2.0.0").Is(Revision("4a54adf81c75375d26d376459c00d5ff9b703e5e")),
   166  			NewVersion("v1.1.0").Is(Revision("b2cb48dda625f6640b34d9ffb664533359ac8b91")),
   167  			NewVersion("v1.0.0").Is(Revision("bf85021c0405edbc4f3648b0603818d641674f72")),
   168  			newDefaultBranch("master").Is(Revision("bf85021c0405edbc4f3648b0603818d641674f72")),
   169  			NewBranch("v1").Is(Revision("e3777f683305eafca223aefe56b4e8ecf103f467")),
   170  			NewBranch("v1.1").Is(Revision("f1fbc520489a98306eb28c235204e39fa8a89c84")),
   171  			NewBranch("v3").Is(Revision("4a54adf81c75375d26d376459c00d5ff9b703e5e")),
   172  		}
   173  
   174  		// SourceManager itself doesn't guarantee ordering; sort them here so we
   175  		// can dependably check output
   176  		SortPairedForUpgrade(pvl)
   177  
   178  		for k, e := range expected {
   179  			if !pvl[k].Matches(e) {
   180  				t.Errorf("Expected version %s in position %v but got %s", e, k, pvl[k])
   181  			}
   182  		}
   183  	}
   184  
   185  	// Two birds, one stone - make sure the internal ProjectManager vlist cache
   186  	// works (or at least doesn't not work) by asking for the versions again,
   187  	// and do it through smcache to ensure its sorting works, as well.
   188  	smc := &bridge{
   189  		sm:     sm,
   190  		vlists: make(map[ProjectIdentifier][]Version),
   191  		s:      &solver{mtr: newMetrics()},
   192  	}
   193  
   194  	vl, err := smc.listVersions(id)
   195  	if err != nil {
   196  		t.Errorf("Unexpected error during initial project setup/fetching %s", err)
   197  	}
   198  
   199  	if len(vl) != 7 {
   200  		t.Errorf("Expected seven version results from the test repo, got %v", len(vl))
   201  	} else {
   202  		expected := []Version{
   203  			NewVersion("v2.0.0").Is(Revision("4a54adf81c75375d26d376459c00d5ff9b703e5e")),
   204  			NewVersion("v1.1.0").Is(Revision("b2cb48dda625f6640b34d9ffb664533359ac8b91")),
   205  			NewVersion("v1.0.0").Is(Revision("bf85021c0405edbc4f3648b0603818d641674f72")),
   206  			newDefaultBranch("master").Is(Revision("bf85021c0405edbc4f3648b0603818d641674f72")),
   207  			NewBranch("v1").Is(Revision("e3777f683305eafca223aefe56b4e8ecf103f467")),
   208  			NewBranch("v1.1").Is(Revision("f1fbc520489a98306eb28c235204e39fa8a89c84")),
   209  			NewBranch("v3").Is(Revision("4a54adf81c75375d26d376459c00d5ff9b703e5e")),
   210  		}
   211  
   212  		for k, e := range expected {
   213  			if !vl[k].Matches(e) {
   214  				t.Errorf("Expected version %s in position %v but got %s", e, k, vl[k])
   215  			}
   216  		}
   217  
   218  		if !vl[3].(versionPair).v.(branchVersion).isDefault {
   219  			t.Error("Expected master branch version to have isDefault flag, but it did not")
   220  		}
   221  		if vl[4].(versionPair).v.(branchVersion).isDefault {
   222  			t.Error("Expected v1 branch version not to have isDefault flag, but it did")
   223  		}
   224  		if vl[5].(versionPair).v.(branchVersion).isDefault {
   225  			t.Error("Expected v1.1 branch version not to have isDefault flag, but it did")
   226  		}
   227  		if vl[6].(versionPair).v.(branchVersion).isDefault {
   228  			t.Error("Expected v3 branch version not to have isDefault flag, but it did")
   229  		}
   230  	}
   231  
   232  	present, err := smc.RevisionPresentIn(id, Revision("4a54adf81c75375d26d376459c00d5ff9b703e5e"))
   233  	if err != nil {
   234  		t.Errorf("Should have found revision in source, but got err: %s", err)
   235  	} else if !present {
   236  		t.Errorf("Should have found revision in source, but did not")
   237  	}
   238  
   239  	// SyncSourceFor will ensure we have everything
   240  	err = smc.SyncSourceFor(id)
   241  	if err != nil {
   242  		t.Errorf("SyncSourceFor failed with unexpected error: %s", err)
   243  	}
   244  
   245  	// Ensure that the appropriate cache dirs and files exist
   246  	_, err = os.Stat(filepath.Join(cpath, "sources", "https---github.com-sdboyer-gpkt", ".git"))
   247  	if err != nil {
   248  		t.Error("Cache repo does not exist in expected location")
   249  	}
   250  
   251  	_, err = os.Stat(filepath.Join(cpath, "metadata", "github.com", "sdboyer", "gpkt", "cache.json"))
   252  	if err != nil {
   253  		// TODO(sdboyer) disabled until we get caching working
   254  		//t.Error("Metadata cache json file does not exist in expected location")
   255  	}
   256  
   257  	// Ensure source existence values are what we expect
   258  	var exists bool
   259  	exists, err = sm.SourceExists(id)
   260  	if err != nil {
   261  		t.Errorf("Error on checking SourceExists: %s", err)
   262  	}
   263  	if !exists {
   264  		t.Error("Source should exist after non-erroring call to ListVersions")
   265  	}
   266  }
   267  
   268  func TestDefaultBranchAssignment(t *testing.T) {
   269  	if testing.Short() {
   270  		t.Skip("Skipping default branch assignment test in short mode")
   271  	}
   272  
   273  	sm, clean := mkNaiveSM(t)
   274  	defer clean()
   275  
   276  	id := mkPI("github.com/sdboyer/test-multibranch")
   277  	v, err := sm.ListVersions(id)
   278  	if err != nil {
   279  		t.Errorf("Unexpected error during initial project setup/fetching %s", err)
   280  	}
   281  
   282  	if len(v) != 3 {
   283  		t.Errorf("Expected three version results from the test repo, got %v", len(v))
   284  	} else {
   285  		brev := Revision("fda020843ac81352004b9dca3fcccdd517600149")
   286  		mrev := Revision("9f9c3a591773d9b28128309ac7a9a72abcab267d")
   287  		expected := []PairedVersion{
   288  			NewBranch("branchone").Is(brev),
   289  			NewBranch("otherbranch").Is(brev),
   290  			NewBranch("master").Is(mrev),
   291  		}
   292  
   293  		SortPairedForUpgrade(v)
   294  
   295  		for k, e := range expected {
   296  			if !v[k].Matches(e) {
   297  				t.Errorf("Expected version %s in position %v but got %s", e, k, v[k])
   298  			}
   299  		}
   300  
   301  		if !v[0].(versionPair).v.(branchVersion).isDefault {
   302  			t.Error("Expected branchone branch version to have isDefault flag, but it did not")
   303  		}
   304  		if !v[0].(versionPair).v.(branchVersion).isDefault {
   305  			t.Error("Expected otherbranch branch version to have isDefault flag, but it did not")
   306  		}
   307  		if v[2].(versionPair).v.(branchVersion).isDefault {
   308  			t.Error("Expected master branch version not to have isDefault flag, but it did")
   309  		}
   310  	}
   311  }
   312  
   313  func TestMgrMethodsFailWithBadPath(t *testing.T) {
   314  	// a symbol will always bork it up
   315  	bad := mkPI("foo/##&^").normalize()
   316  	sm, clean := mkNaiveSM(t)
   317  	defer clean()
   318  
   319  	var err error
   320  	if _, err = sm.SourceExists(bad); err == nil {
   321  		t.Error("SourceExists() did not error on bad input")
   322  	}
   323  	if err = sm.SyncSourceFor(bad); err == nil {
   324  		t.Error("SyncSourceFor() did not error on bad input")
   325  	}
   326  	if _, err = sm.ListVersions(bad); err == nil {
   327  		t.Error("ListVersions() did not error on bad input")
   328  	}
   329  	if _, err = sm.RevisionPresentIn(bad, Revision("")); err == nil {
   330  		t.Error("RevisionPresentIn() did not error on bad input")
   331  	}
   332  	if _, err = sm.ListPackages(bad, nil); err == nil {
   333  		t.Error("ListPackages() did not error on bad input")
   334  	}
   335  	if _, _, err = sm.GetManifestAndLock(bad, nil, naiveAnalyzer{}); err == nil {
   336  		t.Error("GetManifestAndLock() did not error on bad input")
   337  	}
   338  	if err = sm.ExportProject(bad, nil, ""); err == nil {
   339  		t.Error("ExportProject() did not error on bad input")
   340  	}
   341  }
   342  
   343  func TestGetSources(t *testing.T) {
   344  	// This test is a tad slow, skip it on -short
   345  	if testing.Short() {
   346  		t.Skip("Skipping source setup test in short mode")
   347  	}
   348  	requiresBins(t, "git", "hg", "bzr")
   349  
   350  	sm, clean := mkNaiveSM(t)
   351  
   352  	pil := []ProjectIdentifier{
   353  		mkPI("github.com/Masterminds/VCSTestRepo").normalize(),
   354  		mkPI("bitbucket.org/mattfarina/testhgrepo").normalize(),
   355  		mkPI("launchpad.net/govcstestbzrrepo").normalize(),
   356  	}
   357  
   358  	ctx := context.Background()
   359  	// protects against premature release of sm
   360  	t.Run("inner", func(t *testing.T) {
   361  		for _, pi := range pil {
   362  			lpi := pi
   363  			t.Run(lpi.normalizedSource(), func(t *testing.T) {
   364  				t.Parallel()
   365  
   366  				srcg, err := sm.srcCoord.getSourceGatewayFor(ctx, lpi)
   367  				if err != nil {
   368  					t.Errorf("unexpected error setting up source: %s", err)
   369  					return
   370  				}
   371  
   372  				// Re-get the same, make sure they are the same
   373  				srcg2, err := sm.srcCoord.getSourceGatewayFor(ctx, lpi)
   374  				if err != nil {
   375  					t.Errorf("unexpected error re-getting source: %s", err)
   376  				} else if srcg != srcg2 {
   377  					t.Error("first and second sources are not eq")
   378  				}
   379  
   380  				// All of them _should_ select https, so this should work
   381  				lpi.Source = "https://" + lpi.Source
   382  				srcg3, err := sm.srcCoord.getSourceGatewayFor(ctx, lpi)
   383  				if err != nil {
   384  					t.Errorf("unexpected error getting explicit https source: %s", err)
   385  				} else if srcg != srcg3 {
   386  					t.Error("explicit https source should reuse autodetected https source")
   387  				}
   388  
   389  				// Now put in http, and they should differ
   390  				lpi.Source = "http://" + string(lpi.ProjectRoot)
   391  				srcg4, err := sm.srcCoord.getSourceGatewayFor(ctx, lpi)
   392  				if err != nil {
   393  					t.Errorf("unexpected error getting explicit http source: %s", err)
   394  				} else if srcg == srcg4 {
   395  					t.Error("explicit http source should create a new src")
   396  				}
   397  			})
   398  		}
   399  	})
   400  
   401  	// nine entries (of which three are dupes): for each vcs, raw import path,
   402  	// the https url, and the http url
   403  	if len(sm.srcCoord.nameToURL) != 9 {
   404  		t.Errorf("Should have nine discrete entries in the nameToURL map, got %v", len(sm.srcCoord.nameToURL))
   405  	}
   406  	clean()
   407  }
   408  
   409  // Regression test for #32
   410  func TestGetInfoListVersionsOrdering(t *testing.T) {
   411  	// This test is quite slow, skip it on -short
   412  	if testing.Short() {
   413  		t.Skip("Skipping slow test in short mode")
   414  	}
   415  
   416  	sm, clean := mkNaiveSM(t)
   417  	defer clean()
   418  
   419  	// setup done, now do the test
   420  
   421  	id := mkPI("github.com/sdboyer/gpkt").normalize()
   422  
   423  	_, _, err := sm.GetManifestAndLock(id, NewVersion("v1.0.0"), naiveAnalyzer{})
   424  	if err != nil {
   425  		t.Errorf("Unexpected error from GetInfoAt %s", err)
   426  	}
   427  
   428  	v, err := sm.ListVersions(id)
   429  	if err != nil {
   430  		t.Errorf("Unexpected error from ListVersions %s", err)
   431  	}
   432  
   433  	if len(v) != 7 {
   434  		t.Errorf("Expected seven results from ListVersions, got %v", len(v))
   435  	}
   436  }
   437  
   438  func TestDeduceProjectRoot(t *testing.T) {
   439  	sm, clean := mkNaiveSM(t)
   440  	defer clean()
   441  
   442  	in := "github.com/sdboyer/gps"
   443  	pr, err := sm.DeduceProjectRoot(in)
   444  	if err != nil {
   445  		t.Errorf("Problem while detecting root of %q %s", in, err)
   446  	}
   447  	if string(pr) != in {
   448  		t.Errorf("Wrong project root was deduced;\n\t(GOT) %s\n\t(WNT) %s", pr, in)
   449  	}
   450  	if sm.deduceCoord.rootxt.Len() != 1 {
   451  		t.Errorf("Root path trie should have one element after one deduction, has %v", sm.deduceCoord.rootxt.Len())
   452  	}
   453  
   454  	pr, err = sm.DeduceProjectRoot(in)
   455  	if err != nil {
   456  		t.Errorf("Problem while detecting root of %q %s", in, err)
   457  	} else if string(pr) != in {
   458  		t.Errorf("Wrong project root was deduced;\n\t(GOT) %s\n\t(WNT) %s", pr, in)
   459  	}
   460  	if sm.deduceCoord.rootxt.Len() != 1 {
   461  		t.Errorf("Root path trie should still have one element after performing the same deduction twice; has %v", sm.deduceCoord.rootxt.Len())
   462  	}
   463  
   464  	// Now do a subpath
   465  	sub := path.Join(in, "foo")
   466  	pr, err = sm.DeduceProjectRoot(sub)
   467  	if err != nil {
   468  		t.Errorf("Problem while detecting root of %q %s", sub, err)
   469  	} else if string(pr) != in {
   470  		t.Errorf("Wrong project root was deduced;\n\t(GOT) %s\n\t(WNT) %s", pr, in)
   471  	}
   472  	if sm.deduceCoord.rootxt.Len() != 1 {
   473  		t.Errorf("Root path trie should still have one element, as still only one unique root has gone in; has %v", sm.deduceCoord.rootxt.Len())
   474  	}
   475  
   476  	// Now do a fully different root, but still on github
   477  	in2 := "github.com/bagel/lox"
   478  	sub2 := path.Join(in2, "cheese")
   479  	pr, err = sm.DeduceProjectRoot(sub2)
   480  	if err != nil {
   481  		t.Errorf("Problem while detecting root of %q %s", sub2, err)
   482  	} else if string(pr) != in2 {
   483  		t.Errorf("Wrong project root was deduced;\n\t(GOT) %s\n\t(WNT) %s", pr, in)
   484  	}
   485  	if sm.deduceCoord.rootxt.Len() != 2 {
   486  		t.Errorf("Root path trie should have two elements, one for each unique root; has %v", sm.deduceCoord.rootxt.Len())
   487  	}
   488  
   489  	// Ensure that our prefixes are bounded by path separators
   490  	in4 := "github.com/bagel/loxx"
   491  	pr, err = sm.DeduceProjectRoot(in4)
   492  	if err != nil {
   493  		t.Errorf("Problem while detecting root of %q %s", in4, err)
   494  	} else if string(pr) != in4 {
   495  		t.Errorf("Wrong project root was deduced;\n\t(GOT) %s\n\t(WNT) %s", pr, in)
   496  	}
   497  	if sm.deduceCoord.rootxt.Len() != 3 {
   498  		t.Errorf("Root path trie should have three elements, one for each unique root; has %v", sm.deduceCoord.rootxt.Len())
   499  	}
   500  
   501  	// Ensure that vcs extension-based matching comes through
   502  	in5 := "ffffrrrraaaaaapppppdoesnotresolve.com/baz.git"
   503  	pr, err = sm.DeduceProjectRoot(in5)
   504  	if err != nil {
   505  		t.Errorf("Problem while detecting root of %q %s", in5, err)
   506  	} else if string(pr) != in5 {
   507  		t.Errorf("Wrong project root was deduced;\n\t(GOT) %s\n\t(WNT) %s", pr, in)
   508  	}
   509  	if sm.deduceCoord.rootxt.Len() != 4 {
   510  		t.Errorf("Root path trie should have four elements, one for each unique root; has %v", sm.deduceCoord.rootxt.Len())
   511  	}
   512  }
   513  
   514  func TestMultiFetchThreadsafe(t *testing.T) {
   515  	// This test is quite slow, skip it on -short
   516  	if testing.Short() {
   517  		t.Skip("Skipping slow test in short mode")
   518  	}
   519  
   520  	projects := []ProjectIdentifier{
   521  		mkPI("github.com/sdboyer/gps"),
   522  		mkPI("github.com/sdboyer/gpkt"),
   523  		ProjectIdentifier{
   524  			ProjectRoot: ProjectRoot("github.com/sdboyer/gpkt"),
   525  			Source:      "https://github.com/sdboyer/gpkt",
   526  		},
   527  		mkPI("github.com/sdboyer/gogl"),
   528  		mkPI("github.com/sdboyer/gliph"),
   529  		mkPI("github.com/sdboyer/frozone"),
   530  		mkPI("gopkg.in/sdboyer/gpkt.v1"),
   531  		mkPI("gopkg.in/sdboyer/gpkt.v2"),
   532  		mkPI("github.com/Masterminds/VCSTestRepo"),
   533  		mkPI("github.com/go-yaml/yaml"),
   534  		mkPI("github.com/Sirupsen/logrus"),
   535  		mkPI("github.com/Masterminds/semver"),
   536  		mkPI("github.com/Masterminds/vcs"),
   537  		//mkPI("bitbucket.org/sdboyer/withbm"),
   538  		//mkPI("bitbucket.org/sdboyer/nobm"),
   539  	}
   540  
   541  	do := func(name string, sm *SourceMgr) {
   542  		t.Run(name, func(t *testing.T) {
   543  			// This gives us ten calls per op, per project, which should be(?)
   544  			// decently likely to reveal underlying concurrency problems
   545  			ops := 4
   546  			cnum := len(projects) * ops * 10
   547  
   548  			for i := 0; i < cnum; i++ {
   549  				// Trigger all four ops on each project, then move on to the next
   550  				// project.
   551  				id, op := projects[(i/ops)%len(projects)], i%ops
   552  				// The count of times this op has been been invoked on this project
   553  				// (after the upcoming invocation)
   554  				opcount := i/(ops*len(projects)) + 1
   555  
   556  				switch op {
   557  				case 0:
   558  					t.Run(fmt.Sprintf("deduce:%v:%s", opcount, id.errString()), func(t *testing.T) {
   559  						t.Parallel()
   560  						if _, err := sm.DeduceProjectRoot(string(id.ProjectRoot)); err != nil {
   561  							t.Error(err)
   562  						}
   563  					})
   564  				case 1:
   565  					t.Run(fmt.Sprintf("sync:%v:%s", opcount, id.errString()), func(t *testing.T) {
   566  						t.Parallel()
   567  						err := sm.SyncSourceFor(id)
   568  						if err != nil {
   569  							t.Error(err)
   570  						}
   571  					})
   572  				case 2:
   573  					t.Run(fmt.Sprintf("listVersions:%v:%s", opcount, id.errString()), func(t *testing.T) {
   574  						t.Parallel()
   575  						vl, err := sm.ListVersions(id)
   576  						if err != nil {
   577  							t.Fatal(err)
   578  						}
   579  						if len(vl) == 0 {
   580  							t.Error("no versions returned")
   581  						}
   582  					})
   583  				case 3:
   584  					t.Run(fmt.Sprintf("exists:%v:%s", opcount, id.errString()), func(t *testing.T) {
   585  						t.Parallel()
   586  						y, err := sm.SourceExists(id)
   587  						if err != nil {
   588  							t.Fatal(err)
   589  						}
   590  						if !y {
   591  							t.Error("said source does not exist")
   592  						}
   593  					})
   594  				default:
   595  					panic(fmt.Sprintf("wtf, %s %v", id, op))
   596  				}
   597  			}
   598  		})
   599  	}
   600  
   601  	sm, _ := mkNaiveSM(t)
   602  	do("first", sm)
   603  
   604  	// Run the thing twice with a remade sm so that we cover both the cases of
   605  	// pre-existing and new clones.
   606  	//
   607  	// This triggers a release of the first sm, which is much of what we're
   608  	// testing here - that the release is complete and clean, and can be
   609  	// immediately followed by a new sm coming in.
   610  	sm2, clean := remakeNaiveSM(sm, t)
   611  	do("second", sm2)
   612  	clean()
   613  }
   614  
   615  // Ensure that we don't see concurrent map writes when calling ListVersions.
   616  // Regression test for https://github.com/sdboyer/gps/issues/156.
   617  //
   618  // Ideally this would be caught by TestMultiFetchThreadsafe, but perhaps the
   619  // high degree of parallelism pretty much eliminates that as a realistic
   620  // possibility?
   621  func TestListVersionsRacey(t *testing.T) {
   622  	// This test is quite slow, skip it on -short
   623  	if testing.Short() {
   624  		t.Skip("Skipping slow test in short mode")
   625  	}
   626  
   627  	sm, clean := mkNaiveSM(t)
   628  	defer clean()
   629  
   630  	wg := &sync.WaitGroup{}
   631  	id := mkPI("github.com/sdboyer/gps")
   632  	for i := 0; i < 20; i++ {
   633  		wg.Add(1)
   634  		go func() {
   635  			_, err := sm.ListVersions(id)
   636  			if err != nil {
   637  				t.Errorf("listing versions failed with err %s", err.Error())
   638  			}
   639  			wg.Done()
   640  		}()
   641  	}
   642  
   643  	wg.Wait()
   644  }
   645  
   646  func TestErrAfterRelease(t *testing.T) {
   647  	sm, clean := mkNaiveSM(t)
   648  	clean()
   649  	id := ProjectIdentifier{}
   650  
   651  	_, err := sm.SourceExists(id)
   652  	if err == nil {
   653  		t.Errorf("SourceExists did not error after calling Release()")
   654  	} else if terr, ok := err.(smIsReleased); !ok {
   655  		t.Errorf("SourceExists errored after Release(), but with unexpected error: %T %s", terr, terr.Error())
   656  	}
   657  
   658  	err = sm.SyncSourceFor(id)
   659  	if err == nil {
   660  		t.Errorf("SyncSourceFor did not error after calling Release()")
   661  	} else if terr, ok := err.(smIsReleased); !ok {
   662  		t.Errorf("SyncSourceFor errored after Release(), but with unexpected error: %T %s", terr, terr.Error())
   663  	}
   664  
   665  	_, err = sm.ListVersions(id)
   666  	if err == nil {
   667  		t.Errorf("ListVersions did not error after calling Release()")
   668  	} else if terr, ok := err.(smIsReleased); !ok {
   669  		t.Errorf("ListVersions errored after Release(), but with unexpected error: %T %s", terr, terr.Error())
   670  	}
   671  
   672  	_, err = sm.RevisionPresentIn(id, "")
   673  	if err == nil {
   674  		t.Errorf("RevisionPresentIn did not error after calling Release()")
   675  	} else if terr, ok := err.(smIsReleased); !ok {
   676  		t.Errorf("RevisionPresentIn errored after Release(), but with unexpected error: %T %s", terr, terr.Error())
   677  	}
   678  
   679  	_, err = sm.ListPackages(id, nil)
   680  	if err == nil {
   681  		t.Errorf("ListPackages did not error after calling Release()")
   682  	} else if terr, ok := err.(smIsReleased); !ok {
   683  		t.Errorf("ListPackages errored after Release(), but with unexpected error: %T %s", terr, terr.Error())
   684  	}
   685  
   686  	_, _, err = sm.GetManifestAndLock(id, nil, naiveAnalyzer{})
   687  	if err == nil {
   688  		t.Errorf("GetManifestAndLock did not error after calling Release()")
   689  	} else if terr, ok := err.(smIsReleased); !ok {
   690  		t.Errorf("GetManifestAndLock errored after Release(), but with unexpected error: %T %s", terr, terr.Error())
   691  	}
   692  
   693  	err = sm.ExportProject(id, nil, "")
   694  	if err == nil {
   695  		t.Errorf("ExportProject did not error after calling Release()")
   696  	} else if terr, ok := err.(smIsReleased); !ok {
   697  		t.Errorf("ExportProject errored after Release(), but with unexpected error: %T %s", terr, terr.Error())
   698  	}
   699  
   700  	_, err = sm.DeduceProjectRoot("")
   701  	if err == nil {
   702  		t.Errorf("DeduceProjectRoot did not error after calling Release()")
   703  	} else if terr, ok := err.(smIsReleased); !ok {
   704  		t.Errorf("DeduceProjectRoot errored after Release(), but with unexpected error: %T %s", terr, terr.Error())
   705  	}
   706  }
   707  
   708  func TestSignalHandling(t *testing.T) {
   709  	if testing.Short() {
   710  		t.Skip("Skipping slow test in short mode")
   711  	}
   712  
   713  	sm, clean := mkNaiveSM(t)
   714  
   715  	sigch := make(chan os.Signal)
   716  	sm.HandleSignals(sigch)
   717  
   718  	sigch <- os.Interrupt
   719  	<-time.After(10 * time.Millisecond)
   720  
   721  	if atomic.LoadInt32(&sm.releasing) != 1 {
   722  		t.Error("Releasing flag did not get set")
   723  	}
   724  
   725  	lpath := filepath.Join(sm.cachedir, "sm.lock")
   726  	if _, err := os.Stat(lpath); err == nil {
   727  		t.Fatal("Expected error on statting what should be an absent lock file")
   728  	}
   729  	clean()
   730  
   731  	// Test again, this time with a running call
   732  	sm, clean = mkNaiveSM(t)
   733  	sm.HandleSignals(sigch)
   734  
   735  	errchan := make(chan error)
   736  	go func() {
   737  		_, callerr := sm.DeduceProjectRoot("k8s.io/kubernetes")
   738  		errchan <- callerr
   739  	}()
   740  	go func() { sigch <- os.Interrupt }()
   741  	runtime.Gosched()
   742  
   743  	callerr := <-errchan
   744  	if callerr == nil {
   745  		t.Error("network call could not have completed before cancellation, should have gotten an error")
   746  	}
   747  	if atomic.LoadInt32(&sm.releasing) != 1 {
   748  		t.Error("Releasing flag did not get set")
   749  	}
   750  	clean()
   751  
   752  	sm, clean = mkNaiveSM(t)
   753  	// Ensure that handling also works after stopping and restarting itself,
   754  	// and that Release happens only once.
   755  	sm.UseDefaultSignalHandling()
   756  	sm.StopSignalHandling()
   757  	sm.HandleSignals(sigch)
   758  
   759  	go func() {
   760  		_, callerr := sm.DeduceProjectRoot("k8s.io/kubernetes")
   761  		errchan <- callerr
   762  	}()
   763  	go func() {
   764  		sigch <- os.Interrupt
   765  		sm.Release()
   766  	}()
   767  	runtime.Gosched()
   768  
   769  	after := time.After(2 * time.Second)
   770  	select {
   771  	case <-sm.qch:
   772  	case <-after:
   773  		t.Error("did not shut down in reasonable time")
   774  	}
   775  
   776  	clean()
   777  }
   778  
   779  func TestUnreachableSource(t *testing.T) {
   780  	// If a git remote is unreachable (maybe the server is only accessible behind a VPN, or
   781  	// something), we should return a clear error, not a panic.
   782  	if testing.Short() {
   783  		t.Skip("Skipping slow test in short mode")
   784  	}
   785  
   786  	sm, clean := mkNaiveSM(t)
   787  	defer clean()
   788  
   789  	id := mkPI("github.com/golang/notexist").normalize()
   790  	err := sm.SyncSourceFor(id)
   791  	if err == nil {
   792  		t.Error("expected err when listing versions of a bogus source, but got nil")
   793  	}
   794  }
   795  
   796  func TestSupervisor(t *testing.T) {
   797  	bgc := context.Background()
   798  	ctx, cancelFunc := context.WithCancel(bgc)
   799  	superv := newSupervisor(ctx)
   800  
   801  	ci := callInfo{
   802  		name: "foo",
   803  		typ:  0,
   804  	}
   805  
   806  	_, err := superv.start(ci)
   807  	if err != nil {
   808  		t.Fatal("unexpected err on setUpCall:", err)
   809  	}
   810  
   811  	tc, exists := superv.running[ci]
   812  	if !exists {
   813  		t.Fatal("running call not recorded in map")
   814  	}
   815  
   816  	if tc.count != 1 {
   817  		t.Fatalf("wrong count of running ci: wanted 1 got %v", tc.count)
   818  	}
   819  
   820  	// run another, but via do
   821  	block, wait := make(chan struct{}), make(chan struct{})
   822  	go func() {
   823  		wait <- struct{}{}
   824  		err := superv.do(bgc, "foo", 0, func(ctx context.Context) error {
   825  			<-block
   826  			return nil
   827  		})
   828  		if err != nil {
   829  			t.Fatal("unexpected err on do() completion:", err)
   830  		}
   831  		close(wait)
   832  	}()
   833  	<-wait
   834  
   835  	superv.mu.Lock()
   836  	tc, exists = superv.running[ci]
   837  	if !exists {
   838  		t.Fatal("running call not recorded in map")
   839  	}
   840  
   841  	if tc.count != 2 {
   842  		t.Fatalf("wrong count of running ci: wanted 2 got %v", tc.count)
   843  	}
   844  	superv.mu.Unlock()
   845  
   846  	close(block)
   847  	<-wait
   848  	superv.mu.Lock()
   849  	if len(superv.ran) != 0 {
   850  		t.Fatal("should not record metrics until last one drops")
   851  	}
   852  
   853  	tc, exists = superv.running[ci]
   854  	if !exists {
   855  		t.Fatal("running call not recorded in map")
   856  	}
   857  
   858  	if tc.count != 1 {
   859  		t.Fatalf("wrong count of running ci: wanted 1 got %v", tc.count)
   860  	}
   861  	superv.mu.Unlock()
   862  
   863  	superv.done(ci)
   864  	superv.mu.Lock()
   865  	ran, exists := superv.ran[0]
   866  	if !exists {
   867  		t.Fatal("should have metrics after closing last of a ci, but did not")
   868  	}
   869  
   870  	if ran.count != 1 {
   871  		t.Fatalf("wrong count of serial runs of a call: wanted 1 got %v", ran.count)
   872  	}
   873  	superv.mu.Unlock()
   874  
   875  	cancelFunc()
   876  	_, err = superv.start(ci)
   877  	if err == nil {
   878  		t.Fatal("should have errored on cm.run() after canceling cm's input context")
   879  	}
   880  
   881  	superv.do(bgc, "foo", 0, func(ctx context.Context) error {
   882  		t.Fatal("calls should not be initiated by do() after main context is cancelled")
   883  		return nil
   884  	})
   885  }