blob: 0c270cbda631e335fa825467723e7fc48e5c40e0 [file] [log] [blame]
// 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 (
"fmt"
"math/bits"
"strings"
"testing"
"github.com/golang/dep/gps"
)
func contains(haystack []string, needle string) bool {
for _, str := range haystack {
if str == needle {
return true
}
}
return false
}
func (dd DeltaDimension) String() string {
var parts []string
for dd != 0 {
index := bits.TrailingZeros32(uint32(dd))
dd &= ^(1 << uint(index))
switch DeltaDimension(1 << uint(index)) {
case InputImportsChanged:
parts = append(parts, "input imports")
case ProjectAdded:
parts = append(parts, "project added")
case ProjectRemoved:
parts = append(parts, "project removed")
case SourceChanged:
parts = append(parts, "source changed")
case VersionChanged:
parts = append(parts, "version changed")
case RevisionChanged:
parts = append(parts, "revision changed")
case PackagesChanged:
parts = append(parts, "packages changed")
case PruneOptsChanged:
parts = append(parts, "pruneopts changed")
case HashVersionChanged:
parts = append(parts, "hash version changed")
case HashChanged:
parts = append(parts, "hash digest changed")
}
}
return strings.Join(parts, ", ")
}
func TestLockDelta(t *testing.T) {
fooversion := gps.NewVersion("v1.0.0").Pair("foorev1")
bazversion := gps.NewVersion("v2.0.0").Pair("bazrev1")
transver := gps.NewVersion("v0.5.0").Pair("transrev1")
l := safeLock{
i: []string{"foo.com/bar", "baz.com/qux"},
p: []gps.LockedProject{
newVerifiableProject(mkPI("foo.com/bar"), fooversion, []string{".", "subpkg"}),
newVerifiableProject(mkPI("baz.com/qux"), bazversion, []string{".", "other"}),
newVerifiableProject(mkPI("transitive.com/dependency"), transver, []string{"."}),
},
}
var dup lockTransformer = func(l safeLock) safeLock {
return l.dup()
}
tt := map[string]struct {
lt lockTransformer
delta DeltaDimension
checkfn func(*testing.T, LockDelta)
}{
"ident": {
lt: dup,
},
"added import": {
lt: dup.addII("other.org"),
delta: InputImportsChanged,
},
"added import 2x": {
lt: dup.addII("other.org").addII("andsomethingelse.com/wowie"),
delta: InputImportsChanged,
checkfn: func(t *testing.T, ld LockDelta) {
if !contains(ld.AddedImportInputs, "other.org") {
t.Error("first added input import missing")
}
if !contains(ld.AddedImportInputs, "andsomethingelse.com/wowie") {
t.Error("first added input import missing")
}
},
},
"removed import": {
lt: dup.rmII("baz.com/qux"),
delta: InputImportsChanged,
checkfn: func(t *testing.T, ld LockDelta) {
if !contains(ld.RemovedImportInputs, "baz.com/qux") {
t.Error("removed input import missing")
}
},
},
"add project": {
lt: dup.addDumbProject("madeup.org"),
delta: ProjectAdded,
},
"remove project": {
lt: dup.rmProject("foo.com/bar"),
delta: ProjectRemoved,
},
"remove last project": {
lt: dup.rmProject("transitive.com/dependency"),
delta: ProjectRemoved,
},
"all": {
lt: dup.addII("other.org").rmII("baz.com/qux").addDumbProject("zebrafun.org").rmProject("foo.com/bar"),
delta: InputImportsChanged | ProjectRemoved | ProjectAdded,
},
"remove all projects and imports": {
lt: dup.rmII("baz.com/qux").rmII("foo.com/bar").rmProject("baz.com/qux").rmProject("foo.com/bar").rmProject("transitive.com/dependency"),
delta: InputImportsChanged | ProjectRemoved,
},
}
for name, fix := range tt {
fix := fix
t.Run(name, func(t *testing.T) {
fixl := fix.lt(l)
ld := DiffLocks(l, fixl)
if !ld.Changed(AnyChanged) && fix.delta != 0 {
t.Errorf("Changed() reported false when expecting some dimensions to be changed: %s", fix.delta)
} else if ld.Changed(AnyChanged) && fix.delta == 0 {
t.Error("Changed() reported true when expecting no changes")
}
if ld.Changed(AnyChanged & ^fix.delta) {
t.Errorf("Changed() reported true when checking along not-expected dimensions: %s", ld.Changes() & ^fix.delta)
}
gotdelta := ld.Changes()
if fix.delta & ^gotdelta != 0 {
t.Errorf("wanted change in some dimensions that were unchanged: %s", fix.delta & ^gotdelta)
}
if gotdelta & ^fix.delta != 0 {
t.Errorf("did not want change in some dimensions that were changed: %s", gotdelta & ^fix.delta)
}
if fix.checkfn != nil {
fix.checkfn(t, ld)
}
})
}
}
func TestLockedProjectPropertiesDelta(t *testing.T) {
fooversion, foorev := gps.NewVersion("v1.0.0"), gps.Revision("foorev1")
foopair := fooversion.Pair(foorev)
foovp := VerifiableProject{
LockedProject: gps.NewLockedProject(mkPI("foo.com/project"), foopair, []string{".", "subpkg"}),
PruneOpts: gps.PruneNestedVendorDirs,
Digest: VersionedDigest{
HashVersion: HashVersion,
Digest: []byte("foobytes"),
},
}
var dup lockedProjectTransformer = func(lp gps.LockedProject) gps.LockedProject {
return lp.(VerifiableProject).dup()
}
tt := map[string]struct {
lt1, lt2 lockedProjectTransformer
delta DeltaDimension
checkfn func(*testing.T, LockedProjectPropertiesDelta)
}{
"ident": {
lt1: dup,
},
"add pkg": {
lt1: dup.addPkg("whatev"),
delta: PackagesChanged,
},
"rm pkg": {
lt1: dup.rmPkg("subpkg"),
delta: PackagesChanged,
},
"add and rm pkg": {
lt1: dup.rmPkg("subpkg").addPkg("whatev"),
delta: PackagesChanged,
checkfn: func(t *testing.T, ld LockedProjectPropertiesDelta) {
if !contains(ld.PackagesAdded, "whatev") {
t.Error("added pkg missing from list")
}
if !contains(ld.PackagesRemoved, "subpkg") {
t.Error("removed pkg missing from list")
}
},
},
"add source": {
lt1: dup.setSource("somethingelse"),
delta: SourceChanged,
},
"remove source": {
lt1: dup.setSource("somethingelse"),
lt2: dup,
delta: SourceChanged,
},
"to rev only": {
lt1: dup.setVersion(foorev),
delta: VersionChanged,
},
"from rev only": {
lt1: dup.setVersion(foorev),
lt2: dup,
delta: VersionChanged,
},
"to new rev only": {
lt1: dup.setVersion(gps.Revision("newrev")),
delta: VersionChanged | RevisionChanged,
},
"from new rev only": {
lt1: dup.setVersion(gps.Revision("newrev")),
lt2: dup,
delta: VersionChanged | RevisionChanged,
},
"version change": {
lt1: dup.setVersion(gps.NewVersion("v0.5.0").Pair(foorev)),
delta: VersionChanged,
},
"version change to norev": {
lt1: dup.setVersion(gps.NewVersion("v0.5.0")),
delta: VersionChanged | RevisionChanged,
},
"version change from norev": {
lt1: dup.setVersion(gps.NewVersion("v0.5.0")),
lt2: dup.setVersion(gps.NewVersion("v0.5.0").Pair(foorev)),
delta: RevisionChanged,
},
"to branch": {
lt1: dup.setVersion(gps.NewBranch("master").Pair(foorev)),
delta: VersionChanged,
},
"to branch new rev": {
lt1: dup.setVersion(gps.NewBranch("master").Pair(gps.Revision("newrev"))),
delta: VersionChanged | RevisionChanged,
},
"to empty prune opts": {
lt1: dup.setPruneOpts(0),
delta: PruneOptsChanged,
},
"from empty prune opts": {
lt1: dup.setPruneOpts(0),
lt2: dup,
delta: PruneOptsChanged,
},
"prune opts change": {
lt1: dup.setPruneOpts(gps.PruneNestedVendorDirs | gps.PruneNonGoFiles),
delta: PruneOptsChanged,
},
"empty digest": {
lt1: dup.setDigest(VersionedDigest{}),
delta: HashVersionChanged | HashChanged,
},
"to empty digest": {
lt1: dup.setDigest(VersionedDigest{}),
lt2: dup,
delta: HashVersionChanged | HashChanged,
},
"hash version changed": {
lt1: dup.setDigest(VersionedDigest{HashVersion: HashVersion + 1, Digest: []byte("foobytes")}),
delta: HashVersionChanged,
},
"hash contents changed": {
lt1: dup.setDigest(VersionedDigest{HashVersion: HashVersion, Digest: []byte("barbytes")}),
delta: HashChanged,
},
"to plain locked project": {
lt1: dup.toPlainLP(),
delta: PruneOptsChanged | HashChanged | HashVersionChanged,
},
"from plain locked project": {
lt1: dup.toPlainLP(),
lt2: dup,
delta: PruneOptsChanged | HashChanged | HashVersionChanged,
},
"all": {
lt1: dup.setDigest(VersionedDigest{}).setVersion(gps.NewBranch("master").Pair(gps.Revision("newrev"))).setPruneOpts(gps.PruneNestedVendorDirs | gps.PruneNonGoFiles).setSource("whatever"),
delta: SourceChanged | VersionChanged | RevisionChanged | PruneOptsChanged | HashChanged | HashVersionChanged,
},
}
for name, fix := range tt {
fix := fix
t.Run(name, func(t *testing.T) {
// Use two patterns for constructing locks to compare: if only lt1
// is set, use foovp as the first lp and compare with the lt1
// transforms applied. If lt2 is set, transform foovp with lt1 for
// the first lp, then transform foovp with lt2 for the second lp.
var lp1, lp2 gps.LockedProject
if fix.lt2 == nil {
lp1 = foovp
lp2 = fix.lt1(foovp)
} else {
lp1 = fix.lt1(foovp)
lp2 = fix.lt2(foovp)
}
lppd := DiffLockedProjectProperties(lp1, lp2)
if !lppd.Changed(AnyChanged) && fix.delta != 0 {
t.Errorf("Changed() reporting false when expecting some dimensions to be changed: %s", fix.delta)
} else if lppd.Changed(AnyChanged) && fix.delta == 0 {
t.Error("Changed() reporting true when expecting no changes")
}
if lppd.Changed(AnyChanged & ^fix.delta) {
t.Errorf("Changed() reported true when checking along not-expected dimensions: %s", lppd.Changes() & ^fix.delta)
}
gotdelta := lppd.Changes()
if fix.delta & ^gotdelta != 0 {
t.Errorf("wanted change in some dimensions that were unchanged: %s", fix.delta & ^gotdelta)
}
if gotdelta & ^fix.delta != 0 {
t.Errorf("did not want change in some dimensions that were changed: %s", gotdelta & ^fix.delta)
}
if fix.checkfn != nil {
fix.checkfn(t, lppd)
}
})
}
}
type lockTransformer func(safeLock) safeLock
func (lt lockTransformer) compose(lt2 lockTransformer) lockTransformer {
if lt == nil {
return lt2
}
return func(l safeLock) safeLock {
return lt2(lt(l))
}
}
func (lt lockTransformer) addDumbProject(root string) lockTransformer {
vp := newVerifiableProject(mkPI(root), gps.NewVersion("whatever").Pair("addedrev"), []string{"."})
return lt.compose(func(l safeLock) safeLock {
for _, lp := range l.p {
if lp.Ident().ProjectRoot == vp.Ident().ProjectRoot {
panic(fmt.Sprintf("%q already in lock", vp.Ident().ProjectRoot))
}
}
l.p = append(l.p, vp)
return l
})
}
func (lt lockTransformer) rmProject(pr string) lockTransformer {
return lt.compose(func(l safeLock) safeLock {
for k, lp := range l.p {
if lp.Ident().ProjectRoot == gps.ProjectRoot(pr) {
l.p = l.p[:k+copy(l.p[k:], l.p[k+1:])]
return l
}
}
panic(fmt.Sprintf("%q not in lock", pr))
})
}
func (lt lockTransformer) addII(path string) lockTransformer {
return lt.compose(func(l safeLock) safeLock {
for _, impath := range l.i {
if path == impath {
panic(fmt.Sprintf("%q already in input imports", impath))
}
}
l.i = append(l.i, path)
return l
})
}
func (lt lockTransformer) rmII(path string) lockTransformer {
return lt.compose(func(l safeLock) safeLock {
for k, impath := range l.i {
if path == impath {
l.i = l.i[:k+copy(l.i[k:], l.i[k+1:])]
return l
}
}
panic(fmt.Sprintf("%q not in input imports", path))
})
}
type lockedProjectTransformer func(gps.LockedProject) gps.LockedProject
func (lpt lockedProjectTransformer) compose(lpt2 lockedProjectTransformer) lockedProjectTransformer {
if lpt == nil {
return lpt2
}
return func(lp gps.LockedProject) gps.LockedProject {
return lpt2(lpt(lp))
}
}
func (lpt lockedProjectTransformer) addPkg(path string) lockedProjectTransformer {
return lpt.compose(func(lp gps.LockedProject) gps.LockedProject {
for _, pkg := range lp.Packages() {
if path == pkg {
panic(fmt.Sprintf("%q already in pkg list", path))
}
}
nlp := gps.NewLockedProject(lp.Ident(), lp.Version(), append(lp.Packages(), path))
if vp, ok := lp.(VerifiableProject); ok {
vp.LockedProject = nlp
return vp
}
return nlp
})
}
func (lpt lockedProjectTransformer) rmPkg(path string) lockedProjectTransformer {
return lpt.compose(func(lp gps.LockedProject) gps.LockedProject {
pkglist := lp.Packages()
for k, pkg := range pkglist {
if path == pkg {
pkglist = pkglist[:k+copy(pkglist[k:], pkglist[k+1:])]
nlp := gps.NewLockedProject(lp.Ident(), lp.Version(), pkglist)
if vp, ok := lp.(VerifiableProject); ok {
vp.LockedProject = nlp
return vp
}
return nlp
}
}
panic(fmt.Sprintf("%q not in pkg list", path))
})
}
func (lpt lockedProjectTransformer) setSource(source string) lockedProjectTransformer {
return lpt.compose(func(lp gps.LockedProject) gps.LockedProject {
ident := lp.Ident()
ident.Source = source
nlp := gps.NewLockedProject(ident, lp.Version(), lp.Packages())
if vp, ok := lp.(VerifiableProject); ok {
vp.LockedProject = nlp
return vp
}
return nlp
})
}
func (lpt lockedProjectTransformer) setVersion(v gps.Version) lockedProjectTransformer {
return lpt.compose(func(lp gps.LockedProject) gps.LockedProject {
nlp := gps.NewLockedProject(lp.Ident(), v, lp.Packages())
if vp, ok := lp.(VerifiableProject); ok {
vp.LockedProject = nlp
return vp
}
return nlp
})
}
func (lpt lockedProjectTransformer) setPruneOpts(po gps.PruneOptions) lockedProjectTransformer {
return lpt.compose(func(lp gps.LockedProject) gps.LockedProject {
vp := lp.(VerifiableProject)
vp.PruneOpts = po
return vp
})
}
func (lpt lockedProjectTransformer) setDigest(vd VersionedDigest) lockedProjectTransformer {
return lpt.compose(func(lp gps.LockedProject) gps.LockedProject {
vp := lp.(VerifiableProject)
vp.Digest = vd
return vp
})
}
func (lpt lockedProjectTransformer) toPlainLP() lockedProjectTransformer {
return lpt.compose(func(lp gps.LockedProject) gps.LockedProject {
if vp, ok := lp.(VerifiableProject); ok {
return vp.LockedProject
}
return lp
})
}