From 4a1b081afb5e187fa3c6d082a5de9bfa15f9c191 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 2 Oct 2020 10:45:20 -0700 Subject: [PATCH] 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. --- internal/depsfile/locks.go | 83 +++++++++++++++++++++++++++++++++ internal/depsfile/locks_test.go | 82 ++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 internal/depsfile/locks_test.go diff --git a/internal/depsfile/locks.go b/internal/depsfile/locks.go index 34b34d99e..401789766 100644 --- a/internal/depsfile/locks.go +++ b/internal/depsfile/locks.go @@ -91,6 +91,89 @@ func (l *Locks) Sources() map[string][]byte { 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. type ProviderLock struct { // addr is the address of the provider this lock applies to. diff --git a/internal/depsfile/locks_test.go b/internal/depsfile/locks_test.go new file mode 100644 index 000000000..1723113f9 --- /dev/null +++ b/internal/depsfile/locks_test.go @@ -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) + }) +}