| // 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 ( |
| "context" |
| "fmt" |
| "net/url" |
| "regexp" |
| "strings" |
| |
| "github.com/Masterminds/semver" |
| "github.com/golang/dep/gps/pkgtree" |
| ) |
| |
| var regfrom = regexp.MustCompile(`^(\w*) from (\w*) ([0-9\.\*]*)`) |
| |
| // nvSplit splits an "info" string on " " into the pair of name and |
| // version/constraint, and returns each individually. |
| // |
| // This is for narrow use - panics if there are less than two resulting items in |
| // the slice. |
| func nvSplit(info string) (id ProjectIdentifier, version string) { |
| if strings.Contains(info, " from ") { |
| parts := regfrom.FindStringSubmatch(info) |
| info = parts[1] + " " + parts[3] |
| id.Source = parts[2] |
| } |
| |
| s := strings.SplitN(info, " ", 2) |
| if len(s) < 2 { |
| panic(fmt.Sprintf("Malformed name/version info string '%s'", info)) |
| } |
| |
| id.ProjectRoot, version = ProjectRoot(s[0]), s[1] |
| return |
| } |
| |
| // nvrSplit splits an "info" string on " " into the triplet of name, |
| // version/constraint, and revision, and returns each individually. |
| // |
| // It will work fine if only name and version/constraint are provided. |
| // |
| // This is for narrow use - panics if there are less than two resulting items in |
| // the slice. |
| func nvrSplit(info string) (id ProjectIdentifier, version string, revision Revision) { |
| if strings.Contains(info, " from ") { |
| parts := regfrom.FindStringSubmatch(info) |
| info = fmt.Sprintf("%s %s", parts[1], parts[3]) |
| id.Source = parts[2] |
| } |
| |
| s := strings.SplitN(info, " ", 3) |
| if len(s) < 2 { |
| panic(fmt.Sprintf("Malformed name/version info string '%s'", info)) |
| } |
| |
| id.ProjectRoot, version = ProjectRoot(s[0]), s[1] |
| |
| if len(s) == 3 { |
| revision = Revision(s[2]) |
| } |
| return |
| } |
| |
| // mkAtom splits the input string on a space, and uses the first two elements as |
| // the project identifier and version, respectively. |
| // |
| // The version segment may have a leading character indicating the type of |
| // version to create: |
| // |
| // p: create a "plain" (non-semver) version. |
| // b: create a branch version. |
| // r: create a revision. |
| // |
| // No prefix is assumed to indicate a semver version. |
| // |
| // If a third space-delimited element is provided, it will be interepreted as a |
| // revision, and used as the underlying version in a PairedVersion. No prefix |
| // should be provided in this case. It is an error (and will panic) to try to |
| // pass a revision with an underlying revision. |
| func mkAtom(info string) atom { |
| // if info is "root", special case it to use the root "version" |
| if info == "root" { |
| return atom{ |
| id: ProjectIdentifier{ |
| ProjectRoot: ProjectRoot("root"), |
| }, |
| v: rootRev, |
| } |
| } |
| |
| id, ver, rev := nvrSplit(info) |
| |
| var v Version |
| switch ver[0] { |
| case 'r': |
| if rev != "" { |
| panic("Cannot pair a revision with a revision") |
| } |
| v = Revision(ver[1:]) |
| case 'p': |
| v = NewVersion(ver[1:]) |
| case 'b': |
| v = NewBranch(ver[1:]) |
| default: |
| _, err := semver.NewVersion(ver) |
| if err != nil { |
| // don't want to allow bad test data at this level, so just panic |
| panic(fmt.Sprintf("Error when converting '%s' into semver: %s", ver, err)) |
| } |
| v = NewVersion(ver) |
| } |
| |
| if rev != "" { |
| v = v.(UnpairedVersion).Pair(rev) |
| } |
| |
| return atom{ |
| id: id, |
| v: v, |
| } |
| } |
| |
| // mkPCstrnt splits the input string on a space, and uses the first two elements |
| // as the project identifier and constraint body, respectively. |
| // |
| // The constraint body may have a leading character indicating the type of |
| // version to create: |
| // |
| // p: create a "plain" (non-semver) version. |
| // b: create a branch version. |
| // r: create a revision. |
| // |
| // If no leading character is used, a semver constraint is assumed. |
| func mkPCstrnt(info string) ProjectConstraint { |
| id, ver, rev := nvrSplit(info) |
| |
| var c Constraint |
| switch ver[0] { |
| case 'r': |
| c = Revision(ver[1:]) |
| case 'p': |
| c = NewVersion(ver[1:]) |
| case 'b': |
| c = NewBranch(ver[1:]) |
| default: |
| // Without one of those leading characters, we know it's a proper semver |
| // expression, so use the other parser that doesn't look for a rev |
| rev = "" |
| id, ver = nvSplit(info) |
| var err error |
| c, err = NewSemverConstraint(ver) |
| if err != nil { |
| // don't want bad test data at this level, so just panic |
| panic(fmt.Sprintf("Error when converting '%s' into semver constraint: %s (full info: %s)", ver, err, info)) |
| } |
| } |
| |
| // There's no practical reason that a real tool would need to produce a |
| // constraint that's a PairedVersion, but it is a possibility admitted by the |
| // system, so we at least allow for it in our testing harness. |
| if rev != "" { |
| // Of course, this *will* panic if the predicate is a revision or a |
| // semver constraint, neither of which implement UnpairedVersion. This |
| // is as intended, to prevent bad data from entering the system. |
| c = c.(UnpairedVersion).Pair(rev) |
| } |
| |
| return ProjectConstraint{ |
| Ident: id, |
| Constraint: c, |
| } |
| } |
| |
| // mkCDep composes a completeDep struct from the inputs. |
| // |
| // The only real work here is passing the initial string to mkPDep. All the |
| // other args are taken as package names. |
| func mkCDep(pdep string, pl ...string) completeDep { |
| pc := mkPCstrnt(pdep) |
| return completeDep{ |
| workingConstraint: workingConstraint{ |
| Ident: pc.Ident, |
| Constraint: pc.Constraint, |
| }, |
| pl: pl, |
| } |
| } |
| |
| // A depspec is a fixture representing all the information a SourceManager would |
| // ordinarily glean directly from interrogating a repository. |
| type depspec struct { |
| n ProjectRoot |
| v Version |
| deps []ProjectConstraint |
| pkgs []tpkg |
| } |
| |
| // mkDepspec creates a depspec by processing a series of strings, each of which |
| // contains an identiifer and version information. |
| // |
| // The first string is broken out into the name and version of the package being |
| // described - see the docs on mkAtom for details. subsequent strings are |
| // interpreted as dep constraints of that dep at that version. See the docs on |
| // mkPDep for details. |
| func mkDepspec(pi string, deps ...string) depspec { |
| pa := mkAtom(pi) |
| if string(pa.id.ProjectRoot) != pa.id.Source && pa.id.Source != "" { |
| panic("alternate source on self makes no sense") |
| } |
| |
| ds := depspec{ |
| n: pa.id.ProjectRoot, |
| v: pa.v, |
| } |
| |
| for _, dep := range deps { |
| ds.deps = append(ds.deps, mkPCstrnt(dep)) |
| } |
| |
| return ds |
| } |
| |
| func mkDep(atom, pdep string, pl ...string) dependency { |
| return dependency{ |
| depender: mkAtom(atom), |
| dep: mkCDep(pdep, pl...), |
| } |
| } |
| |
| func mkADep(atom, pdep string, c Constraint, pl ...string) dependency { |
| return dependency{ |
| depender: mkAtom(atom), |
| dep: completeDep{ |
| workingConstraint: workingConstraint{ |
| Ident: ProjectIdentifier{ |
| ProjectRoot: ProjectRoot(pdep), |
| }, |
| Constraint: c, |
| }, |
| pl: pl, |
| }, |
| } |
| } |
| |
| // mkPI creates a ProjectIdentifier with the ProjectRoot as the provided |
| // string, and the Source unset. |
| // |
| // Call normalize() on the returned value if you need the Source to be be |
| // equal to the ProjectRoot. |
| func mkPI(root string) ProjectIdentifier { |
| return ProjectIdentifier{ |
| ProjectRoot: ProjectRoot(root), |
| } |
| } |
| |
| // mkSVC creates a new semver constraint, panicking if an error is returned. |
| func mkSVC(body string) Constraint { |
| c, err := NewSemverConstraint(body) |
| if err != nil { |
| panic(fmt.Sprintf("Error while trying to create semver constraint from %s: %s", body, err.Error())) |
| } |
| return c |
| } |
| |
| // mklock makes a fixLock, suitable to act as a lock file |
| func mklock(pairs ...string) fixLock { |
| l := make(fixLock, 0) |
| for _, s := range pairs { |
| pa := mkAtom(s) |
| l = append(l, NewLockedProject(pa.id, pa.v, nil)) |
| } |
| |
| return l |
| } |
| |
| // mkrevlock makes a fixLock, suitable to act as a lock file, with only a name |
| // and a rev |
| func mkrevlock(pairs ...string) fixLock { |
| l := make(fixLock, 0) |
| for _, s := range pairs { |
| pa := mkAtom(s) |
| l = append(l, NewLockedProject(pa.id, pa.v.(PairedVersion).Revision(), nil)) |
| } |
| |
| return l |
| } |
| |
| // mksolution creates a map of project identifiers to their LockedProject |
| // result, which is sufficient to act as a solution fixture for the purposes of |
| // most tests. |
| // |
| // Either strings or LockedProjects can be provided. If a string is provided, it |
| // is assumed that we're in the default, "basic" case where there is exactly one |
| // package in a project, and it is the root of the project - meaning that only |
| // the "." package should be listed. If a LockedProject is provided (e.g. as |
| // returned from mklp()), then it's incorporated directly. |
| // |
| // If any other type is provided, the func will panic. |
| func mksolution(inputs ...interface{}) map[ProjectIdentifier]LockedProject { |
| m := make(map[ProjectIdentifier]LockedProject) |
| for _, in := range inputs { |
| switch t := in.(type) { |
| case string: |
| a := mkAtom(t) |
| m[a.id] = NewLockedProject(a.id, a.v, []string{"."}) |
| case LockedProject: |
| m[t.Ident()] = t |
| default: |
| panic(fmt.Sprintf("unexpected input to mksolution: %T %s", in, in)) |
| } |
| } |
| |
| return m |
| } |
| |
| // mklp creates a LockedProject from string inputs |
| func mklp(pair string, pkgs ...string) LockedProject { |
| a := mkAtom(pair) |
| return NewLockedProject(a.id, a.v, pkgs) |
| } |
| |
| // computeBasicReachMap takes a depspec and computes a reach map which is |
| // identical to the explicit depgraph. |
| // |
| // Using a reachMap here is overkill for what the basic fixtures actually need, |
| // but we use it anyway for congruence with the more general cases. |
| func computeBasicReachMap(ds []depspec) reachMap { |
| rm := make(reachMap) |
| |
| for k, d := range ds { |
| n := string(d.n) |
| lm := map[string][]string{ |
| n: nil, |
| } |
| v := d.v |
| if k == 0 { |
| // Put the root in with a nil rev, to accommodate the solver |
| v = nil |
| } |
| rm[pident{n: d.n, v: v}] = lm |
| |
| for _, dep := range d.deps { |
| lm[n] = append(lm[n], string(dep.Ident.ProjectRoot)) |
| } |
| } |
| |
| return rm |
| } |
| |
| type pident struct { |
| n ProjectRoot |
| v Version |
| } |
| |
| type specfix interface { |
| name() string |
| rootmanifest() RootManifest |
| rootTree() pkgtree.PackageTree |
| specs() []depspec |
| maxTries() int |
| solution() map[ProjectIdentifier]LockedProject |
| failure() error |
| } |
| |
| // A basicFixture is a declarative test fixture that can cover a wide variety of |
| // solver cases. All cases, however, maintain one invariant: package == project. |
| // There are no subpackages, and so it is impossible for them to trigger or |
| // require bimodal solving. |
| // |
| // This type is separate from bimodalFixture in part for legacy reasons - many |
| // of these were adapted from similar tests in dart's pub lib, where there is no |
| // such thing as "bimodal solving". |
| // |
| // But it's also useful to keep them separate because bimodal solving involves |
| // considerably more complexity than simple solving, both in terms of fixture |
| // declaration and actual solving mechanics. Thus, we gain a lot of value for |
| // contributors and maintainers by keeping comprehension costs relatively low |
| // while still covering important cases. |
| type basicFixture struct { |
| // name of this fixture datum |
| n string |
| // depspecs. always treat first as root |
| ds []depspec |
| // results; map of name/atom pairs |
| r map[ProjectIdentifier]LockedProject |
| // max attempts the solver should need to find solution. 0 means no limit |
| maxAttempts int |
| // Use downgrade instead of default upgrade sorter |
| downgrade bool |
| // lock file simulator, if one's to be used at all |
| l fixLock |
| // solve failure expected, if any |
| fail error |
| // overrides, if any |
| ovr ProjectConstraints |
| // request up/downgrade to all projects |
| changeall bool |
| // individual projects to change |
| changelist []ProjectRoot |
| // if the fixture is currently broken/expected to fail, this has a message |
| // recording why |
| broken string |
| } |
| |
| func (f basicFixture) name() string { |
| return f.n |
| } |
| |
| func (f basicFixture) specs() []depspec { |
| return f.ds |
| } |
| |
| func (f basicFixture) maxTries() int { |
| return f.maxAttempts |
| } |
| |
| func (f basicFixture) solution() map[ProjectIdentifier]LockedProject { |
| return f.r |
| } |
| |
| func (f basicFixture) rootmanifest() RootManifest { |
| return simpleRootManifest{ |
| c: pcSliceToMap(f.ds[0].deps), |
| ovr: f.ovr, |
| } |
| } |
| |
| func (f basicFixture) rootTree() pkgtree.PackageTree { |
| var imp []string |
| for _, dep := range f.ds[0].deps { |
| imp = append(imp, string(dep.Ident.ProjectRoot)) |
| } |
| |
| n := string(f.ds[0].n) |
| pt := pkgtree.PackageTree{ |
| ImportRoot: n, |
| Packages: map[string]pkgtree.PackageOrErr{ |
| string(n): { |
| P: pkgtree.Package{ |
| ImportPath: n, |
| Name: n, |
| Imports: imp, |
| }, |
| }, |
| }, |
| } |
| |
| return pt |
| } |
| |
| func (f basicFixture) failure() error { |
| return f.fail |
| } |
| |
| // A table of basicFixtures, used in the basic solving test set. |
| var basicFixtures = map[string]basicFixture{ |
| // basic fixtures |
| "no dependencies": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0"), |
| }, |
| r: mksolution(), |
| }, |
| "simple dependency tree": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "a 1.0.0", "b 1.0.0"), |
| mkDepspec("a 1.0.0", "aa 1.0.0", "ab 1.0.0"), |
| mkDepspec("aa 1.0.0"), |
| mkDepspec("ab 1.0.0"), |
| mkDepspec("b 1.0.0", "ba 1.0.0", "bb 1.0.0"), |
| mkDepspec("ba 1.0.0"), |
| mkDepspec("bb 1.0.0"), |
| }, |
| r: mksolution( |
| "a 1.0.0", |
| "aa 1.0.0", |
| "ab 1.0.0", |
| "b 1.0.0", |
| "ba 1.0.0", |
| "bb 1.0.0", |
| ), |
| }, |
| "shared dependency with overlapping constraints": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "a 1.0.0", "b 1.0.0"), |
| mkDepspec("a 1.0.0", "shared >=2.0.0, <4.0.0"), |
| mkDepspec("b 1.0.0", "shared >=3.0.0, <5.0.0"), |
| mkDepspec("shared 2.0.0"), |
| mkDepspec("shared 3.0.0"), |
| mkDepspec("shared 3.6.9"), |
| mkDepspec("shared 4.0.0"), |
| mkDepspec("shared 5.0.0"), |
| }, |
| r: mksolution( |
| "a 1.0.0", |
| "b 1.0.0", |
| "shared 3.6.9", |
| ), |
| }, |
| "downgrade on overlapping constraints": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "a 1.0.0", "b 1.0.0"), |
| mkDepspec("a 1.0.0", "shared >=2.0.0, <=4.0.0"), |
| mkDepspec("b 1.0.0", "shared >=3.0.0, <5.0.0"), |
| mkDepspec("shared 2.0.0"), |
| mkDepspec("shared 3.0.0"), |
| mkDepspec("shared 3.6.9"), |
| mkDepspec("shared 4.0.0"), |
| mkDepspec("shared 5.0.0"), |
| }, |
| r: mksolution( |
| "a 1.0.0", |
| "b 1.0.0", |
| "shared 3.0.0", |
| ), |
| downgrade: true, |
| }, |
| "shared dependency where dependent version in turn affects other dependencies": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo <=1.0.2", "bar 1.0.0"), |
| mkDepspec("foo 1.0.0"), |
| mkDepspec("foo 1.0.1", "bang 1.0.0"), |
| mkDepspec("foo 1.0.2", "whoop 1.0.0"), |
| mkDepspec("foo 1.0.3", "zoop 1.0.0"), |
| mkDepspec("bar 1.0.0", "foo <=1.0.1"), |
| mkDepspec("bang 1.0.0"), |
| mkDepspec("whoop 1.0.0"), |
| mkDepspec("zoop 1.0.0"), |
| }, |
| r: mksolution( |
| "foo 1.0.1", |
| "bar 1.0.0", |
| "bang 1.0.0", |
| ), |
| }, |
| "removed dependency": { |
| ds: []depspec{ |
| mkDepspec("root 1.0.0", "foo 1.0.0", "bar *"), |
| mkDepspec("foo 1.0.0"), |
| mkDepspec("foo 2.0.0"), |
| mkDepspec("bar 1.0.0"), |
| mkDepspec("bar 2.0.0", "baz 1.0.0"), |
| mkDepspec("baz 1.0.0", "foo 2.0.0"), |
| }, |
| r: mksolution( |
| "foo 1.0.0", |
| "bar 1.0.0", |
| ), |
| maxAttempts: 2, |
| }, |
| // fixtures with locks |
| "with compatible locked dependency": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo *"), |
| mkDepspec("foo 1.0.0", "bar 1.0.0"), |
| mkDepspec("foo 1.0.1", "bar 1.0.1"), |
| mkDepspec("foo 1.0.2", "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.1", |
| "bar 1.0.1", |
| ), |
| }, |
| "upgrade through lock": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo *"), |
| mkDepspec("foo 1.0.0", "bar 1.0.0"), |
| mkDepspec("foo 1.0.1", "bar 1.0.1"), |
| mkDepspec("foo 1.0.2", "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", |
| "bar 1.0.2", |
| ), |
| changeall: true, |
| }, |
| "downgrade through lock": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo *"), |
| mkDepspec("foo 1.0.0", "bar 1.0.0"), |
| mkDepspec("foo 1.0.1", "bar 1.0.1"), |
| mkDepspec("foo 1.0.2", "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.0", |
| "bar 1.0.0", |
| ), |
| changeall: true, |
| downgrade: true, |
| }, |
| "update one with only one": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo *"), |
| mkDepspec("foo 1.0.0"), |
| mkDepspec("foo 1.0.1"), |
| mkDepspec("foo 1.0.2"), |
| }, |
| l: mklock( |
| "foo 1.0.1", |
| ), |
| r: mksolution( |
| "foo 1.0.2", |
| ), |
| changelist: []ProjectRoot{"foo"}, |
| }, |
| "update one of multi": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo *", "bar *"), |
| mkDepspec("foo 1.0.0"), |
| mkDepspec("foo 1.0.1"), |
| mkDepspec("foo 1.0.2"), |
| mkDepspec("bar 1.0.0"), |
| mkDepspec("bar 1.0.1"), |
| mkDepspec("bar 1.0.2"), |
| }, |
| l: mklock( |
| "foo 1.0.1", |
| "bar 1.0.1", |
| ), |
| r: mksolution( |
| "foo 1.0.2", |
| "bar 1.0.1", |
| ), |
| changelist: []ProjectRoot{"foo"}, |
| }, |
| "update both of multi": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo *", "bar *"), |
| mkDepspec("foo 1.0.0"), |
| mkDepspec("foo 1.0.1"), |
| mkDepspec("foo 1.0.2"), |
| mkDepspec("bar 1.0.0"), |
| mkDepspec("bar 1.0.1"), |
| mkDepspec("bar 1.0.2"), |
| }, |
| l: mklock( |
| "foo 1.0.1", |
| "bar 1.0.1", |
| ), |
| r: mksolution( |
| "foo 1.0.2", |
| "bar 1.0.2", |
| ), |
| changelist: []ProjectRoot{"foo", "bar"}, |
| }, |
| "update two of more": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo *", "bar *", "baz *"), |
| mkDepspec("foo 1.0.0"), |
| mkDepspec("foo 1.0.1"), |
| mkDepspec("foo 1.0.2"), |
| mkDepspec("bar 1.0.0"), |
| mkDepspec("bar 1.0.1"), |
| mkDepspec("bar 1.0.2"), |
| mkDepspec("baz 1.0.0"), |
| mkDepspec("baz 1.0.1"), |
| mkDepspec("baz 1.0.2"), |
| }, |
| l: mklock( |
| "foo 1.0.1", |
| "bar 1.0.1", |
| "baz 1.0.1", |
| ), |
| r: mksolution( |
| "foo 1.0.2", |
| "bar 1.0.2", |
| "baz 1.0.1", |
| ), |
| changelist: []ProjectRoot{"foo", "bar"}, |
| }, |
| "break other lock with targeted update": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo *", "baz *"), |
| mkDepspec("foo 1.0.0", "bar 1.0.0"), |
| mkDepspec("foo 1.0.1", "bar 1.0.1"), |
| mkDepspec("foo 1.0.2", "bar 1.0.2"), |
| mkDepspec("bar 1.0.0"), |
| mkDepspec("bar 1.0.1"), |
| mkDepspec("bar 1.0.2"), |
| mkDepspec("baz 1.0.0"), |
| mkDepspec("baz 1.0.1"), |
| mkDepspec("baz 1.0.2"), |
| }, |
| l: mklock( |
| "foo 1.0.1", |
| "bar 1.0.1", |
| "baz 1.0.1", |
| ), |
| r: mksolution( |
| "foo 1.0.2", |
| "bar 1.0.2", |
| "baz 1.0.1", |
| ), |
| changelist: []ProjectRoot{"foo", "bar"}, |
| }, |
| "with incompatible locked dependency": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo >1.0.1"), |
| mkDepspec("foo 1.0.0", "bar 1.0.0"), |
| mkDepspec("foo 1.0.1", "bar 1.0.1"), |
| mkDepspec("foo 1.0.2", "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", |
| "bar 1.0.2", |
| ), |
| }, |
| "with unrelated locked dependency": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo *"), |
| mkDepspec("foo 1.0.0", "bar 1.0.0"), |
| mkDepspec("foo 1.0.1", "bar 1.0.1"), |
| mkDepspec("foo 1.0.2", "bar 1.0.2"), |
| mkDepspec("bar 1.0.0"), |
| mkDepspec("bar 1.0.1"), |
| mkDepspec("bar 1.0.2"), |
| mkDepspec("baz 1.0.0 bazrev"), |
| }, |
| l: mklock( |
| "baz 1.0.0 bazrev", |
| ), |
| r: mksolution( |
| "foo 1.0.2", |
| "bar 1.0.2", |
| ), |
| }, |
| "unlocks dependencies if necessary to ensure that a new dependency is satisfied": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo *", "newdep *"), |
| mkDepspec("foo 1.0.0 foorev", "bar <2.0.0"), |
| mkDepspec("bar 1.0.0 barrev", "baz <2.0.0"), |
| mkDepspec("baz 1.0.0 bazrev", "qux <2.0.0"), |
| mkDepspec("qux 1.0.0 quxrev"), |
| mkDepspec("foo 2.0.0", "bar <3.0.0"), |
| mkDepspec("bar 2.0.0", "baz <3.0.0"), |
| mkDepspec("baz 2.0.0", "qux <3.0.0"), |
| mkDepspec("qux 2.0.0"), |
| mkDepspec("newdep 2.0.0", "baz >=1.5.0"), |
| }, |
| l: mklock( |
| "foo 1.0.0 foorev", |
| "bar 1.0.0 barrev", |
| "baz 1.0.0 bazrev", |
| "qux 1.0.0 quxrev", |
| ), |
| r: mksolution( |
| "foo 2.0.0", |
| "bar 2.0.0", |
| "baz 2.0.0", |
| "qux 1.0.0 quxrev", |
| "newdep 2.0.0", |
| ), |
| maxAttempts: 4, |
| }, |
| "break lock when only the deps necessitate it": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo *", "bar *"), |
| mkDepspec("foo 1.0.0 foorev", "bar <2.0.0"), |
| mkDepspec("foo 2.0.0", "bar <3.0.0"), |
| mkDepspec("bar 2.0.0", "baz <3.0.0"), |
| mkDepspec("baz 2.0.0", "foo >1.0.0"), |
| }, |
| l: mklock( |
| "foo 1.0.0 foorev", |
| ), |
| r: mksolution( |
| "foo 2.0.0", |
| "bar 2.0.0", |
| "baz 2.0.0", |
| ), |
| maxAttempts: 4, |
| }, |
| "locked atoms are matched on both local and net name": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo *"), |
| mkDepspec("foo 1.0.0 foorev"), |
| mkDepspec("foo 2.0.0 foorev2"), |
| }, |
| l: mklock( |
| "foo from baz 1.0.0 foorev", |
| ), |
| r: mksolution( |
| "foo 2.0.0 foorev2", |
| ), |
| }, |
| // This fixture describes a situation that should be impossible with a |
| // real-world VCS (contents of dep at same rev are different, as indicated |
| // by different constraints on bar). But, that's not the SUT here, so it's |
| // OK. |
| "pairs bare revs in lock with all versions": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo ~1.0.1"), |
| 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: mkrevlock( |
| "foo 1.0.1 foorev", // mkrevlock drops the 1.0.1 |
| ), |
| r: mksolution( |
| "foo 1.0.2 foorev", |
| "bar 1.0.2", |
| ), |
| }, |
| "does not pair bare revs in manifest with unpaired lock version": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo ~1.0.1"), |
| mkDepspec("foo 1.0.0", "bar 1.0.0"), |
| mkDepspec("foo 1.0.1 foorev", "bar 1.0.1"), |
| mkDepspec("foo 1.0.2", "bar 1.0.2"), |
| mkDepspec("bar 1.0.0"), |
| mkDepspec("bar 1.0.1"), |
| mkDepspec("bar 1.0.2"), |
| }, |
| l: mkrevlock( |
| "foo 1.0.1 foorev", // mkrevlock drops the 1.0.1 |
| ), |
| r: mksolution( |
| "foo 1.0.2", |
| "bar 1.0.2", |
| ), |
| }, |
| "lock to branch on old rev keeps old rev": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo bmaster"), |
| mkDepspec("foo bmaster newrev"), |
| }, |
| l: mklock( |
| "foo bmaster oldrev", |
| ), |
| r: mksolution( |
| "foo bmaster oldrev", |
| ), |
| }, |
| // Whereas this is a normal situation for a branch, when it occurs for a |
| // tag, it means someone's been naughty upstream. Still, though, the outcome |
| // is the same. |
| // |
| // TODO(sdboyer) this needs to generate a warning, once we start doing that |
| "lock to now-moved tag on old rev keeps old rev": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo ptaggerino"), |
| mkDepspec("foo ptaggerino newrev"), |
| }, |
| l: mklock( |
| "foo ptaggerino oldrev", |
| ), |
| r: mksolution( |
| "foo ptaggerino oldrev", |
| ), |
| }, |
| "no version that matches requirement": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo ^1.0.0"), |
| mkDepspec("foo 2.0.0"), |
| mkDepspec("foo 2.1.3"), |
| }, |
| fail: &noVersionError{ |
| pn: mkPI("foo"), |
| fails: []failedVersion{ |
| { |
| v: NewVersion("2.1.3"), |
| f: &versionNotAllowedFailure{ |
| goal: mkAtom("foo 2.1.3"), |
| failparent: []dependency{mkDep("root", "foo ^1.0.0", "foo")}, |
| c: mkSVC("^1.0.0"), |
| }, |
| }, |
| { |
| v: NewVersion("2.0.0"), |
| f: &versionNotAllowedFailure{ |
| goal: mkAtom("foo 2.0.0"), |
| failparent: []dependency{mkDep("root", "foo ^1.0.0", "foo")}, |
| c: mkSVC("^1.0.0"), |
| }, |
| }, |
| }, |
| }, |
| }, |
| "no version that matches combined constraint": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo 1.0.0", "bar 1.0.0"), |
| mkDepspec("foo 1.0.0", "shared >=2.0.0, <3.0.0"), |
| mkDepspec("bar 1.0.0", "shared >=2.9.0, <4.0.0"), |
| mkDepspec("shared 2.5.0"), |
| mkDepspec("shared 3.5.0"), |
| }, |
| fail: &noVersionError{ |
| pn: mkPI("shared"), |
| fails: []failedVersion{ |
| { |
| v: NewVersion("3.5.0"), |
| f: &versionNotAllowedFailure{ |
| goal: mkAtom("shared 3.5.0"), |
| failparent: []dependency{mkDep("foo 1.0.0", "shared >=2.0.0, <3.0.0", "shared")}, |
| c: mkSVC(">=2.9.0, <3.0.0"), |
| }, |
| }, |
| { |
| v: NewVersion("2.5.0"), |
| f: &versionNotAllowedFailure{ |
| goal: mkAtom("shared 2.5.0"), |
| failparent: []dependency{mkDep("bar 1.0.0", "shared >=2.9.0, <4.0.0", "shared")}, |
| c: mkSVC(">=2.9.0, <3.0.0"), |
| }, |
| }, |
| }, |
| }, |
| }, |
| "disjoint constraints": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo 1.0.0", "bar 1.0.0"), |
| mkDepspec("foo 1.0.0", "shared <=2.0.0"), |
| mkDepspec("bar 1.0.0", "shared >3.0.0"), |
| mkDepspec("shared 2.0.0"), |
| mkDepspec("shared 4.0.0"), |
| }, |
| fail: &noVersionError{ |
| pn: mkPI("foo"), |
| fails: []failedVersion{ |
| { |
| v: NewVersion("1.0.0"), |
| f: &disjointConstraintFailure{ |
| goal: mkDep("foo 1.0.0", "shared <=2.0.0", "shared"), |
| failsib: []dependency{mkDep("bar 1.0.0", "shared >3.0.0", "shared")}, |
| nofailsib: nil, |
| c: mkSVC(">3.0.0"), |
| }, |
| }, |
| }, |
| }, |
| }, |
| "no valid solution": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "a *", "b *"), |
| mkDepspec("a 1.0.0", "b 1.0.0"), |
| mkDepspec("a 2.0.0", "b 2.0.0"), |
| mkDepspec("b 1.0.0", "a 2.0.0"), |
| mkDepspec("b 2.0.0", "a 1.0.0"), |
| }, |
| fail: &noVersionError{ |
| pn: mkPI("b"), |
| fails: []failedVersion{ |
| { |
| v: NewVersion("2.0.0"), |
| f: &versionNotAllowedFailure{ |
| goal: mkAtom("b 2.0.0"), |
| failparent: []dependency{mkDep("a 1.0.0", "b 1.0.0", "b")}, |
| c: mkSVC("1.0.0"), |
| }, |
| }, |
| { |
| v: NewVersion("1.0.0"), |
| f: &constraintNotAllowedFailure{ |
| goal: mkDep("b 1.0.0", "a 2.0.0", "a"), |
| v: NewVersion("1.0.0"), |
| }, |
| }, |
| }, |
| }, |
| }, |
| "no version that matches while backtracking": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "a *", "b >1.0.0"), |
| mkDepspec("a 1.0.0"), |
| mkDepspec("b 1.0.0"), |
| }, |
| fail: &noVersionError{ |
| pn: mkPI("b"), |
| fails: []failedVersion{ |
| { |
| v: NewVersion("1.0.0"), |
| f: &versionNotAllowedFailure{ |
| goal: mkAtom("b 1.0.0"), |
| failparent: []dependency{mkDep("root", "b >1.0.0", "b")}, |
| c: mkSVC(">1.0.0"), |
| }, |
| }, |
| }, |
| }, |
| }, |
| // The latest versions of a and b disagree on c. An older version of either |
| // will resolve the problem. This test validates that b, which is farther |
| // in the dependency graph from myapp is downgraded first. |
| "rolls back leaf versions first": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "a *"), |
| mkDepspec("a 1.0.0", "b *"), |
| mkDepspec("a 2.0.0", "b *", "c 2.0.0"), |
| mkDepspec("b 1.0.0"), |
| mkDepspec("b 2.0.0", "c 1.0.0"), |
| mkDepspec("c 1.0.0"), |
| mkDepspec("c 2.0.0"), |
| }, |
| r: mksolution( |
| "a 2.0.0", |
| "b 1.0.0", |
| "c 2.0.0", |
| ), |
| maxAttempts: 2, |
| }, |
| // Only one version of baz, so foo and bar will have to downgrade until they |
| // reach it. |
| "mutual downgrading": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo *"), |
| mkDepspec("foo 1.0.0", "bar 1.0.0"), |
| mkDepspec("foo 2.0.0", "bar 2.0.0"), |
| mkDepspec("foo 3.0.0", "bar 3.0.0"), |
| mkDepspec("bar 1.0.0", "baz *"), |
| mkDepspec("bar 2.0.0", "baz 2.0.0"), |
| mkDepspec("bar 3.0.0", "baz 3.0.0"), |
| mkDepspec("baz 1.0.0"), |
| }, |
| r: mksolution( |
| "foo 1.0.0", |
| "bar 1.0.0", |
| "baz 1.0.0", |
| ), |
| maxAttempts: 3, |
| }, |
| // Ensures the solver doesn't exhaustively search all versions of b when |
| // it's a-2.0.0 whose dependency on c-2.0.0-nonexistent led to the |
| // problem. We make sure b has more versions than a so that the solver |
| // tries a first since it sorts sibling dependencies by number of |
| // versions. |
| "search real failer": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "a *", "b *"), |
| mkDepspec("a 1.0.0", "c 1.0.0"), |
| mkDepspec("a 2.0.0", "c 2.0.0"), |
| mkDepspec("b 1.0.0"), |
| mkDepspec("b 2.0.0"), |
| mkDepspec("b 3.0.0"), |
| mkDepspec("c 1.0.0"), |
| }, |
| r: mksolution( |
| "a 1.0.0", |
| "b 3.0.0", |
| "c 1.0.0", |
| ), |
| maxAttempts: 2, |
| }, |
| // Dependencies are ordered so that packages with fewer versions are tried |
| // first. Here, there are two valid solutions (either a or b must be |
| // downgraded once). The chosen one depends on which dep is traversed first. |
| // Since b has fewer versions, it will be traversed first, which means a |
| // will come later. Since later selections are revised first, a gets |
| // downgraded. |
| "traverse into package with fewer versions first": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "a *", "b *"), |
| mkDepspec("a 1.0.0", "c *"), |
| mkDepspec("a 2.0.0", "c *"), |
| mkDepspec("a 3.0.0", "c *"), |
| mkDepspec("a 4.0.0", "c *"), |
| mkDepspec("a 5.0.0", "c 1.0.0"), |
| mkDepspec("b 1.0.0", "c *"), |
| mkDepspec("b 2.0.0", "c *"), |
| mkDepspec("b 3.0.0", "c *"), |
| mkDepspec("b 4.0.0", "c 2.0.0"), |
| mkDepspec("c 1.0.0"), |
| mkDepspec("c 2.0.0"), |
| }, |
| r: mksolution( |
| "a 4.0.0", |
| "b 4.0.0", |
| "c 2.0.0", |
| ), |
| maxAttempts: 2, |
| }, |
| // This is similar to the preceding fixture. When getting the number of |
| // versions of a package to determine which to traverse first, versions that |
| // are disallowed by the root package's constraints should not be |
| // considered. Here, foo has more versions than bar in total (4), but fewer |
| // that meet myapp"s constraints (only 2). There is no solution, but we will |
| // do less backtracking if foo is tested first. |
| "root constraints pre-eliminate versions": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo *", "bar *"), |
| mkDepspec("foo 1.0.0", "none 2.0.0"), |
| mkDepspec("foo 2.0.0", "none 2.0.0"), |
| mkDepspec("foo 3.0.0", "none 2.0.0"), |
| mkDepspec("foo 4.0.0", "none 2.0.0"), |
| mkDepspec("bar 1.0.0"), |
| mkDepspec("bar 2.0.0"), |
| mkDepspec("bar 3.0.0"), |
| mkDepspec("none 1.0.0"), |
| }, |
| fail: &noVersionError{ |
| pn: mkPI("none"), |
| fails: []failedVersion{ |
| { |
| v: NewVersion("1.0.0"), |
| f: &versionNotAllowedFailure{ |
| goal: mkAtom("none 1.0.0"), |
| failparent: []dependency{mkDep("foo 1.0.0", "none 2.0.0", "none")}, |
| c: mkSVC("2.0.0"), |
| }, |
| }, |
| }, |
| }, |
| }, |
| // If there"s a disjoint constraint on a package, then selecting other |
| // versions of it is a waste of time: no possible versions can match. We |
| // need to jump past it to the most recent package that affected the |
| // constraint. |
| "backjump past failed package on disjoint constraint": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "a *", "foo *"), |
| mkDepspec("a 1.0.0", "foo *"), |
| mkDepspec("a 2.0.0", "foo <1.0.0"), |
| mkDepspec("foo 2.0.0"), |
| mkDepspec("foo 2.0.1"), |
| mkDepspec("foo 2.0.2"), |
| mkDepspec("foo 2.0.3"), |
| mkDepspec("foo 2.0.4"), |
| mkDepspec("none 1.0.0"), |
| }, |
| r: mksolution( |
| "a 1.0.0", |
| "foo 2.0.4", |
| ), |
| maxAttempts: 2, |
| }, |
| // Revision enters vqueue if a dep has a constraint on that revision |
| "revision injected into vqueue": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo r123abc"), |
| mkDepspec("foo r123abc"), |
| mkDepspec("foo 1.0.0 foorev"), |
| mkDepspec("foo 2.0.0 foorev2"), |
| }, |
| r: mksolution( |
| "foo r123abc", |
| ), |
| }, |
| // Some basic override checks |
| "override root's own constraint": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "a *", "b *"), |
| mkDepspec("a 1.0.0", "b 1.0.0"), |
| mkDepspec("a 2.0.0", "b 1.0.0"), |
| mkDepspec("b 1.0.0"), |
| }, |
| ovr: ProjectConstraints{ |
| ProjectRoot("a"): ProjectProperties{ |
| Constraint: NewVersion("1.0.0"), |
| }, |
| }, |
| r: mksolution( |
| "a 1.0.0", |
| "b 1.0.0", |
| ), |
| }, |
| "override dep's constraint": { |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "a *"), |
| mkDepspec("a 1.0.0", "b 1.0.0"), |
| mkDepspec("a 2.0.0", "b 1.0.0"), |
| mkDepspec("b 1.0.0"), |
| mkDepspec("b 2.0.0"), |
| }, |
| ovr: ProjectConstraints{ |
| ProjectRoot("b"): ProjectProperties{ |
| Constraint: NewVersion("2.0.0"), |
| }, |
| }, |
| r: mksolution( |
| "a 2.0.0", |
| "b 2.0.0", |
| ), |
| }, |
| "overridden mismatched net addrs, alt in dep, back to default": { |
| ds: []depspec{ |
| mkDepspec("root 1.0.0", "foo 1.0.0", "bar 1.0.0"), |
| mkDepspec("foo 1.0.0", "bar from baz 1.0.0"), |
| mkDepspec("bar 1.0.0"), |
| }, |
| ovr: ProjectConstraints{ |
| ProjectRoot("bar"): ProjectProperties{ |
| Source: "bar", |
| }, |
| }, |
| r: mksolution( |
| "foo 1.0.0", |
| "bar from bar 1.0.0", |
| ), |
| }, |
| |
| // TODO(sdboyer) decide how to refactor the solver in order to re-enable these. |
| // Checking for revision existence is important...but kinda obnoxious. |
| //{ |
| //// Solve fails if revision constraint calls for a nonexistent revision |
| //n: "fail on missing revision", |
| //ds: []depspec{ |
| //mkDepspec("root 0.0.0", "bar *"), |
| //mkDepspec("bar 1.0.0", "foo r123abc"), |
| //mkDepspec("foo r123nomatch"), |
| //mkDepspec("foo 1.0.0"), |
| //mkDepspec("foo 2.0.0"), |
| //}, |
| //errp: []string{"bar", "foo", "bar"}, |
| //}, |
| //{ |
| //// Solve fails if revision constraint calls for a nonexistent revision, |
| //// even if rev constraint is specified by root |
| //n: "fail on missing revision from root", |
| //ds: []depspec{ |
| //mkDepspec("root 0.0.0", "foo r123nomatch"), |
| //mkDepspec("foo r123abc"), |
| //mkDepspec("foo 1.0.0"), |
| //mkDepspec("foo 2.0.0"), |
| //}, |
| //errp: []string{"foo", "root", "foo"}, |
| //}, |
| |
| // TODO(sdboyer) add fixture that tests proper handling of loops via aliases (where |
| // a project that wouldn't be a loop is aliased to a project that is a loop) |
| } |
| |
| func init() { |
| // This sets up a hundred versions of foo and bar, 0.0.0 through 9.9.0. Each |
| // version of foo depends on a baz with the same major version. Each version |
| // of bar depends on a baz with the same minor version. There is only one |
| // version of baz, 0.0.0, so only older versions of foo and bar will |
| // satisfy it. |
| fix := basicFixture{ |
| ds: []depspec{ |
| mkDepspec("root 0.0.0", "foo *", "bar *"), |
| mkDepspec("baz 0.0.0"), |
| }, |
| r: mksolution( |
| "foo 0.9.0", |
| "bar 9.0.0", |
| "baz 0.0.0", |
| ), |
| maxAttempts: 10, |
| } |
| |
| for i := 0; i < 10; i++ { |
| for j := 0; j < 10; j++ { |
| fix.ds = append(fix.ds, mkDepspec(fmt.Sprintf("foo %v.%v.0", i, j), fmt.Sprintf("baz %v.0.0", i))) |
| fix.ds = append(fix.ds, mkDepspec(fmt.Sprintf("bar %v.%v.0", i, j), fmt.Sprintf("baz 0.%v.0", j))) |
| } |
| } |
| |
| basicFixtures["complex backtrack"] = fix |
| |
| for k, fix := range basicFixtures { |
| // Assign the name into the fixture itself |
| fix.n = k |
| basicFixtures[k] = fix |
| } |
| } |
| |
| // reachMaps contain externalReach()-type data for a given depspec fixture's |
| // universe of projects, packages, and versions. |
| type reachMap map[pident]map[string][]string |
| |
| type depspecSourceManager struct { |
| specs []depspec |
| rm reachMap |
| ig map[string]bool |
| } |
| |
| type fixSM interface { |
| SourceManager |
| rootSpec() depspec |
| allSpecs() []depspec |
| ignore() map[string]bool |
| } |
| |
| var _ fixSM = &depspecSourceManager{} |
| |
| func newdepspecSM(ds []depspec, ignore []string) *depspecSourceManager { |
| ig := make(map[string]bool) |
| if len(ignore) > 0 { |
| for _, pkg := range ignore { |
| ig[pkg] = true |
| } |
| } |
| |
| return &depspecSourceManager{ |
| specs: ds, |
| rm: computeBasicReachMap(ds), |
| ig: ig, |
| } |
| } |
| |
| func (sm *depspecSourceManager) GetManifestAndLock(id ProjectIdentifier, v Version, an ProjectAnalyzer) (Manifest, Lock, error) { |
| // If the input version is a PairedVersion, look only at its top version, |
| // not the underlying. This is generally consistent with the idea that, for |
| // this class of lookup, the rev probably DOES exist, but upstream changed |
| // it (typically a branch). For the purposes of tests, then, that's an OK |
| // scenario, because otherwise we'd have to enumerate all the revs in the |
| // fixture declarations, which would screw up other things. |
| if pv, ok := v.(PairedVersion); ok { |
| v = pv.Unpair() |
| } |
| |
| src := toFold(id.normalizedSource()) |
| for _, ds := range sm.specs { |
| if src == string(ds.n) && v.Matches(ds.v) { |
| return ds, dummyLock{}, nil |
| } |
| } |
| |
| return nil, nil, fmt.Errorf("project %s at version %s could not be found", id, v) |
| } |
| |
| func (sm *depspecSourceManager) ListPackages(id ProjectIdentifier, v Version) (pkgtree.PackageTree, error) { |
| pid := pident{n: ProjectRoot(toFold(id.normalizedSource())), v: v} |
| if pv, ok := v.(PairedVersion); ok && pv.Revision() == "FAKEREV" { |
| // An empty rev may come in here because that's what we produce in |
| // ListVersions(). If that's what we see, then just pretend like we have |
| // an unpaired. |
| pid.v = pv.Unpair() |
| } |
| |
| if r, exists := sm.rm[pid]; exists { |
| return pkgtree.PackageTree{ |
| ImportRoot: id.normalizedSource(), |
| Packages: map[string]pkgtree.PackageOrErr{ |
| string(pid.n): { |
| P: pkgtree.Package{ |
| ImportPath: string(pid.n), |
| Name: string(pid.n), |
| Imports: r[string(pid.n)], |
| }, |
| }, |
| }, |
| }, nil |
| } |
| |
| // if incoming version was paired, walk the map and search for a match on |
| // top-only version |
| if pv, ok := v.(PairedVersion); ok { |
| uv := pv.Unpair() |
| for pid, r := range sm.rm { |
| if uv.Matches(pid.v) { |
| return pkgtree.PackageTree{ |
| ImportRoot: id.normalizedSource(), |
| Packages: map[string]pkgtree.PackageOrErr{ |
| string(pid.n): { |
| P: pkgtree.Package{ |
| ImportPath: string(pid.n), |
| Name: string(pid.n), |
| Imports: r[string(pid.n)], |
| }, |
| }, |
| }, |
| }, nil |
| } |
| } |
| } |
| |
| return pkgtree.PackageTree{}, fmt.Errorf("project %s at version %s could not be found", pid.n, v) |
| } |
| |
| func (sm *depspecSourceManager) ListVersions(id ProjectIdentifier) ([]PairedVersion, error) { |
| var pvl []PairedVersion |
| src := toFold(id.normalizedSource()) |
| for _, ds := range sm.specs { |
| if src != string(ds.n) { |
| continue |
| } |
| |
| switch tv := ds.v.(type) { |
| case Revision: |
| // To simulate the behavior of the real SourceManager, we do not return |
| // raw revisions from listVersions(). |
| case PairedVersion: |
| pvl = append(pvl, tv) |
| case UnpairedVersion: |
| // Dummy revision; if the fixture doesn't provide it, we know |
| // the test doesn't need revision info, anyway. |
| pvl = append(pvl, tv.Pair(Revision("FAKEREV"))) |
| default: |
| panic(fmt.Sprintf("unreachable: type of version was %#v for spec %s", ds.v, id)) |
| } |
| } |
| |
| if len(pvl) == 0 { |
| return nil, fmt.Errorf("project %s could not be found", id) |
| } |
| return pvl, nil |
| } |
| |
| func (sm *depspecSourceManager) RevisionPresentIn(id ProjectIdentifier, r Revision) (bool, error) { |
| src := toFold(id.normalizedSource()) |
| for _, ds := range sm.specs { |
| if src == string(ds.n) && r == ds.v { |
| return true, nil |
| } |
| } |
| |
| return false, fmt.Errorf("project %s has no revision %s", id, r) |
| } |
| |
| func (sm *depspecSourceManager) SourceExists(id ProjectIdentifier) (bool, error) { |
| src := toFold(id.normalizedSource()) |
| for _, ds := range sm.specs { |
| if src == string(ds.n) { |
| return true, nil |
| } |
| } |
| |
| return false, nil |
| } |
| |
| func (sm *depspecSourceManager) SyncSourceFor(id ProjectIdentifier) error { |
| // Ignore err because it can't happen |
| if exist, _ := sm.SourceExists(id); !exist { |
| return fmt.Errorf("source %s does not exist", id) |
| } |
| return nil |
| } |
| |
| func (sm *depspecSourceManager) Release() {} |
| |
| func (sm *depspecSourceManager) ExportProject(context.Context, ProjectIdentifier, Version, string) error { |
| return fmt.Errorf("dummy sm doesn't support exporting") |
| } |
| |
| func (sm *depspecSourceManager) ExportPrunedProject(context.Context, LockedProject, PruneOptions, string) error { |
| return fmt.Errorf("dummy sm doesn't support exporting") |
| } |
| |
| func (sm *depspecSourceManager) DeduceProjectRoot(ip string) (ProjectRoot, error) { |
| fip := toFold(ip) |
| for _, ds := range sm.allSpecs() { |
| n := string(ds.n) |
| if fip == n || strings.HasPrefix(fip, n+"/") { |
| return ProjectRoot(ip[:len(n)]), nil |
| } |
| } |
| return "", fmt.Errorf("could not find %s, or any parent, in list of known fixtures", ip) |
| } |
| |
| func (sm *depspecSourceManager) SourceURLsForPath(ip string) ([]*url.URL, error) { |
| return nil, fmt.Errorf("dummy sm doesn't implement SourceURLsForPath") |
| } |
| |
| func (sm *depspecSourceManager) rootSpec() depspec { |
| return sm.specs[0] |
| } |
| |
| func (sm *depspecSourceManager) allSpecs() []depspec { |
| return sm.specs |
| } |
| |
| func (sm *depspecSourceManager) ignore() map[string]bool { |
| return sm.ig |
| } |
| |
| // InferConstraint tries to puzzle out what kind of version is given in a string - |
| // semver, a revision, or as a fallback, a plain tag. This current implementation |
| // is a panic because there's no current circumstance under which the depspecSourceManager |
| // is useful outside of the gps solving tests, and it shouldn't be used anywhere else without a conscious and intentional |
| // expansion of its semantics. |
| func (sm *depspecSourceManager) InferConstraint(s string, pi ProjectIdentifier) (Constraint, error) { |
| panic("depsecSourceManager is only for gps solving tests") |
| } |
| |
| type depspecBridge struct { |
| *bridge |
| } |
| |
| func (b *depspecBridge) listVersions(id ProjectIdentifier) ([]Version, error) { |
| if vl, exists := b.vlists[id]; exists { |
| return vl, nil |
| } |
| |
| pvl, err := b.sm.ListVersions(id) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Construct a []Version slice. If any paired versions use the fake rev, |
| // remove the underlying component. |
| vl := make([]Version, 0, len(pvl)) |
| for _, v := range pvl { |
| if v.Revision() == "FAKEREV" { |
| vl = append(vl, v.Unpair()) |
| } else { |
| vl = append(vl, v) |
| } |
| } |
| |
| if b.down { |
| SortForDowngrade(vl) |
| } else { |
| SortForUpgrade(vl) |
| } |
| |
| b.vlists[id] = vl |
| return vl, nil |
| } |
| |
| // override verifyRoot() on bridge to prevent any filesystem interaction |
| func (b *depspecBridge) verifyRootDir(path string) error { |
| root := b.sm.(fixSM).rootSpec() |
| if string(root.n) != path { |
| return fmt.Errorf("expected only root project %q to verifyRootDir(), got %q", root.n, path) |
| } |
| |
| return nil |
| } |
| |
| func (b *depspecBridge) ListPackages(id ProjectIdentifier, v Version) (pkgtree.PackageTree, error) { |
| return b.sm.(fixSM).ListPackages(id, v) |
| } |
| |
| func (b *depspecBridge) vendorCodeExists(id ProjectIdentifier) (bool, error) { |
| return false, nil |
| } |
| |
| // enforce interfaces |
| var _ Manifest = depspec{} |
| var _ Lock = dummyLock{} |
| var _ Lock = fixLock{} |
| |
| // impl Spec interface |
| func (ds depspec) DependencyConstraints() ProjectConstraints { |
| return pcSliceToMap(ds.deps) |
| } |
| |
| type fixLock []LockedProject |
| |
| // impl Lock interface |
| func (l fixLock) Projects() []LockedProject { |
| return l |
| } |
| |
| // impl Lock interface |
| func (fixLock) InputImports() []string { |
| return nil |
| } |
| |
| type dummyLock struct{} |
| |
| // impl Lock interface |
| func (dummyLock) Projects() []LockedProject { |
| return nil |
| } |
| |
| // impl Lock interface |
| func (dummyLock) InputImports() []string { |
| return nil |
| } |