gps/solve_test.go

Summary

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