opentofu/config/module/tree_test.go
Martin Atkins ba0514106a return tfdiags.Diagnostics from validation methods
Validation is the best time to return detailed diagnostics
to the user since we're much more likely to have source
location information, etc than we are in later operations.

This change doesn't actually add any detail to the messages
yet, but it changes the interface so that we can gradually
introduce more detailed diagnostics over time.

While here there are some minor adjustments to some of the
messages to improve their consistency with terminology we
use elsewhere.
2017-11-28 11:15:29 -08:00

676 lines
15 KiB
Go

package module
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/helper/copy"
)
func TestTreeChild(t *testing.T) {
var nilTree *Tree
if nilTree.Child(nil) != nil {
t.Fatal("child should be nil")
}
storage := testStorage(t, nil)
storage.Mode = GetModeGet
tree := NewTree("", testConfig(t, "child"))
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
// Should be able to get the root child
if c := tree.Child([]string{}); c == nil {
t.Fatal("should not be nil")
} else if c.Name() != "root" {
t.Fatalf("bad: %#v", c.Name())
} else if !reflect.DeepEqual(c.Path(), []string(nil)) {
t.Fatalf("bad: %#v", c.Path())
}
// Should be able to get the root child
if c := tree.Child(nil); c == nil {
t.Fatal("should not be nil")
} else if c.Name() != "root" {
t.Fatalf("bad: %#v", c.Name())
} else if !reflect.DeepEqual(c.Path(), []string(nil)) {
t.Fatalf("bad: %#v", c.Path())
}
// Should be able to get the foo child
if c := tree.Child([]string{"foo"}); c == nil {
t.Fatal("should not be nil")
} else if c.Name() != "foo" {
t.Fatalf("bad: %#v", c.Name())
} else if !reflect.DeepEqual(c.Path(), []string{"foo"}) {
t.Fatalf("bad: %#v", c.Path())
}
// Should be able to get the nested child
if c := tree.Child([]string{"foo", "bar"}); c == nil {
t.Fatal("should not be nil")
} else if c.Name() != "bar" {
t.Fatalf("bad: %#v", c.Name())
} else if !reflect.DeepEqual(c.Path(), []string{"foo", "bar"}) {
t.Fatalf("bad: %#v", c.Path())
}
}
func TestTreeLoad(t *testing.T) {
storage := testStorage(t, nil)
tree := NewTree("", testConfig(t, "basic"))
if tree.Loaded() {
t.Fatal("should not be loaded")
}
// This should error because we haven't gotten things yet
if err := tree.Load(storage); err == nil {
t.Fatal("should error")
}
if tree.Loaded() {
t.Fatal("should not be loaded")
}
// This should get things
storage.Mode = GetModeGet
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
if !tree.Loaded() {
t.Fatal("should be loaded")
}
// This should no longer error
storage.Mode = GetModeNone
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(tree.String())
expected := strings.TrimSpace(treeLoadStr)
if actual != expected {
t.Fatalf("bad: \n\n%s", actual)
}
}
func TestTreeLoad_duplicate(t *testing.T) {
storage := testStorage(t, nil)
tree := NewTree("", testConfig(t, "dup"))
if tree.Loaded() {
t.Fatal("should not be loaded")
}
// This should get things
storage.Mode = GetModeGet
if err := tree.Load(storage); err == nil {
t.Fatalf("should error")
}
}
func TestTreeLoad_copyable(t *testing.T) {
dir := tempDir(t)
storage := &Storage{
StorageDir: dir,
Mode: GetModeGet,
}
cfg := testConfig(t, "basic")
tree := NewTree("", cfg)
// This should get things
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
if !tree.Loaded() {
t.Fatal("should be loaded")
}
// This should no longer error
storage.Mode = GetModeNone
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
// Now we copy the directory, this COPIES symlink values, and
// doesn't create symlinks themselves. That is important.
dir2 := tempDir(t)
os.RemoveAll(dir2)
defer os.RemoveAll(dir2)
if err := copy.CopyDir(dir, dir2); err != nil {
t.Fatalf("err: %s", err)
}
// Now copy the configuration
cfgDir := tempDir(t)
os.RemoveAll(cfgDir)
defer os.RemoveAll(cfgDir)
if err := copy.CopyDir(cfg.Dir, cfgDir); err != nil {
t.Fatalf("err: %s", err)
}
{
cfg, err := config.LoadDir(cfgDir)
if err != nil {
t.Fatalf("err: %s", err)
}
tree := NewTree("", cfg)
storage := &Storage{
StorageDir: dir2,
Mode: GetModeNone,
}
// This should not error since we already got it!
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
if !tree.Loaded() {
t.Fatal("should be loaded")
}
}
}
func TestTreeLoad_parentRef(t *testing.T) {
storage := testStorage(t, nil)
tree := NewTree("", testConfig(t, "basic-parent"))
if tree.Loaded() {
t.Fatal("should not be loaded")
}
// This should error because we haven't gotten things yet
storage.Mode = GetModeNone
if err := tree.Load(storage); err == nil {
t.Fatal("should error")
}
if tree.Loaded() {
t.Fatal("should not be loaded")
}
// This should get things
storage.Mode = GetModeGet
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
if !tree.Loaded() {
t.Fatal("should be loaded")
}
// This should no longer error
storage.Mode = GetModeNone
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(tree.String())
expected := strings.TrimSpace(treeLoadParentStr)
if actual != expected {
t.Fatalf("bad: \n\n%s", actual)
}
}
func TestTreeLoad_subdir(t *testing.T) {
fixtures := []string{
"basic-subdir",
"basic-tar-subdir",
"tar-subdir-to-parent",
}
for _, tc := range fixtures {
t.Run(tc, func(t *testing.T) {
storage := testStorage(t, nil)
tree := NewTree("", testConfig(t, tc))
if tree.Loaded() {
t.Fatal("should not be loaded")
}
// This should error because we haven't gotten things yet
storage.Mode = GetModeNone
if err := tree.Load(storage); err == nil {
t.Fatal("should error")
}
if tree.Loaded() {
t.Fatal("should not be loaded")
}
// This should get things
storage.Mode = GetModeGet
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
if !tree.Loaded() {
t.Fatal("should be loaded")
}
// This should no longer error
storage.Mode = GetModeNone
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(tree.String())
expected := strings.TrimSpace(treeLoadSubdirStr)
if actual != expected {
t.Fatalf("bad: \n\n%s", actual)
}
})
}
}
func TestTree_recordManifest(t *testing.T) {
td, err := ioutil.TempDir("", "tf-module")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(td)
storage := Storage{StorageDir: td}
dir := filepath.Join(td, "0131bf0fef686e090b16bdbab4910ddf")
subDir := "subDirName"
// record and read the subdir path
if err := storage.recordModuleRoot(dir, subDir); err != nil {
t.Fatal(err)
}
actual, err := storage.getModuleRoot(dir)
if err != nil {
t.Fatal(err)
}
if actual != subDir {
t.Fatalf("expected subDir %q, got %q", subDir, actual)
}
// overwrite the path, and nmake sure we get the new one
subDir = "newSubDir"
if err := storage.recordModuleRoot(dir, subDir); err != nil {
t.Fatal(err)
}
actual, err = storage.getModuleRoot(dir)
if err != nil {
t.Fatal(err)
}
if actual != subDir {
t.Fatalf("expected subDir %q, got %q", subDir, actual)
}
// create a fake entry
if err := ioutil.WriteFile(filepath.Join(td, manifestName), []byte("BAD DATA"), 0644); err != nil {
t.Fatal(err)
}
// this should fail because there aare now 2 entries
actual, err = storage.getModuleRoot(dir)
if err == nil {
t.Fatal("expected multiple subdir entries")
}
// writing the subdir entry should remove the incorrect value
if err := storage.recordModuleRoot(dir, subDir); err != nil {
t.Fatal(err)
}
actual, err = storage.getModuleRoot(dir)
if err != nil {
t.Fatal(err)
}
if actual != subDir {
t.Fatalf("expected subDir %q, got %q", subDir, actual)
}
}
func TestTreeModules(t *testing.T) {
tree := NewTree("", testConfig(t, "basic"))
actual := tree.Modules()
expected := []*Module{
&Module{Name: "foo", Source: "./foo"},
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestTreeName(t *testing.T) {
tree := NewTree("", testConfig(t, "basic"))
actual := tree.Name()
if actual != RootName {
t.Fatalf("bad: %#v", actual)
}
}
// This is a table-driven test for tree validation. This is the preferred
// way to test Validate. Non table-driven tests exist historically but
// that style shouldn't be done anymore.
func TestTreeValidate_table(t *testing.T) {
cases := []struct {
Name string
Fixture string
Err string
}{
{
"provider alias in child",
"validate-alias-good",
"",
},
{
"undefined provider alias in child",
"validate-alias-bad",
"alias must be defined",
},
{
"root module named root",
"validate-module-root",
"cannot contain module",
},
{
"grandchild module named root",
"validate-module-root-grandchild",
"",
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
tree := NewTree("", testConfig(t, tc.Fixture))
storage := testStorage(t, nil)
storage.Mode = GetModeGet
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
diags := tree.Validate()
if (diags.HasErrors()) != (tc.Err != "") {
t.Fatalf("err: %s", diags.Err())
}
if len(diags) == 0 {
return
}
if !strings.Contains(diags.Err().Error(), tc.Err) {
t.Fatalf("err should contain %q: %s", tc.Err, diags.Err().Error())
}
})
}
}
func TestTreeValidate_badChild(t *testing.T) {
tree := NewTree("", testConfig(t, "validate-child-bad"))
storage := testStorage(t, nil)
storage.Mode = GetModeGet
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
if err := tree.Validate(); err == nil {
t.Fatal("should error")
}
}
func TestTreeValidate_badChildOutput(t *testing.T) {
tree := NewTree("", testConfig(t, "validate-bad-output"))
storage := testStorage(t, nil)
storage.Mode = GetModeGet
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
if err := tree.Validate(); err == nil {
t.Fatal("should error")
}
}
func TestTreeValidate_badChildOutputToModule(t *testing.T) {
tree := NewTree("", testConfig(t, "validate-bad-output-to-module"))
storage := testStorage(t, nil)
storage.Mode = GetModeGet
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
if err := tree.Validate(); err == nil {
t.Fatal("should error")
}
}
func TestTreeValidate_badChildVar(t *testing.T) {
tree := NewTree("", testConfig(t, "validate-bad-var"))
storage := testStorage(t, nil)
storage.Mode = GetModeGet
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
if err := tree.Validate(); err == nil {
t.Fatal("should error")
}
}
func TestTreeValidate_badRoot(t *testing.T) {
tree := NewTree("", testConfig(t, "validate-root-bad"))
storage := testStorage(t, nil)
storage.Mode = GetModeGet
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
if err := tree.Validate(); err == nil {
t.Fatal("should error")
}
}
func TestTreeValidate_good(t *testing.T) {
tree := NewTree("", testConfig(t, "validate-child-good"))
storage := testStorage(t, nil)
storage.Mode = GetModeGet
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
if err := tree.Validate(); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestTreeValidate_notLoaded(t *testing.T) {
tree := NewTree("", testConfig(t, "basic"))
if err := tree.Validate(); err == nil {
t.Fatal("should error")
}
}
func TestTreeValidate_requiredChildVar(t *testing.T) {
tree := NewTree("", testConfig(t, "validate-required-var"))
storage := testStorage(t, nil)
storage.Mode = GetModeGet
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
diags := tree.Validate()
if !diags.HasErrors() {
t.Fatal("should error")
}
// ensure both variables are mentioned in the output
errMsg := diags.Err().Error()
for _, v := range []string{"feature", "memory"} {
if !strings.Contains(errMsg, v) {
t.Fatalf("no mention of missing variable %q", v)
}
}
}
func TestTreeValidate_unknownModule(t *testing.T) {
tree := NewTree("", testConfig(t, "validate-module-unknown"))
storage := testStorage(t, nil)
storage.Mode = GetModeNone
if err := tree.Load(storage); err != nil {
t.Fatalf("err: %s", err)
}
if err := tree.Validate(); err == nil {
t.Fatal("should error")
}
}
func TestTreeLoad_conflictingSubmoduleNames(t *testing.T) {
storage := testStorage(t, nil)
tree := NewTree("", testConfig(t, "conficting-submodule-names"))
storage.Mode = GetModeGet
if err := tree.Load(storage); err != nil {
t.Fatalf("load failed: %s", err)
}
if !tree.Loaded() {
t.Fatal("should be loaded")
}
// Try to reload
storage.Mode = GetModeNone
if err := tree.Load(storage); err != nil {
t.Fatalf("reload failed: %s", err)
}
// verify that the grand-children are correctly loaded
for _, c := range tree.Children() {
for _, gc := range c.Children() {
if len(gc.config.Resources) != 1 {
t.Fatalf("expected 1 resource in %s, got %d", gc.name, len(gc.config.Resources))
}
res := gc.config.Resources[0]
switch gc.path[0] {
case "a":
if res.Name != "a-c" {
t.Fatal("found wrong resource in a/c:", res.Name)
}
case "b":
if res.Name != "b-c" {
t.Fatal("found wrong resource in b/c:", res.Name)
}
}
}
}
}
// changing the source for a module but not the module "path"
func TestTreeLoad_changeIntermediateSource(t *testing.T) {
// copy the config to our tempdir this time, since we're going to edit it
td, err := ioutil.TempDir("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(td)
if err := copyDir(td, filepath.Join(fixtureDir, "change-intermediate-source")); err != nil {
t.Fatal(err)
}
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(td); err != nil {
t.Fatal(err)
}
defer os.Chdir(wd)
if err := os.MkdirAll(".terraform/modules", 0777); err != nil {
t.Fatal(err)
}
storage := &Storage{StorageDir: ".terraform/modules"}
cfg, err := config.LoadDir("./")
if err != nil {
t.Fatal(err)
}
tree := NewTree("", cfg)
storage.Mode = GetModeGet
if err := tree.Load(storage); err != nil {
t.Fatalf("load failed: %s", err)
}
// now we change the source of our module, without changing its path
if err := os.Rename("main.tf.disabled", "main.tf"); err != nil {
t.Fatal(err)
}
// reload the tree
cfg, err = config.LoadDir("./")
if err != nil {
t.Fatal(err)
}
tree = NewTree("", cfg)
if err := tree.Load(storage); err != nil {
t.Fatalf("load failed: %s", err)
}
// check for our resource in b
for _, c := range tree.Children() {
for _, gc := range c.Children() {
if len(gc.config.Resources) != 1 {
t.Fatalf("expected 1 resource in %s, got %d", gc.name, len(gc.config.Resources))
}
res := gc.config.Resources[0]
expected := "c-b"
if res.Name != expected {
t.Fatalf("expexted resource %q, got %q", expected, res.Name)
}
}
}
}
const treeLoadStr = `
root
foo (path: foo)
`
const treeLoadParentStr = `
root
a (path: a)
b (path: a, b)
`
const treeLoadSubdirStr = `
root
foo (path: foo)
bar (path: foo, bar)
`
const treeLoadRegistrySubdirStr = `
root
foo (path: foo)
`