gps/solve_test.go
// Copyright 2017 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"
"log"
"reflect"
"sort"
"testing"
"github.com/golang/dep/internal/test"
)
// overrideMkBridge overrides the base bridge with the depspecBridge that skips
// verifyRootDir calls
func overrideMkBridge(s *solver, sm SourceManager, down bool) sourceBridge {
return &depspecBridge{mkBridge(s, sm, down)}
}
func fixSolve(params SolveParameters, sm SourceManager, t *testing.T) (Solution, error) {
// Trace unconditionally; by passing the trace through t.Log(), the testing
// system will decide whether or not to actually show the output (based on
// -v, or selectively on test failure).
params.TraceLogger = log.New(test.Writer{TB: t}, "", 0)
// always return false, otherwise it would identify pretty much all of
// our fixtures as being stdlib and skip everything
params.stdLibFn = func(string) bool { return false }
params.mkBridgeFn = overrideMkBridge
s, err := Prepare(params, sm)
if err != nil {
return nil, err
}
return s.Solve(context.Background())
}
// Test all the basic table fixtures.
//
// Or, just the one named in the fix arg.
func TestBasicSolves(t *testing.T) {
// sort them by their keys so we get stable output
names := make([]string, 0, len(basicFixtures))
for n := range basicFixtures {
names = append(names, n)
}
sort.Strings(names)
for _, n := range names {
n := n
t.Run(n, func(t *testing.T) {
t.Parallel()
solveBasicsAndCheck(basicFixtures[n], t)
})
}
}
func solveBasicsAndCheck(fix basicFixture, t *testing.T) (res Solution, err error) {
sm := newdepspecSM(fix.ds, nil)
if fix.broken != "" {
t.Skip(fix.broken)
}
params := SolveParameters{
RootDir: string(fix.ds[0].n),
RootPackageTree: fix.rootTree(),
Manifest: fix.rootmanifest(),
Lock: dummyLock{},
Downgrade: fix.downgrade,
ChangeAll: fix.changeall,
ToChange: fix.changelist,
ProjectAnalyzer: naiveAnalyzer{},
}
if fix.l != nil {
params.Lock = fix.l
}
res, err = fixSolve(params, sm, t)
return fixtureSolveSimpleChecks(fix, res, err, t)
}
// Test all the bimodal table fixtures.
//
// Or, just the one named in the fix arg.
func TestBimodalSolves(t *testing.T) {
// sort them by their keys so we get stable output
names := make([]string, 0, len(bimodalFixtures))
for n := range bimodalFixtures {
names = append(names, n)
}
sort.Strings(names)
for _, n := range names {
n := n
t.Run(n, func(t *testing.T) {
t.Parallel()
solveBimodalAndCheck(bimodalFixtures[n], t)
})
}
}
func solveBimodalAndCheck(fix bimodalFixture, t *testing.T) (res Solution, err error) {
sm := newbmSM(fix)
if fix.broken != "" {
t.Skip(fix.broken)
}
params := SolveParameters{
RootDir: string(fix.ds[0].n),
RootPackageTree: fix.rootTree(),
Manifest: fix.rootmanifest(),
Lock: dummyLock{},
Downgrade: fix.downgrade,
ChangeAll: fix.changeall,
ProjectAnalyzer: naiveAnalyzer{},
}
if fix.l != nil {
params.Lock = fix.l
}
res, err = fixSolve(params, sm, t)
return fixtureSolveSimpleChecks(fix, res, err, t)
}
func fixtureSolveSimpleChecks(fix specfix, soln Solution, err error, t *testing.T) (Solution, error) {
ppi := func(id ProjectIdentifier) string {
// need this so we can clearly tell if there's a Source or not
if id.Source == "" {
return string(id.ProjectRoot)
}
return fmt.Sprintf("%s (from %s)", id.ProjectRoot, id.Source)
}
pv := func(v Version) string {
if pv, ok := v.(PairedVersion); ok {
return fmt.Sprintf("%s (%s)", pv.Unpair(), pv.Revision())
}
return v.String()
}
fixfail := fix.failure()
if err != nil {
if fixfail == nil {
t.Errorf("Solve failed unexpectedly:\n%s", err)
} else if fixfail.Error() != err.Error() {
// TODO(sdboyer) reflect.DeepEqual works for now, but once we start
// modeling more complex cases, this should probably become more robust
t.Errorf("Failure mismatch:\n\t(GOT): %s\n\t(WNT): %s", err, fixfail)
}
} else if fixfail != nil {
var buf bytes.Buffer
fmt.Fprintf(&buf, "Solver succeeded, but expecting failure:\n%s\nProjects in solution:", fixfail)
for _, p := range soln.Projects() {
fmt.Fprintf(&buf, "\n\t- %s at %s", ppi(p.Ident()), p.Version())
}
t.Error(buf.String())
} else {
r := soln.(solution)
if fix.maxTries() > 0 && r.Attempts() > fix.maxTries() {
t.Errorf("Solver completed in %v attempts, but expected %v or fewer", r.att, fix.maxTries())
}
// Dump result projects into a map for easier interrogation
rp := make(map[ProjectIdentifier]LockedProject)
for _, lp := range r.p {
rp[lp.Ident()] = lp
}
fixlen, rlen := len(fix.solution()), len(rp)
if fixlen != rlen {
// Different length, so they definitely disagree
t.Errorf("Solver reported %v package results, result expected %v", rlen, fixlen)
}
// Whether or not len is same, still have to verify that results agree
// Walk through fixture/expected results first
for id, flp := range fix.solution() {
if lp, exists := rp[id]; !exists {
t.Errorf("Project %q expected but missing from results", ppi(id))
} else {
// delete result from map so we skip it on the reverse pass
delete(rp, id)
if flp.Version() != lp.Version() {
t.Errorf("Expected version %q of project %q, but actual version was %q", pv(flp.Version()), ppi(id), pv(lp.Version()))
}
if !reflect.DeepEqual(lp.Packages(), flp.Packages()) {
t.Errorf("Package list was not not as expected for project %s@%s:\n\t(GOT) %s\n\t(WNT) %s", ppi(id), pv(lp.Version()), lp.Packages(), flp.Packages())
}
}
}
// Now walk through remaining actual results
for id, lp := range rp {
if _, exists := fix.solution()[id]; !exists {
t.Errorf("Unexpected project %s@%s present in results, with pkgs:\n\t%s", ppi(id), pv(lp.Version()), lp.Packages())
}
}
}
return soln, err
}
// This tests that, when a root lock is underspecified (has only a version) we
// don't allow a match on that version from a rev in the manifest. We may allow
// this in the future, but disallow it for now because going from an immutable
// requirement to a mutable lock automagically is a bad direction that could
// produce weird side effects.
func TestRootLockNoVersionPairMatching(t *testing.T) {
fix := basicFixture{
n: "does not match unpaired lock versions with paired real versions",
ds: []depspec{
mkDepspec("root 0.0.0", "foo *"), // foo's constraint rewritten below to foorev
mkDepspec("foo 1.0.0", "bar 1.0.0"),
mkDepspec("foo 1.0.1 foorev", "bar 1.0.1"),
mkDepspec("foo 1.0.2 foorev", "bar 1.0.2"),
mkDepspec("bar 1.0.0"),
mkDepspec("bar 1.0.1"),
mkDepspec("bar 1.0.2"),
},
l: mklock(
"foo 1.0.1",
),
r: mksolution(
"foo 1.0.2 foorev",
"bar 1.0.2",
),
}
pd := fix.ds[0].deps[0]
pd.Constraint = Revision("foorev")
fix.ds[0].deps[0] = pd
sm := newdepspecSM(fix.ds, nil)
l2 := make(fixLock, 1)
copy(l2, fix.l)
l2lp := l2[0].(lockedProject)
l2lp.v = nil
l2[0] = l2lp
params := SolveParameters{
RootDir: string(fix.ds[0].n),
RootPackageTree: fix.rootTree(),
Manifest: fix.rootmanifest(),
Lock: l2,
ProjectAnalyzer: naiveAnalyzer{},
}
res, err := fixSolve(params, sm, t)
fixtureSolveSimpleChecks(fix, res, err, t)
}