depsfile: Locks.Equal and Locks.Empty methods

These are helper functions to give the installation UI some hints about
whether the lock file has changed so that it can in turn give the user
advice about it. The UI-layer callers of these will follow in a later
commit.
This commit is contained in:
Martin Atkins 2020-10-02 10:45:20 -07:00
parent eb2a027684
commit 4a1b081afb
2 changed files with 165 additions and 0 deletions

View File

@ -91,6 +91,89 @@ func (l *Locks) Sources() map[string][]byte {
return l.sources return l.sources
} }
// Equal returns true if the given Locks represents the same information as
// the receiver.
//
// Equal explicitly _does not_ consider the equality of version constraints
// in the saved locks, because those are saved only as hints to help the UI
// explain what's changed between runs, and are never used as part of
// dependency installation decisions.
func (l *Locks) Equal(other *Locks) bool {
if len(l.providers) != len(other.providers) {
return false
}
for addr, thisLock := range l.providers {
otherLock, ok := other.providers[addr]
if !ok {
return false
}
if thisLock.addr != otherLock.addr {
// It'd be weird to get here because we already looked these up
// by address above.
return false
}
if thisLock.version != otherLock.version {
// Equality rather than "Version.Same" because changes to the
// build metadata are significant for the purpose of this function:
// it's a different package even if it has the same precedence.
return false
}
// Although "hashes" is declared as a slice, it's logically an
// unordered set and so we'll compare it as such.
if len(thisLock.hashes) != len(otherLock.hashes) {
return false
}
found := make(map[getproviders.Hash]int, len(thisLock.hashes))
for _, hash := range thisLock.hashes {
found[hash]++
}
for _, hash := range otherLock.hashes {
found[hash]++
}
for _, count := range found {
if count != 2 {
// It wasn't in both sets, then
return false
}
}
}
// We don't need to worry about providers that are in "other" but not
// in the receiver, because we tested the lengths being equal above.
return true
}
// Empty returns true if the given Locks object contains no actual locks.
//
// UI code might wish to use this to distinguish a lock file being
// written for the first time from subsequent updates to that lock file.
func (l *Locks) Empty() bool {
return len(l.providers) == 0
}
// DeepCopy creates a new Locks that represents the same information as the
// receiver but does not share memory for any parts of the structure that.
// are mutable through methods on Locks.
//
// Note that this does _not_ create deep copies of parts of the structure
// that are technically mutable but are immutable by convention, such as the
// array underlying the slice of version constraints. Callers may mutate the
// resulting data structure only via the direct methods of Locks.
func (l *Locks) DeepCopy() *Locks {
ret := NewLocks()
for addr, lock := range l.providers {
var hashes []getproviders.Hash
if len(lock.hashes) > 0 {
hashes = make([]getproviders.Hash, len(lock.hashes))
copy(hashes, lock.hashes)
}
ret.SetProvider(addr, lock.version, lock.versionConstraints, hashes)
}
return ret
}
// ProviderLock represents lock information for a specific provider. // ProviderLock represents lock information for a specific provider.
type ProviderLock struct { type ProviderLock struct {
// addr is the address of the provider this lock applies to. // addr is the address of the provider this lock applies to.

View File

@ -0,0 +1,82 @@
package depsfile
import (
"testing"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/internal/getproviders"
)
func TestLocksEqual(t *testing.T) {
boopProvider := addrs.NewDefaultProvider("boop")
v2 := getproviders.MustParseVersion("2.0.0")
v2LocalBuild := getproviders.MustParseVersion("2.0.0+awesomecorp.1")
v2GtConstraints := getproviders.MustParseVersionConstraints(">= 2.0.0")
v2EqConstraints := getproviders.MustParseVersionConstraints("2.0.0")
hash1 := getproviders.HashScheme("test").New("1")
hash2 := getproviders.HashScheme("test").New("2")
hash3 := getproviders.HashScheme("test").New("3")
equalBothWays := func(t *testing.T, a, b *Locks) {
t.Helper()
if !a.Equal(b) {
t.Errorf("a should be equal to b")
}
if !b.Equal(a) {
t.Errorf("b should be equal to a")
}
}
nonEqualBothWays := func(t *testing.T, a, b *Locks) {
t.Helper()
if a.Equal(b) {
t.Errorf("a should be equal to b")
}
if b.Equal(a) {
t.Errorf("b should be equal to a")
}
}
t.Run("both empty", func(t *testing.T) {
a := NewLocks()
b := NewLocks()
equalBothWays(t, a, b)
})
t.Run("an extra provider lock", func(t *testing.T) {
a := NewLocks()
b := NewLocks()
b.SetProvider(boopProvider, v2, v2GtConstraints, nil)
nonEqualBothWays(t, a, b)
})
t.Run("both have boop provider with same version", func(t *testing.T) {
a := NewLocks()
b := NewLocks()
// Note: the constraints are not part of the definition of "Equal", so they can differ
a.SetProvider(boopProvider, v2, v2GtConstraints, nil)
b.SetProvider(boopProvider, v2, v2EqConstraints, nil)
equalBothWays(t, a, b)
})
t.Run("both have boop provider with different versions", func(t *testing.T) {
a := NewLocks()
b := NewLocks()
a.SetProvider(boopProvider, v2, v2EqConstraints, nil)
b.SetProvider(boopProvider, v2LocalBuild, v2EqConstraints, nil)
nonEqualBothWays(t, a, b)
})
t.Run("both have boop provider with same version and same hashes", func(t *testing.T) {
a := NewLocks()
b := NewLocks()
hashes := []getproviders.Hash{hash1, hash2, hash3}
a.SetProvider(boopProvider, v2, v2EqConstraints, hashes)
b.SetProvider(boopProvider, v2, v2EqConstraints, hashes)
equalBothWays(t, a, b)
})
t.Run("both have boop provider with same version but different hashes", func(t *testing.T) {
a := NewLocks()
b := NewLocks()
hashesA := []getproviders.Hash{hash1, hash2}
hashesB := []getproviders.Hash{hash1, hash3}
a.SetProvider(boopProvider, v2, v2EqConstraints, hashesA)
b.SetProvider(boopProvider, v2, v2EqConstraints, hashesB)
nonEqualBothWays(t, a, b)
})
}