gps/manager_test.go

Summary

Maintainability
A
0 mins
Test Coverage
// 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
    })
}