You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
terraform/internal/command/meta_dependencies_test.go

152 lines
5.7 KiB

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"testing"
"github.com/apparentlymart/go-versions/versions"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/cli"
tfaddr "github.com/hashicorp/terraform-registry-address"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
)
// This tests combining locks from config and state. Locks derived from state are always unconstrained, i.e. no version constraint data,
// so this test
func Test_mergeLockedDependencies_config_and_state(t *testing.T) {
providerA := tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "my-org", "providerA")
providerB := tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "my-org", "providerB")
v1_0_0 := providerreqs.MustParseVersion("1.0.0")
versionConstraintv1, _ := providerreqs.ParseVersionConstraints("1.0.0")
hashesProviderA := []providerreqs.Hash{providerreqs.MustParseHash("providerA:this-is-providerA")}
hashesProviderB := []providerreqs.Hash{providerreqs.MustParseHash("providerB:this-is-providerB")}
var versionUnconstrained providerreqs.VersionConstraints = nil
noVersion := versions.Version{}
cases := map[string]struct {
configLocks *depsfile.Locks
stateLocks *depsfile.Locks
expectedLocks *depsfile.Locks
}{
"no locks when all inputs empty": {
configLocks: depsfile.NewLocks(),
stateLocks: depsfile.NewLocks(),
expectedLocks: depsfile.NewLocks(),
},
"when provider only described in config, output locks have matching constraints": {
configLocks: func() *depsfile.Locks {
configLocks := depsfile.NewLocks()
configLocks.SetProvider(providerA, v1_0_0, versionConstraintv1, hashesProviderA)
return configLocks
}(),
stateLocks: depsfile.NewLocks(),
expectedLocks: func() *depsfile.Locks {
combinedLocks := depsfile.NewLocks()
combinedLocks.SetProvider(providerA, v1_0_0, versionConstraintv1, hashesProviderA)
return combinedLocks
}(),
},
"when provider only described in state, output locks are unconstrained": {
configLocks: depsfile.NewLocks(),
stateLocks: func() *depsfile.Locks {
stateLocks := depsfile.NewLocks()
stateLocks.SetProvider(providerA, noVersion, versionUnconstrained, hashesProviderA)
return stateLocks
}(),
expectedLocks: func() *depsfile.Locks {
combinedLocks := depsfile.NewLocks()
combinedLocks.SetProvider(providerA, noVersion, versionUnconstrained, hashesProviderA)
return combinedLocks
}(),
},
"different providers present in state and config are combined, with version constraints kept on config providers": {
configLocks: func() *depsfile.Locks {
configLocks := depsfile.NewLocks()
configLocks.SetProvider(providerA, v1_0_0, versionConstraintv1, hashesProviderA)
return configLocks
}(),
stateLocks: func() *depsfile.Locks {
stateLocks := depsfile.NewLocks()
// Imagine that the state locks contain:
// 1) provider for resources in the config
stateLocks.SetProvider(providerA, noVersion, versionUnconstrained, hashesProviderA)
// 2) also, a provider that's deleted from the config and only present in state
stateLocks.SetProvider(providerB, noVersion, versionUnconstrained, hashesProviderB)
return stateLocks
}(),
expectedLocks: func() *depsfile.Locks {
combinedLocks := depsfile.NewLocks()
combinedLocks.SetProvider(providerA, v1_0_0, versionConstraintv1, hashesProviderA) // version constraint preserved
combinedLocks.SetProvider(providerB, noVersion, versionUnconstrained, hashesProviderB) // sourced from state only
return combinedLocks
}(),
},
}
for tn, tc := range cases {
t.Run(tn, func(t *testing.T) {
// Use tmp dir as we're creating lock files in the test
td := t.TempDir()
t.Chdir(td)
ui := new(cli.MockUi)
view, _ := testView(t)
m := Meta{
Ui: ui,
View: view,
}
// Code under test - combine deps from state with prior deps from config
// mergedLocks := m.mergeLockedDependencies(tc.configLocks, tc.stateLocks)
mergedLocks := m.mergeLockedDependencies(tc.configLocks, tc.stateLocks)
// We cannot use (l *depsfile.Locks) Equal here as it doesn't compare version constraints
// Instead, inspect entries directly
if len(mergedLocks.AllProviders()) != len(tc.expectedLocks.AllProviders()) {
t.Fatalf("expected merged dependencies to include %d providers, but got %d:\n %#v",
len(tc.expectedLocks.AllProviders()),
len(mergedLocks.AllProviders()),
mergedLocks.AllProviders(),
)
}
for _, lock := range tc.expectedLocks.AllProviders() {
match := mergedLocks.Provider(lock.Provider())
if match == nil {
t.Fatalf("expected merged dependencies to include provider %s, but it's missing", lock.Provider())
}
if len(match.VersionConstraints()) != len(lock.VersionConstraints()) {
t.Fatalf("detected a problem with version constraints for provider %s, got: %d, want %d",
lock.Provider(),
len(match.VersionConstraints()),
len(lock.VersionConstraints()),
)
}
if len(match.VersionConstraints()) > 0 && len(lock.VersionConstraints()) > 0 {
gotConstraints := match.VersionConstraints()[0]
wantConstraints := lock.VersionConstraints()[0]
if gotConstraints.Boundary.String() != wantConstraints.Boundary.String() {
t.Fatalf("expected merged dependencies to include provider %s with version constraint %v, but instead got %v",
lock.Provider(),
gotConstraints.Boundary.String(),
wantConstraints.Boundary.String(),
)
}
}
}
if diff := cmp.Diff(tc.expectedLocks, mergedLocks); diff != "" {
t.Errorf("difference in file contents detected\n%s", diff)
}
})
}
}