blob: f3892d6a9097d9e8241622a450bc6e6551646e8e [file] [log] [blame]
package gps
import (
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"runtime"
"sync"
"testing"
"github.com/Masterminds/semver"
)
var bd string
// An analyzer that passes nothing back, but doesn't error. This is the naive
// case - no constraints, no lock, and no errors. The SourceMgr 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() (name string, version *semver.Version) {
return "naive-analyzer", sv("v0.0.1")
}
func sv(s string) *semver.Version {
sv, err := semver.NewVersion(s)
if err != nil {
panic(fmt.Sprintf("Error creating semver from %q: %s", s, err))
}
return sv
}
func mkNaiveSM(t *testing.T) (*SourceMgr, func()) {
cpath, err := ioutil.TempDir("", "smcache")
if err != nil {
t.Errorf("Failed to create temp dir: %s", err)
t.FailNow()
}
sm, err := NewSourceManager(naiveAnalyzer{}, cpath, false)
if err != nil {
t.Errorf("Unexpected error on SourceManager creation: %s", err)
t.FailNow()
}
return sm, func() {
sm.Release()
err := removeAll(cpath)
if err != nil {
t.Errorf("removeAll failed: %s", err)
}
}
}
func init() {
_, filename, _, _ := runtime.Caller(1)
bd = path.Dir(filename)
}
func TestSourceManagerInit(t *testing.T) {
cpath, err := ioutil.TempDir("", "smcache")
if err != nil {
t.Errorf("Failed to create temp dir: %s", err)
}
_, err = NewSourceManager(naiveAnalyzer{}, cpath, false)
if err != nil {
t.Errorf("Unexpected error on SourceManager creation: %s", err)
}
defer func() {
err := removeAll(cpath)
if err != nil {
t.Errorf("removeAll failed: %s", err)
}
}()
_, err = NewSourceManager(naiveAnalyzer{}, cpath, false)
if err == nil {
t.Errorf("Creating second SourceManager should have failed due to file lock contention")
}
sm, err := NewSourceManager(naiveAnalyzer{}, cpath, true)
defer sm.Release()
if err != nil {
t.Errorf("Creating second SourceManager should have succeeded when force flag was passed, but failed with err %s", err)
}
if _, err = os.Stat(path.Join(cpath, "sm.lock")); err != nil {
t.Errorf("Global cache lock file not created correctly")
}
}
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.Errorf("Failed to create temp dir: %s", err)
t.FailNow()
}
sm, err := NewSourceManager(naiveAnalyzer{}, cpath, false)
if err != nil {
t.Errorf("Unexpected error on SourceManager creation: %s", err)
t.FailNow()
}
defer func() {
sm.Release()
err := removeAll(cpath)
if err != nil {
t.Errorf("removeAll failed: %s", err)
}
}()
id := mkPI("github.com/Masterminds/VCSTestRepo").normalize()
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 {
rev := Revision("30605f6ac35fcb075ad0bfa9296f90a7d891523e")
expected := []Version{
NewVersion("1.0.0").Is(rev),
NewBranch("master").Is(rev),
NewBranch("test").Is(rev),
}
// SourceManager itself doesn't guarantee ordering; sort them here so we
// can dependably check output
SortForUpgrade(v)
for k, e := range expected {
if v[k] != e {
t.Errorf("Expected version %s in position %v but got %s", e, k, v[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{},
}
v, err = smc.ListVersions(id)
if err != nil {
t.Errorf("Unexpected error during initial project setup/fetching %s", err)
}
rev := Revision("30605f6ac35fcb075ad0bfa9296f90a7d891523e")
if len(v) != 3 {
t.Errorf("Expected three version results from the test repo, got %v", len(v))
} else {
expected := []Version{
NewVersion("1.0.0").Is(rev),
NewBranch("master").Is(rev),
NewBranch("test").Is(rev),
}
for k, e := range expected {
if v[k] != e {
t.Errorf("Expected version %s in position %v but got %s", e, k, v[k])
}
}
}
present, err := smc.RevisionPresentIn(id, rev)
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-Masterminds-VCSTestRepo", ".git"))
if err != nil {
t.Error("Cache repo does not exist in expected location")
}
_, err = os.Stat(filepath.Join(cpath, "metadata", "github.com", "Masterminds", "VCSTestRepo", "cache.json"))
if err != nil {
// TODO(sdboyer) disabled until we get caching working
//t.Error("Metadata cache json file does not exist in expected location")
}
// 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 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); err == nil {
t.Error("GetManifestAndLock() did not error on bad input")
}
if err = sm.ExportProject(bad, nil, ""); err == nil {
t.Error("ExportProject() did not error on bad input")
}
}
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")
}
sm, clean := mkNaiveSM(t)
pil := []ProjectIdentifier{
mkPI("github.com/Masterminds/VCSTestRepo").normalize(),
mkPI("bitbucket.org/mattfarina/testhgrepo").normalize(),
mkPI("launchpad.net/govcstestbzrrepo").normalize(),
}
wg := &sync.WaitGroup{}
wg.Add(3)
for _, pi := range pil {
go func(lpi ProjectIdentifier) {
nn := lpi.netName()
src, err := sm.getSourceFor(lpi)
if err != nil {
t.Errorf("(src %q) unexpected error setting up source: %s", nn, err)
return
}
// Re-get the same, make sure they are the same
src2, err := sm.getSourceFor(lpi)
if err != nil {
t.Errorf("(src %q) unexpected error re-getting source: %s", nn, err)
} else if src != src2 {
t.Errorf("(src %q) first and second sources are not eq", nn)
}
// All of them _should_ select https, so this should work
lpi.NetworkName = "https://" + lpi.NetworkName
src3, err := sm.getSourceFor(lpi)
if err != nil {
t.Errorf("(src %q) unexpected error getting explicit https source: %s", nn, err)
} else if src != src3 {
t.Errorf("(src %q) explicit https source should reuse autodetected https source", nn)
}
// Now put in http, and they should differ
lpi.NetworkName = "http://" + string(lpi.ProjectRoot)
src4, err := sm.getSourceFor(lpi)
if err != nil {
t.Errorf("(src %q) unexpected error getting explicit http source: %s", nn, err)
} else if src == src4 {
t.Errorf("(src %q) explicit http source should create a new src", nn)
}
wg.Done()
}(pi)
}
wg.Wait()
// nine entries (of which three are dupes): for each vcs, raw import path,
// the https url, and the http url
if len(sm.srcs) != 9 {
t.Errorf("Should have nine discrete entries in the srcs map, got %v", len(sm.srcs))
}
clean()
}
// 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/Masterminds/VCSTestRepo").normalize()
_, _, err := sm.GetManifestAndLock(id, NewVersion("1.0.0"))
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) != 3 {
t.Errorf("Expected three 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.rootxt.Len() != 1 {
t.Errorf("Root path trie should have one element after one deduction, has %v", sm.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.rootxt.Len() != 1 {
t.Errorf("Root path trie should still have one element after performing the same deduction twice; has %v", sm.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.rootxt.Len() != 2 {
t.Errorf("Root path trie should have two elements, one for root and one for subpath; has %v", sm.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.rootxt.Len() != 4 {
t.Errorf("Root path trie should have four elements, one for each unique root and subpath; has %v", sm.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.rootxt.Len() != 5 {
t.Errorf("Root path trie should have five elements, one for each unique root and subpath; has %v", sm.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.rootxt.Len() != 6 {
t.Errorf("Root path trie should have six elements, one for each unique root and subpath; has %v", sm.rootxt.Len())
}
}
// Test that the future returned from SourceMgr.deducePathAndProcess() is safe
// to call concurrently.
//
// Obviously, this is just a heuristic; passage does not guarantee correctness
// (though failure does guarantee incorrectness)
func TestMultiDeduceThreadsafe(t *testing.T) {
sm, clean := mkNaiveSM(t)
defer clean()
in := "github.com/sdboyer/gps"
rootf, srcf, err := sm.deducePathAndProcess(in)
if err != nil {
t.Errorf("Known-good path %q had unexpected basic deduction error: %s", in, err)
t.FailNow()
}
cnum := 50
wg := &sync.WaitGroup{}
// Set up channel for everything else to block on
c := make(chan struct{}, 1)
f := func(rnum int) {
defer func() {
wg.Done()
if e := recover(); e != nil {
t.Errorf("goroutine number %v panicked with err: %s", rnum, e)
}
}()
<-c
_, err := rootf()
if err != nil {
t.Errorf("err was non-nil on root detection in goroutine number %v: %s", rnum, err)
}
}
for k := range make([]struct{}, cnum) {
wg.Add(1)
go f(k)
runtime.Gosched()
}
close(c)
wg.Wait()
if sm.rootxt.Len() != 1 {
t.Errorf("Root path trie should have just one element; has %v", sm.rootxt.Len())
}
// repeat for srcf
wg2 := &sync.WaitGroup{}
c = make(chan struct{}, 1)
f = func(rnum int) {
defer func() {
wg2.Done()
if e := recover(); e != nil {
t.Errorf("goroutine number %v panicked with err: %s", rnum, e)
}
}()
<-c
_, _, err := srcf()
if err != nil {
t.Errorf("err was non-nil on root detection in goroutine number %v: %s", rnum, err)
}
}
for k := range make([]struct{}, cnum) {
wg2.Add(1)
go f(k)
runtime.Gosched()
}
close(c)
wg2.Wait()
if len(sm.srcs) != 2 {
t.Errorf("Sources map should have just two elements, but has %v", len(sm.srcs))
}
}