gps/verify/locksat.go

Summary

Maintainability
A
0 mins
Test Coverage
// Copyright 2018 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 verify

import (
    radix "github.com/armon/go-radix"
    "github.com/golang/dep/gps"
    "github.com/golang/dep/gps/paths"
    "github.com/golang/dep/gps/pkgtree"
)

// LockSatisfaction holds the compound result of LockSatisfiesInputs, allowing
// the caller to inspect each of several orthogonal possible types of failure.
//
// The zero value assumes that there was no input lock, which necessarily means
// the inputs were not satisfied. This zero value means we err on the side of
// failure.
type LockSatisfaction struct {
    // If LockExisted is false, it indicates that a nil gps.Lock was passed to
    // LockSatisfiesInputs().
    LockExisted bool
    // MissingImports is the set of import paths that were present in the
    // inputs but missing in the Lock.
    MissingImports []string
    // ExcessImports is the set of import paths that were present in the Lock
    // but absent from the inputs.
    ExcessImports []string
    // UnmatchedConstraints reports any normal, non-override constraint rules that
    // were not satisfied by the corresponding LockedProject in the Lock.
    UnmetConstraints map[gps.ProjectRoot]ConstraintMismatch
    // UnmatchedOverrides reports any override rules that were not satisfied by the
    // corresponding LockedProject in the Lock.
    UnmetOverrides map[gps.ProjectRoot]ConstraintMismatch
}

// ConstraintMismatch is a two-tuple of a gps.Version, and a gps.Constraint that
// does not allow that version.
type ConstraintMismatch struct {
    C gps.Constraint
    V gps.Version
}

// LockSatisfiesInputs determines whether the provided Lock satisfies all the
// requirements indicated by the inputs (RootManifest and PackageTree).
//
// The second parameter is expected to be the list of imports that were used to
// generate the input Lock. Without this explicit list, it is not possible to
// compute package imports that may have been removed. Figuring out that
// negative space would require exploring the entire graph to ensure there are
// no in-edges for particular imports.
func LockSatisfiesInputs(l gps.Lock, m gps.RootManifest, ptree pkgtree.PackageTree) LockSatisfaction {
    if l == nil {
        return LockSatisfaction{}
    }

    lsat := LockSatisfaction{
        LockExisted:      true,
        UnmetOverrides:   make(map[gps.ProjectRoot]ConstraintMismatch),
        UnmetConstraints: make(map[gps.ProjectRoot]ConstraintMismatch),
    }

    var ig *pkgtree.IgnoredRuleset
    var req map[string]bool
    if m != nil {
        ig = m.IgnoredPackages()
        req = m.RequiredPackages()
    }

    rm, _ := ptree.ToReachMap(true, true, false, ig)
    reach := rm.FlattenFn(paths.IsStandardImportPath)

    inlock := make(map[string]bool, len(l.InputImports()))
    ininputs := make(map[string]bool, len(reach)+len(req))

    type lockUnsatisfy uint8
    const (
        missingFromLock lockUnsatisfy = iota
        inAdditionToLock
    )

    pkgDiff := make(map[string]lockUnsatisfy)

    for _, imp := range reach {
        ininputs[imp] = true
    }

    for imp := range req {
        ininputs[imp] = true
    }

    for _, imp := range l.InputImports() {
        inlock[imp] = true
    }

    for ip := range ininputs {
        if !inlock[ip] {
            pkgDiff[ip] = missingFromLock
        } else {
            // So we don't have to revisit it below
            delete(inlock, ip)
        }
    }

    // Something in the missing list might already be in the packages list,
    // because another package in the depgraph imports it. We could make a
    // special case for that, but it would break the simplicity of the model and
    // complicate the notion of LockSatisfaction.Passed(), so let's see if we
    // can get away without it.

    for ip := range inlock {
        if !ininputs[ip] {
            pkgDiff[ip] = inAdditionToLock
        }
    }

    for ip, typ := range pkgDiff {
        if typ == missingFromLock {
            lsat.MissingImports = append(lsat.MissingImports, ip)
        } else {
            lsat.ExcessImports = append(lsat.ExcessImports, ip)
        }
    }

    eff := findEffectualConstraints(m, ininputs)
    ovr, constraints := m.Overrides(), m.DependencyConstraints()

    for _, lp := range l.Projects() {
        pr := lp.Ident().ProjectRoot

        if pp, has := ovr[pr]; has {
            if !pp.Constraint.Matches(lp.Version()) {
                lsat.UnmetOverrides[pr] = ConstraintMismatch{
                    C: pp.Constraint,
                    V: lp.Version(),
                }
            }
            // The constraint isn't considered if we have an override,
            // independent of whether the override is satisfied.
            continue
        }

        if pp, has := constraints[pr]; has && eff[string(pr)] && !pp.Constraint.Matches(lp.Version()) {
            lsat.UnmetConstraints[pr] = ConstraintMismatch{
                C: pp.Constraint,
                V: lp.Version(),
            }
        }
    }

    return lsat
}

// Satisfied is a shortcut method that indicates whether there were any ways in
// which the Lock did not satisfy the inputs. It will return true only if the
// Lock was satisfactory in all respects vis-a-vis the inputs.
func (ls LockSatisfaction) Satisfied() bool {
    if !ls.LockExisted {
        return false
    }

    if len(ls.MissingImports) > 0 {
        return false
    }

    if len(ls.ExcessImports) > 0 {
        return false
    }

    if len(ls.UnmetOverrides) > 0 {
        return false
    }

    if len(ls.UnmetConstraints) > 0 {
        return false
    }

    return true
}

func findEffectualConstraints(m gps.Manifest, imports map[string]bool) map[string]bool {
    eff := make(map[string]bool)
    xt := radix.New()

    for pr := range m.DependencyConstraints() {
        // FIXME(sdboyer) this has the trailing slash ambiguity problem; adapt
        // code from the solver
        xt.Insert(string(pr), nil)
    }

    for imp := range imports {
        if root, _, has := xt.LongestPrefix(imp); has {
            eff[root] = true
        }
    }

    return eff
}