gps/manager_test.go
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gps
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"runtime"
"sort"
"sync"
"sync/atomic"
"testing"
"text/tabwriter"
"time"
"github.com/golang/dep/internal/test"
)
// An analyzer that passes nothing back, but doesn't error. This is the naive
// case - no constraints, no lock, and no errors. The SourceManager will
// interpret this as open/Any constraints on everything in the import graph.
type naiveAnalyzer struct{}
func (naiveAnalyzer) DeriveManifestAndLock(string, ProjectRoot) (Manifest, Lock, error) {
return nil, nil, nil
}
func (a naiveAnalyzer) Info() ProjectAnalyzerInfo {
return ProjectAnalyzerInfo{
Name: "naive-analyzer",
Version: 1,
}
}
func mkNaiveSM(t *testing.T) (*SourceMgr, func()) {
cpath, err := ioutil.TempDir("", "smcache")
if err != nil {
t.Fatalf("Failed to create temp dir: %s", err)
}
sm, err := NewSourceManager(SourceManagerConfig{
Cachedir: cpath,
Logger: log.New(test.Writer{TB: t}, "", 0),
})
if err != nil {
t.Fatalf("Unexpected error on SourceManager creation: %s", err)
}
return sm, func() {
sm.Release()
err := os.RemoveAll(cpath)
if err != nil {
t.Errorf("removeAll failed: %s", err)
}
}
}
func remakeNaiveSM(osm *SourceMgr, t *testing.T) (*SourceMgr, func()) {
cpath := osm.cachedir
osm.Release()
sm, err := NewSourceManager(SourceManagerConfig{
Cachedir: cpath,
Logger: log.New(test.Writer{TB: t}, "", 0),
})
if err != nil {
t.Fatalf("unexpected error on SourceManager recreation: %s", err)
}
return sm, func() {
sm.Release()
err := os.RemoveAll(cpath)
if err != nil {
t.Errorf("removeAll failed: %s", err)
}
}
}
func TestSourceManagerInit(t *testing.T) {
cpath, err := ioutil.TempDir("", "smcache")
if err != nil {
t.Errorf("Failed to create temp dir: %s", err)
}
cfg := SourceManagerConfig{
Cachedir: cpath,
Logger: log.New(test.Writer{TB: t}, "", 0),
}
sm, err := NewSourceManager(cfg)
if err != nil {
t.Errorf("Unexpected error on SourceManager creation: %s", err)
}
_, err = NewSourceManager(cfg)
if err == nil {
t.Errorf("Creating second SourceManager should have failed due to file lock contention")
} else if te, ok := err.(CouldNotCreateLockError); !ok {
t.Errorf("Should have gotten CouldNotCreateLockError error type, but got %T", te)
}
if _, err = os.Stat(path.Join(cpath, "sm.lock")); err != nil {
t.Errorf("Global cache lock file not created correctly")
}
sm.Release()
err = os.RemoveAll(cpath)
if err != nil {
t.Errorf("removeAll failed: %s", err)
}
if _, err = os.Stat(path.Join(cpath, "sm.lock")); !os.IsNotExist(err) {
t.Fatalf("Global cache lock file not cleared correctly on Release()")
}
err = os.MkdirAll(cpath, 0777)
if err != nil {
t.Errorf("Failed to re-create temp dir: %s", err)
}
defer func() {
err = os.RemoveAll(cpath)
if err != nil {
t.Errorf("removeAll failed: %s", err)
}
}()
// Set another one up at the same spot now, just to be sure
sm, err = NewSourceManager(cfg)
if err != nil {
t.Fatalf("Creating a second SourceManager should have succeeded when the first was released, but failed with err %s", err)
}
sm.Release()
}
func TestSourceInit(t *testing.T) {
// This test is a bit slow, skip it on -short
if testing.Short() {
t.Skip("Skipping project manager init test in short mode")
}
cpath, err := ioutil.TempDir("", "smcache")
if err != nil {
t.Fatalf("Failed to create temp dir: %s", err)
}
sm, err := NewSourceManager(SourceManagerConfig{
Cachedir: cpath,
Logger: log.New(test.Writer{TB: t}, "", 0),
})
if err != nil {
t.Fatalf("Unexpected error on SourceManager creation: %s", err)
}
defer func() {
sm.Release()
err := os.RemoveAll(cpath)
if err != nil {
t.Errorf("removeAll failed: %s", err)
}
}()
id := mkPI("github.com/sdboyer/gpkt").normalize()
pvl, err := sm.ListVersions(id)
if err != nil {
t.Errorf("Unexpected error during initial project setup/fetching %s", err)
}
if len(pvl) != 7 {
t.Errorf("Expected seven version results from the test repo, got %v", len(pvl))
} else {
expected := []PairedVersion{
NewVersion("v2.0.0").Pair(Revision("4a54adf81c75375d26d376459c00d5ff9b703e5e")),
NewVersion("v1.1.0").Pair(Revision("b2cb48dda625f6640b34d9ffb664533359ac8b91")),
NewVersion("v1.0.0").Pair(Revision("bf85021c0405edbc4f3648b0603818d641674f72")),
newDefaultBranch("master").Pair(Revision("bf85021c0405edbc4f3648b0603818d641674f72")),
NewBranch("v1").Pair(Revision("e3777f683305eafca223aefe56b4e8ecf103f467")),
NewBranch("v1.1").Pair(Revision("f1fbc520489a98306eb28c235204e39fa8a89c84")),
NewBranch("v3").Pair(Revision("4a54adf81c75375d26d376459c00d5ff9b703e5e")),
}
// SourceManager itself doesn't guarantee ordering; sort them here so we
// can dependably check output
SortPairedForUpgrade(pvl)
for k, e := range expected {
if !pvl[k].Matches(e) {
t.Errorf("Expected version %s in position %v but got %s", e, k, pvl[k])
}
}
}
// Two birds, one stone - make sure the internal ProjectManager vlist cache
// works (or at least doesn't not work) by asking for the versions again,
// and do it through smcache to ensure its sorting works, as well.
smc := &bridge{
sm: sm,
vlists: make(map[ProjectIdentifier][]Version),
s: &solver{mtr: newMetrics()},
}
vl, err := smc.listVersions(id)
if err != nil {
t.Errorf("Unexpected error during initial project setup/fetching %s", err)
}
if len(vl) != 7 {
t.Errorf("Expected seven version results from the test repo, got %v", len(vl))
} else {
expected := []Version{
NewVersion("v2.0.0").Pair(Revision("4a54adf81c75375d26d376459c00d5ff9b703e5e")),
NewVersion("v1.1.0").Pair(Revision("b2cb48dda625f6640b34d9ffb664533359ac8b91")),
NewVersion("v1.0.0").Pair(Revision("bf85021c0405edbc4f3648b0603818d641674f72")),
newDefaultBranch("master").Pair(Revision("bf85021c0405edbc4f3648b0603818d641674f72")),
NewBranch("v1").Pair(Revision("e3777f683305eafca223aefe56b4e8ecf103f467")),
NewBranch("v1.1").Pair(Revision("f1fbc520489a98306eb28c235204e39fa8a89c84")),
NewBranch("v3").Pair(Revision("4a54adf81c75375d26d376459c00d5ff9b703e5e")),
}
for k, e := range expected {
if !vl[k].Matches(e) {
t.Errorf("Expected version %s in position %v but got %s", e, k, vl[k])
}
}
if !vl[3].(versionPair).v.(branchVersion).isDefault {
t.Error("Expected master branch version to have isDefault flag, but it did not")
}
if vl[4].(versionPair).v.(branchVersion).isDefault {
t.Error("Expected v1 branch version not to have isDefault flag, but it did")
}
if vl[5].(versionPair).v.(branchVersion).isDefault {
t.Error("Expected v1.1 branch version not to have isDefault flag, but it did")
}
if vl[6].(versionPair).v.(branchVersion).isDefault {
t.Error("Expected v3 branch version not to have isDefault flag, but it did")
}
}
present, err := smc.RevisionPresentIn(id, Revision("4a54adf81c75375d26d376459c00d5ff9b703e5e"))
if err != nil {
t.Errorf("Should have found revision in source, but got err: %s", err)
} else if !present {
t.Errorf("Should have found revision in source, but did not")
}
// SyncSourceFor will ensure we have everything
err = smc.SyncSourceFor(id)
if err != nil {
t.Errorf("SyncSourceFor failed with unexpected error: %s", err)
}
// Ensure that the appropriate cache dirs and files exist
_, err = os.Stat(filepath.Join(cpath, "sources", "https---github.com-sdboyer-gpkt", ".git"))
if err != nil {
t.Error("Cache repo does not exist in expected location")
}
os.Stat(filepath.Join(cpath, "metadata", "github.com", "sdboyer", "gpkt", "cache.json"))
// Ensure source existence values are what we expect
var exists bool
exists, err = sm.SourceExists(id)
if err != nil {
t.Errorf("Error on checking SourceExists: %s", err)
}
if !exists {
t.Error("Source should exist after non-erroring call to ListVersions")
}
}
func TestDefaultBranchAssignment(t *testing.T) {
if testing.Short() {
t.Skip("Skipping default branch assignment test in short mode")
}
sm, clean := mkNaiveSM(t)
defer clean()
id := mkPI("github.com/sdboyer/test-multibranch")
v, err := sm.ListVersions(id)
if err != nil {
t.Errorf("Unexpected error during initial project setup/fetching %s", err)
}
if len(v) != 3 {
t.Errorf("Expected three version results from the test repo, got %v", len(v))
} else {
brev := Revision("fda020843ac81352004b9dca3fcccdd517600149")
mrev := Revision("9f9c3a591773d9b28128309ac7a9a72abcab267d")
expected := []PairedVersion{
NewBranch("branchone").Pair(brev),
NewBranch("otherbranch").Pair(brev),
NewBranch("master").Pair(mrev),
}
SortPairedForUpgrade(v)
for k, e := range expected {
if !v[k].Matches(e) {
t.Errorf("Expected version %s in position %v but got %s", e, k, v[k])
}
}
if !v[0].(versionPair).v.(branchVersion).isDefault {
t.Error("Expected branchone branch version to have isDefault flag, but it did not")
}
if !v[0].(versionPair).v.(branchVersion).isDefault {
t.Error("Expected otherbranch branch version to have isDefault flag, but it did not")
}
if v[2].(versionPair).v.(branchVersion).isDefault {
t.Error("Expected master branch version not to have isDefault flag, but it did")
}
}
}
func TestMgrMethodsFailWithBadPath(t *testing.T) {
// a symbol will always bork it up
bad := mkPI("foo/##&^").normalize()
sm, clean := mkNaiveSM(t)
defer clean()
var err error
if _, err = sm.SourceExists(bad); err == nil {
t.Error("SourceExists() did not error on bad input")
}
if err = sm.SyncSourceFor(bad); err == nil {
t.Error("SyncSourceFor() did not error on bad input")
}
if _, err = sm.ListVersions(bad); err == nil {
t.Error("ListVersions() did not error on bad input")
}
if _, err = sm.RevisionPresentIn(bad, Revision("")); err == nil {
t.Error("RevisionPresentIn() did not error on bad input")
}
if _, err = sm.ListPackages(bad, nil); err == nil {
t.Error("ListPackages() did not error on bad input")
}
if _, _, err = sm.GetManifestAndLock(bad, nil, naiveAnalyzer{}); err == nil {
t.Error("GetManifestAndLock() did not error on bad input")
}
if err = sm.ExportProject(context.Background(), bad, nil, ""); err == nil {
t.Error("ExportProject() did not error on bad input")
}
}
type sourceCreationTestFixture struct {
roots []ProjectIdentifier
namecount, srccount int
}
func (f sourceCreationTestFixture) run(t *testing.T) {
t.Parallel()
sm, clean := mkNaiveSM(t)
defer clean()
for _, pi := range f.roots {
_, err := sm.SourceExists(pi)
if err != nil {
t.Fatal(err)
}
}
if len(sm.srcCoord.nameToURL) != f.namecount {
t.Errorf("want %v names in the name->url map, but got %v. contents: \n%v", f.namecount, len(sm.srcCoord.nameToURL), sm.srcCoord.nameToURL)
}
if len(sm.srcCoord.srcs) != f.srccount {
t.Errorf("want %v gateways in the sources map, but got %v", f.srccount, len(sm.srcCoord.srcs))
}
if t.Failed() {
var keys []string
for k := range sm.srcCoord.nameToURL {
keys = append(keys, k)
}
sort.Strings(keys)
var buf bytes.Buffer
w := tabwriter.NewWriter(&buf, 0, 4, 2, ' ', 0)
fmt.Fprint(w, "NAME\tMAPPED URL\n")
for _, r := range keys {
fmt.Fprintf(w, "%s\t%s\n", r, sm.srcCoord.nameToURL[r])
}
w.Flush()
t.Log("\n", buf.String())
t.Log("SRC KEYS")
for k := range sm.srcCoord.srcs {
t.Log(k)
}
}
}
// This test is primarily about making sure that the logic around folding
// together different ways of referencing the same underlying resource - whether
// that be intentionally folding them, or intentionally keeping them separate -
// work as intended.
func TestSourceCreationCounts(t *testing.T) {
if testing.Short() {
t.Skip("Skipping slow test in short mode")
}
fixtures := map[string]sourceCreationTestFixture{
"gopkgin uniqueness": {
roots: []ProjectIdentifier{
mkPI("gopkg.in/sdboyer/gpkt.v1"),
mkPI("gopkg.in/sdboyer/gpkt.v2"),
mkPI("gopkg.in/sdboyer/gpkt.v3"),
},
namecount: 6,
srccount: 3,
},
"gopkgin separation from github": {
roots: []ProjectIdentifier{
mkPI("gopkg.in/sdboyer/gpkt.v1"),
mkPI("github.com/sdboyer/gpkt"),
mkPI("http://github.com/sdboyer/gpkt"),
mkPI("https://github.com/sdboyer/gpkt"),
},
namecount: 5,
srccount: 3,
},
"case variance across path and URL-based access": {
roots: []ProjectIdentifier{
{ProjectRoot: ProjectRoot("github.com/sdboyer/gpkt"), Source: "https://github.com/Sdboyer/gpkt"},
{ProjectRoot: ProjectRoot("github.com/sdboyer/gpkt"), Source: "https://github.com/SdbOyer/gpkt"},
mkPI("github.com/sdboyer/gpkt"),
{ProjectRoot: ProjectRoot("github.com/sdboyer/gpkt"), Source: "https://github.com/sdboyeR/gpkt"},
mkPI("github.com/sdboyeR/gpkt"),
},
namecount: 6,
srccount: 1,
},
}
for name, fix := range fixtures {
t.Run(name, fix.run)
}
}
func TestGetSources(t *testing.T) {
// This test is a tad slow, skip it on -short
if testing.Short() {
t.Skip("Skipping source setup test in short mode")
}
requiresBins(t, "git", "hg", "bzr")
sm, clean := mkNaiveSM(t)
pil := []ProjectIdentifier{
mkPI("github.com/Masterminds/VCSTestRepo").normalize(),
mkPI("bitbucket.org/mattfarina/testhgrepo").normalize(),
mkPI("launchpad.net/govcstestbzrrepo").normalize(),
}
ctx := context.Background()
// protects against premature release of sm
t.Run("inner", func(t *testing.T) {
for _, pi := range pil {
lpi := pi
t.Run(lpi.normalizedSource(), func(t *testing.T) {
t.Parallel()
srcg, err := sm.srcCoord.getSourceGatewayFor(ctx, lpi)
if err != nil {
t.Errorf("unexpected error setting up source: %s", err)
return
}
// Re-get the same, make sure they are the same
srcg2, err := sm.srcCoord.getSourceGatewayFor(ctx, lpi)
if err != nil {
t.Errorf("unexpected error re-getting source: %s", err)
} else if srcg != srcg2 {
t.Error("first and second sources are not eq")
}
// All of them _should_ select https, so this should work
lpi.Source = "https://" + lpi.Source
srcg3, err := sm.srcCoord.getSourceGatewayFor(ctx, lpi)
if err != nil {
t.Errorf("unexpected error getting explicit https source: %s", err)
} else if srcg != srcg3 {
t.Error("explicit https source should reuse autodetected https source")
}
// Now put in http, and they should differ
lpi.Source = "http://" + string(lpi.ProjectRoot)
srcg4, err := sm.srcCoord.getSourceGatewayFor(ctx, lpi)
if err != nil {
t.Errorf("unexpected error getting explicit http source: %s", err)
} else if srcg == srcg4 {
t.Error("explicit http source should create a new src")
}
})
}
})
// nine entries (of which three are dupes): for each vcs, raw import path,
// the https url, and the http url. also three more from case folding of
// github.com/Masterminds/VCSTestRepo -> github.com/masterminds/vcstestrepo
if len(sm.srcCoord.nameToURL) != 12 {
t.Errorf("Should have twelve discrete entries in the nameToURL map, got %v", len(sm.srcCoord.nameToURL))
}
clean()
}
func TestFSCaseSensitivityConvergesSources(t *testing.T) {
if testing.Short() {
t.Skip("Skipping slow test in short mode")
}
f := func(name string, pi1, pi2 ProjectIdentifier) {
t.Run(name, func(t *testing.T) {
t.Parallel()
sm, clean := mkNaiveSM(t)
defer clean()
sm.SyncSourceFor(pi1)
sg1, err := sm.srcCoord.getSourceGatewayFor(context.Background(), pi1)
if err != nil {
t.Fatal(err)
}
sm.SyncSourceFor(pi2)
sg2, err := sm.srcCoord.getSourceGatewayFor(context.Background(), pi2)
if err != nil {
t.Fatal(err)
}
path1 := sg1.src.(*gitSource).repo.LocalPath()
stat1, err := os.Stat(path1)
if err != nil {
t.Fatal("path1:", path1, err)
}
path2 := sg2.src.(*gitSource).repo.LocalPath()
stat2, err := os.Stat(path2)
if err != nil {
t.Fatal("path2:", path2, err)
}
same, count := os.SameFile(stat1, stat2), len(sm.srcCoord.srcs)
if same && count != 1 {
t.Log("are same, count", count)
t.Fatal("on case-insensitive filesystem, case-varying sources should have been folded together but were not")
}
if !same && count != 2 {
t.Log("not same, count", count)
t.Fatal("on case-sensitive filesystem, case-varying sources should not have been folded together, but were")
}
})
}
folded := mkPI("github.com/sdboyer/deptest").normalize()
casevar1 := mkPI("github.com/Sdboyer/deptest").normalize()
casevar2 := mkPI("github.com/SdboyeR/deptest").normalize()
f("folded first", folded, casevar1)
f("folded second", casevar1, folded)
f("both unfolded", casevar1, casevar2)
}
// Regression test for #32
func TestGetInfoListVersionsOrdering(t *testing.T) {
// This test is quite slow, skip it on -short
if testing.Short() {
t.Skip("Skipping slow test in short mode")
}
sm, clean := mkNaiveSM(t)
defer clean()
// setup done, now do the test
id := mkPI("github.com/sdboyer/gpkt").normalize()
_, _, err := sm.GetManifestAndLock(id, NewVersion("v1.0.0"), naiveAnalyzer{})
if err != nil {
t.Errorf("Unexpected error from GetInfoAt %s", err)
}
v, err := sm.ListVersions(id)
if err != nil {
t.Errorf("Unexpected error from ListVersions %s", err)
}
if len(v) != 7 {
t.Errorf("Expected seven results from ListVersions, got %v", len(v))
}
}
func TestDeduceProjectRoot(t *testing.T) {
sm, clean := mkNaiveSM(t)
defer clean()
in := "github.com/sdboyer/gps"
pr, err := sm.DeduceProjectRoot(in)
if err != nil {
t.Errorf("Problem while detecting root of %q %s", in, err)
}
if string(pr) != in {
t.Errorf("Wrong project root was deduced;\n\t(GOT) %s\n\t(WNT) %s", pr, in)
}
if sm.deduceCoord.rootxt.Len() != 1 {
t.Errorf("Root path trie should have one element after one deduction, has %v", sm.deduceCoord.rootxt.Len())
}
pr, err = sm.DeduceProjectRoot(in)
if err != nil {
t.Errorf("Problem while detecting root of %q %s", in, err)
} else if string(pr) != in {
t.Errorf("Wrong project root was deduced;\n\t(GOT) %s\n\t(WNT) %s", pr, in)
}
if sm.deduceCoord.rootxt.Len() != 1 {
t.Errorf("Root path trie should still have one element after performing the same deduction twice; has %v", sm.deduceCoord.rootxt.Len())
}
// Now do a subpath
sub := path.Join(in, "foo")
pr, err = sm.DeduceProjectRoot(sub)
if err != nil {
t.Errorf("Problem while detecting root of %q %s", sub, err)
} else if string(pr) != in {
t.Errorf("Wrong project root was deduced;\n\t(GOT) %s\n\t(WNT) %s", pr, in)
}
if sm.deduceCoord.rootxt.Len() != 1 {
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())
}
// Now do a fully different root, but still on github
in2 := "github.com/bagel/lox"
sub2 := path.Join(in2, "cheese")
pr, err = sm.DeduceProjectRoot(sub2)
if err != nil {
t.Errorf("Problem while detecting root of %q %s", sub2, err)
} else if string(pr) != in2 {
t.Errorf("Wrong project root was deduced;\n\t(GOT) %s\n\t(WNT) %s", pr, in)
}
if sm.deduceCoord.rootxt.Len() != 2 {
t.Errorf("Root path trie should have two elements, one for each unique root; has %v", sm.deduceCoord.rootxt.Len())
}
// Ensure that our prefixes are bounded by path separators
in4 := "github.com/bagel/loxx"
pr, err = sm.DeduceProjectRoot(in4)
if err != nil {
t.Errorf("Problem while detecting root of %q %s", in4, err)
} else if string(pr) != in4 {
t.Errorf("Wrong project root was deduced;\n\t(GOT) %s\n\t(WNT) %s", pr, in)
}
if sm.deduceCoord.rootxt.Len() != 3 {
t.Errorf("Root path trie should have three elements, one for each unique root; has %v", sm.deduceCoord.rootxt.Len())
}
// Ensure that vcs extension-based matching comes through
in5 := "ffffrrrraaaaaapppppdoesnotresolve.com/baz.git"
pr, err = sm.DeduceProjectRoot(in5)
if err != nil {
t.Errorf("Problem while detecting root of %q %s", in5, err)
} else if string(pr) != in5 {
t.Errorf("Wrong project root was deduced;\n\t(GOT) %s\n\t(WNT) %s", pr, in)
}
if sm.deduceCoord.rootxt.Len() != 4 {
t.Errorf("Root path trie should have four elements, one for each unique root; has %v", sm.deduceCoord.rootxt.Len())
}
}
func TestMultiFetchThreadsafe(t *testing.T) {
// This test is quite slow, skip it on -short
if testing.Short() {
t.Skip("Skipping slow test in short mode")
}
projects := []ProjectIdentifier{
mkPI("github.com/sdboyer/gps"),
mkPI("github.com/sdboyer/gpkt"),
{
ProjectRoot: ProjectRoot("github.com/sdboyer/gpkt"),
Source: "https://github.com/sdboyer/gpkt",
},
mkPI("github.com/sdboyer/gogl"),
mkPI("github.com/sdboyer/gliph"),
mkPI("github.com/sdboyer/frozone"),
mkPI("gopkg.in/sdboyer/gpkt.v1"),
mkPI("gopkg.in/sdboyer/gpkt.v2"),
mkPI("github.com/Masterminds/VCSTestRepo"),
mkPI("github.com/go-yaml/yaml"),
mkPI("github.com/sirupsen/logrus"),
mkPI("github.com/Masterminds/semver"),
mkPI("github.com/Masterminds/vcs"),
//mkPI("bitbucket.org/sdboyer/withbm"),
//mkPI("bitbucket.org/sdboyer/nobm"),
}
do := func(name string, sm SourceManager) {
t.Run(name, func(t *testing.T) {
// This gives us ten calls per op, per project, which should be(?)
// decently likely to reveal underlying concurrency problems
ops := 4
cnum := len(projects) * ops * 10
for i := 0; i < cnum; i++ {
// Trigger all four ops on each project, then move on to the next
// project.
id, op := projects[(i/ops)%len(projects)], i%ops
// The count of times this op has been been invoked on this project
// (after the upcoming invocation)
opcount := i/(ops*len(projects)) + 1
switch op {
case 0:
t.Run(fmt.Sprintf("deduce:%v:%s", opcount, id), func(t *testing.T) {
t.Parallel()
if _, err := sm.DeduceProjectRoot(string(id.ProjectRoot)); err != nil {
t.Error(err)
}
})
case 1:
t.Run(fmt.Sprintf("sync:%v:%s", opcount, id), func(t *testing.T) {
t.Parallel()
err := sm.SyncSourceFor(id)
if err != nil {
t.Error(err)
}
})
case 2:
t.Run(fmt.Sprintf("listVersions:%v:%s", opcount, id), func(t *testing.T) {
t.Parallel()
vl, err := sm.ListVersions(id)
if err != nil {
t.Fatal(err)
}
if len(vl) == 0 {
t.Error("no versions returned")
}
})
case 3:
t.Run(fmt.Sprintf("exists:%v:%s", opcount, id), func(t *testing.T) {
t.Parallel()
y, err := sm.SourceExists(id)
if err != nil {
t.Fatal(err)
}
if !y {
t.Error("said source does not exist")
}
})
default:
panic(fmt.Sprintf("wtf, %s %v", id, op))
}
}
})
}
sm, _ := mkNaiveSM(t)
do("first", sm)
// Run the thing twice with a remade sm so that we cover both the cases of
// pre-existing and new clones.
//
// This triggers a release of the first sm, which is much of what we're
// testing here - that the release is complete and clean, and can be
// immediately followed by a new sm coming in.
sm2, clean := remakeNaiveSM(sm, t)
do("second", sm2)
clean()
}
// Ensure that we don't see concurrent map writes when calling ListVersions.
// Regression test for https://github.com/sdboyer/gps/issues/156.
//
// Ideally this would be caught by TestMultiFetchThreadsafe, but perhaps the
// high degree of parallelism pretty much eliminates that as a realistic
// possibility?
func TestListVersionsRacey(t *testing.T) {
// This test is quite slow, skip it on -short
if testing.Short() {
t.Skip("Skipping slow test in short mode")
}
sm, clean := mkNaiveSM(t)
defer clean()
wg := &sync.WaitGroup{}
id := mkPI("github.com/sdboyer/gps")
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
_, err := sm.ListVersions(id)
if err != nil {
t.Errorf("listing versions failed with err %s", err.Error())
}
wg.Done()
}()
}
wg.Wait()
}
func TestErrAfterRelease(t *testing.T) {
sm, clean := mkNaiveSM(t)
clean()
id := ProjectIdentifier{}
_, err := sm.SourceExists(id)
if err == nil {
t.Errorf("SourceExists did not error after calling Release()")
} else if err != ErrSourceManagerIsReleased {
t.Errorf("SourceExists errored after Release(), but with unexpected error: %T %s", err, err.Error())
}
err = sm.SyncSourceFor(id)
if err == nil {
t.Errorf("SyncSourceFor did not error after calling Release()")
} else if err != ErrSourceManagerIsReleased {
t.Errorf("SyncSourceFor errored after Release(), but with unexpected error: %T %s", err, err.Error())
}
_, err = sm.ListVersions(id)
if err == nil {
t.Errorf("ListVersions did not error after calling Release()")
} else if err != ErrSourceManagerIsReleased {
t.Errorf("ListVersions errored after Release(), but with unexpected error: %T %s", err, err.Error())
}
_, err = sm.RevisionPresentIn(id, "")
if err == nil {
t.Errorf("RevisionPresentIn did not error after calling Release()")
} else if err != ErrSourceManagerIsReleased {
t.Errorf("RevisionPresentIn errored after Release(), but with unexpected error: %T %s", err, err.Error())
}
_, err = sm.ListPackages(id, nil)
if err == nil {
t.Errorf("ListPackages did not error after calling Release()")
} else if err != ErrSourceManagerIsReleased {
t.Errorf("ListPackages errored after Release(), but with unexpected error: %T %s", err, err.Error())
}
_, _, err = sm.GetManifestAndLock(id, nil, naiveAnalyzer{})
if err == nil {
t.Errorf("GetManifestAndLock did not error after calling Release()")
} else if err != ErrSourceManagerIsReleased {
t.Errorf("GetManifestAndLock errored after Release(), but with unexpected error: %T %s", err, err.Error())
}
err = sm.ExportProject(context.Background(), id, nil, "")
if err == nil {
t.Errorf("ExportProject did not error after calling Release()")
} else if err != ErrSourceManagerIsReleased {
t.Errorf("ExportProject errored after Release(), but with unexpected error: %T %s", err, err.Error())
}
_, err = sm.DeduceProjectRoot("")
if err == nil {
t.Errorf("DeduceProjectRoot did not error after calling Release()")
} else if err != ErrSourceManagerIsReleased {
t.Errorf("DeduceProjectRoot errored after Release(), but with unexpected error: %T %s", err, err.Error())
}
}
func TestSignalHandling(t *testing.T) {
if testing.Short() {
t.Skip("Skipping slow test in short mode")
}
sm, clean := mkNaiveSM(t)
sigch := make(chan os.Signal)
sm.HandleSignals(sigch)
sigch <- os.Interrupt
<-time.After(10 * time.Millisecond)
if atomic.LoadInt32(&sm.releasing) != 1 {
t.Error("Releasing flag did not get set")
}
clean()
// Test again, this time with a running call
sm, clean = mkNaiveSM(t)
sm.HandleSignals(sigch)
errchan := make(chan error)
go func() {
_, callerr := sm.DeduceProjectRoot("k8s.io/kubernetes")
errchan <- callerr
}()
go func() { sigch <- os.Interrupt }()
runtime.Gosched()
callerr := <-errchan
if callerr == nil {
t.Error("network call could not have completed before cancellation, should have gotten an error")
}
if atomic.LoadInt32(&sm.releasing) != 1 {
t.Error("Releasing flag did not get set")
}
clean()
sm, clean = mkNaiveSM(t)
// Ensure that handling also works after stopping and restarting itself,
// and that Release happens only once.
sm.UseDefaultSignalHandling()
sm.StopSignalHandling()
sm.HandleSignals(sigch)
go func() {
_, callerr := sm.DeduceProjectRoot("k8s.io/kubernetes")
errchan <- callerr
}()
go func() {
sigch <- os.Interrupt
sm.Release()
}()
runtime.Gosched()
after := time.After(2 * time.Second)
select {
case <-sm.qch:
case <-after:
t.Error("did not shut down in reasonable time")
}
clean()
}
func TestUnreachableSource(t *testing.T) {
// If a git remote is unreachable (maybe the server is only accessible behind a VPN, or
// something), we should return a clear error, not a panic.
if testing.Short() {
t.Skip("Skipping slow test in short mode")
}
sm, clean := mkNaiveSM(t)
defer clean()
id := mkPI("github.com/golang/notexist").normalize()
err := sm.SyncSourceFor(id)
if err == nil {
t.Error("expected err when listing versions of a bogus source, but got nil")
}
}
func TestSupervisor(t *testing.T) {
bgc := context.Background()
ctx, cancelFunc := context.WithCancel(bgc)
superv := newSupervisor(ctx)
ci := callInfo{
name: "foo",
typ: 0,
}
_, err := superv.start(ci)
if err != nil {
t.Fatal("unexpected err on setUpCall:", err)
}
tc, exists := superv.running[ci]
if !exists {
t.Fatal("running call not recorded in map")
}
if tc.count != 1 {
t.Fatalf("wrong count of running ci: wanted 1 got %v", tc.count)
}
// run another, but via do
block, wait := make(chan struct{}), make(chan struct{})
errchan := make(chan error)
go func() {
wait <- struct{}{}
err := superv.do(bgc, "foo", 0, func(ctx context.Context) error {
<-block
return nil
})
errchan <- err
//if err != nil {
// t.Fatal("unexpected err on do() completion:", err)
//}
close(wait)
}()
<-wait
superv.mu.Lock()
tc, exists = superv.running[ci]
if !exists {
t.Fatal("running call not recorded in map")
}
// TODO (kris-nova) We need to disable this bypass here, and in the .travis.yml
// as soon as dep#501 is fixed
bypass := os.Getenv("DEPTESTBYPASS501")
if bypass != "" {
t.Log("bypassing tc.count check for running ci")
} else if tc.count != 2 {
t.Fatalf("wrong count of running ci: wanted 2 got %v", tc.count)
}
superv.mu.Unlock()
close(block)
possibleConcurrentError := <-errchan
if possibleConcurrentError != nil {
t.Fatal("unexpected err on do() completion:", err)
}
<-wait
superv.mu.Lock()
if len(superv.ran) != 0 {
t.Fatal("should not record metrics until last one drops")
}
tc, exists = superv.running[ci]
if !exists {
t.Fatal("running call not recorded in map")
}
if tc.count != 1 {
t.Fatalf("wrong count of running ci: wanted 1 got %v", tc.count)
}
superv.mu.Unlock()
superv.done(ci)
superv.mu.Lock()
ran, exists := superv.ran[0]
if !exists {
t.Fatal("should have metrics after closing last of a ci, but did not")
}
if ran.count != 1 {
t.Fatalf("wrong count of serial runs of a call: wanted 1 got %v", ran.count)
}
superv.mu.Unlock()
cancelFunc()
_, err = superv.start(ci)
if err == nil {
t.Fatal("should have errored on cm.run() after canceling cm's input context")
}
superv.do(bgc, "foo", 0, func(ctx context.Context) error {
t.Fatal("calls should not be initiated by do() after main context is cancelled")
return nil
})
}