mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-23 07:33:32 -06:00
8f8e0aa4aa
Signed-off-by: Christian Mesh <christianmesh1@gmail.com> Signed-off-by: Christian Mesh <cristianmesh1@gmail.com> Co-authored-by: James Humphries <James@james-humphries.co.uk> Co-authored-by: Oleksandr Levchenkov <ollevche@gmail.com>
432 lines
13 KiB
Go
432 lines
13 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package configs
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
|
|
version "github.com/hashicorp/go-version"
|
|
"github.com/hashicorp/hcl/v2"
|
|
)
|
|
|
|
func TestBuildConfig(t *testing.T) {
|
|
parser := NewParser(nil)
|
|
mod, diags := parser.LoadConfigDir("testdata/config-build", RootModuleCallForTesting())
|
|
assertNoDiagnostics(t, diags)
|
|
if mod == nil {
|
|
t.Fatal("got nil root module; want non-nil")
|
|
}
|
|
|
|
versionI := 0
|
|
cfg, diags := BuildConfig(mod, ModuleWalkerFunc(
|
|
func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) {
|
|
// For the sake of this test we're going to just treat our
|
|
// SourceAddr as a path relative to our fixture directory.
|
|
// A "real" implementation of ModuleWalker should accept the
|
|
// various different source address syntaxes OpenTofu supports.
|
|
sourcePath := filepath.Join("testdata/config-build", req.SourceAddr.String())
|
|
|
|
mod, modDiags := parser.LoadConfigDir(sourcePath, req.Call)
|
|
version, _ := version.NewVersion(fmt.Sprintf("1.0.%d", versionI))
|
|
versionI++
|
|
return mod, version, modDiags
|
|
},
|
|
))
|
|
assertNoDiagnostics(t, diags)
|
|
if cfg == nil {
|
|
t.Fatal("got nil config; want non-nil")
|
|
}
|
|
|
|
var got []string
|
|
cfg.DeepEach(func(c *Config) {
|
|
got = append(got, fmt.Sprintf("%s %s", strings.Join(c.Path, "."), c.Version))
|
|
})
|
|
sort.Strings(got)
|
|
want := []string{
|
|
" <nil>",
|
|
"child_a 1.0.0",
|
|
"child_a.child_c 1.0.1",
|
|
"child_b 1.0.2",
|
|
"child_b.child_c 1.0.3",
|
|
}
|
|
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(want))
|
|
}
|
|
|
|
if _, exists := cfg.Children["child_a"].Children["child_c"].Module.Outputs["hello"]; !exists {
|
|
t.Fatalf("missing output 'hello' in child_a.child_c")
|
|
}
|
|
if _, exists := cfg.Children["child_b"].Children["child_c"].Module.Outputs["hello"]; !exists {
|
|
t.Fatalf("missing output 'hello' in child_b.child_c")
|
|
}
|
|
if cfg.Children["child_a"].Children["child_c"].Module == cfg.Children["child_b"].Children["child_c"].Module {
|
|
t.Fatalf("child_a.child_c is same object as child_b.child_c; should not be")
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigDiags(t *testing.T) {
|
|
parser := NewParser(nil)
|
|
mod, diags := parser.LoadConfigDir("testdata/nested-errors", RootModuleCallForTesting())
|
|
assertNoDiagnostics(t, diags)
|
|
if mod == nil {
|
|
t.Fatal("got nil root module; want non-nil")
|
|
}
|
|
|
|
versionI := 0
|
|
cfg, diags := BuildConfig(mod, ModuleWalkerFunc(
|
|
func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) {
|
|
// For the sake of this test we're going to just treat our
|
|
// SourceAddr as a path relative to our fixture directory.
|
|
// A "real" implementation of ModuleWalker should accept the
|
|
// various different source address syntaxes OpenTofu supports.
|
|
sourcePath := filepath.Join("testdata/nested-errors", req.SourceAddr.String())
|
|
|
|
mod, modDiags := parser.LoadConfigDir(sourcePath, req.Call)
|
|
version, _ := version.NewVersion(fmt.Sprintf("1.0.%d", versionI))
|
|
versionI++
|
|
return mod, version, modDiags
|
|
},
|
|
))
|
|
|
|
wantDiag := filepath.FromSlash(`testdata/nested-errors/child_c/child_c.tf:5,1-8: `) +
|
|
`Unsupported block type; Blocks of type "invalid" are not expected here.`
|
|
assertExactDiagnostics(t, diags, []string{wantDiag})
|
|
|
|
// we should still have module structure loaded
|
|
var got []string
|
|
cfg.DeepEach(func(c *Config) {
|
|
got = append(got, fmt.Sprintf("%s %s", strings.Join(c.Path, "."), c.Version))
|
|
})
|
|
sort.Strings(got)
|
|
want := []string{
|
|
" <nil>",
|
|
"child_a 1.0.0",
|
|
"child_a.child_c 1.0.1",
|
|
}
|
|
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(want))
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigChildModuleBackend(t *testing.T) {
|
|
parser := NewParser(nil)
|
|
mod, diags := parser.LoadConfigDir("testdata/nested-backend-warning", RootModuleCallForTesting())
|
|
assertNoDiagnostics(t, diags)
|
|
if mod == nil {
|
|
t.Fatal("got nil root module; want non-nil")
|
|
}
|
|
|
|
cfg, diags := BuildConfig(mod, ModuleWalkerFunc(
|
|
func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) {
|
|
// For the sake of this test we're going to just treat our
|
|
// SourceAddr as a path relative to our fixture directory.
|
|
// A "real" implementation of ModuleWalker should accept the
|
|
// various different source address syntaxes OpenTofu supports.
|
|
sourcePath := filepath.Join("testdata/nested-backend-warning", req.SourceAddr.String())
|
|
|
|
mod, modDiags := parser.LoadConfigDir(sourcePath, req.Call)
|
|
version, _ := version.NewVersion("1.0.0")
|
|
return mod, version, modDiags
|
|
},
|
|
))
|
|
|
|
assertDiagnosticSummary(t, diags, "Backend configuration ignored")
|
|
|
|
// we should still have module structure loaded
|
|
var got []string
|
|
cfg.DeepEach(func(c *Config) {
|
|
got = append(got, fmt.Sprintf("%s %s", strings.Join(c.Path, "."), c.Version))
|
|
})
|
|
sort.Strings(got)
|
|
want := []string{
|
|
" <nil>",
|
|
"child 1.0.0",
|
|
}
|
|
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(want))
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigInvalidModules(t *testing.T) {
|
|
testDir := "testdata/config-diagnostics"
|
|
dirs, err := os.ReadDir(testDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, info := range dirs {
|
|
name := info.Name()
|
|
t.Run(name, func(t *testing.T) {
|
|
parser := NewParser(nil)
|
|
path := filepath.Join(testDir, name)
|
|
|
|
mod, diags := parser.LoadConfigDirWithTests(path, "tests", RootModuleCallForTesting())
|
|
if diags.HasErrors() {
|
|
// these tests should only trigger errors that are caught in
|
|
// the config loader.
|
|
t.Errorf("error loading config dir")
|
|
for _, diag := range diags {
|
|
t.Logf("- %s", diag)
|
|
}
|
|
}
|
|
|
|
readDiags := func(data []byte, _ error) []string {
|
|
var expected []string
|
|
for _, s := range strings.Split(string(data), "\n") {
|
|
msg := strings.TrimSpace(s)
|
|
msg = strings.ReplaceAll(msg, `\n`, "\n")
|
|
// The filepath preset in testdata with unix-style slash.
|
|
// We should from slash to adapt to Linux, Windows and others OS.
|
|
msgSplit := strings.SplitN(msg, ":", 2)
|
|
if len(msgSplit) == 2 {
|
|
msgSplit[0] = filepath.FromSlash(msgSplit[0])
|
|
msg = strings.Join(msgSplit, ":")
|
|
}
|
|
|
|
if msg != "" {
|
|
expected = append(expected, msg)
|
|
}
|
|
}
|
|
return expected
|
|
}
|
|
|
|
// Load expected errors and warnings.
|
|
// Each line in the file is matched as a substring against the
|
|
// diagnostic outputs.
|
|
// Capturing part of the path and source range in the message lets
|
|
// us also ensure the diagnostic is being attributed to the
|
|
// expected location in the source, but is not required.
|
|
// The literal characters `\n` are replaced with newlines, but
|
|
// otherwise the string is unchanged.
|
|
expectedErrs := readDiags(os.ReadFile(filepath.Join(testDir, name, "errors")))
|
|
expectedWarnings := readDiags(os.ReadFile(filepath.Join(testDir, name, "warnings")))
|
|
|
|
_, buildDiags := BuildConfig(mod, ModuleWalkerFunc(
|
|
func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) {
|
|
// for simplicity, these tests will treat all source
|
|
// addresses as relative to the root module
|
|
sourcePath := filepath.Join(path, req.SourceAddr.String())
|
|
mod, diags := parser.LoadConfigDir(sourcePath, req.Call)
|
|
version, _ := version.NewVersion("1.0.0")
|
|
return mod, version, diags
|
|
},
|
|
))
|
|
|
|
// we can make this less repetitive later if we want
|
|
for _, msg := range expectedErrs {
|
|
found := false
|
|
for _, diag := range buildDiags {
|
|
if diag.Severity == hcl.DiagError && strings.Contains(diag.Error(), msg) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
t.Errorf("Expected error diagnostic containing:\n %s", msg)
|
|
}
|
|
}
|
|
|
|
for _, diag := range buildDiags {
|
|
if diag.Severity != hcl.DiagError {
|
|
continue
|
|
}
|
|
found := false
|
|
for _, msg := range expectedErrs {
|
|
if strings.Contains(diag.Error(), msg) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
t.Errorf("Unexpected error:\n %s", diag)
|
|
}
|
|
}
|
|
|
|
for _, msg := range expectedWarnings {
|
|
found := false
|
|
for _, diag := range buildDiags {
|
|
if diag.Severity == hcl.DiagWarning && strings.Contains(diag.Error(), msg) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
t.Errorf("Expected warning diagnostic containing:\n %s", msg)
|
|
}
|
|
}
|
|
|
|
for _, diag := range buildDiags {
|
|
if diag.Severity != hcl.DiagWarning {
|
|
continue
|
|
}
|
|
found := false
|
|
for _, msg := range expectedWarnings {
|
|
if strings.Contains(diag.Error(), msg) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
t.Errorf("Unexpected warning:\n %s", diag)
|
|
}
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildConfig_WithNestedTestModules(t *testing.T) {
|
|
parser := NewParser(nil)
|
|
mod, diags := parser.LoadConfigDirWithTests("testdata/valid-modules/with-tests-nested-module", "tests", RootModuleCallForTesting())
|
|
assertNoDiagnostics(t, diags)
|
|
if mod == nil {
|
|
t.Fatal("got nil root module; want non-nil")
|
|
}
|
|
|
|
cfg, diags := BuildConfig(mod, ModuleWalkerFunc(
|
|
func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) {
|
|
|
|
// Bit of a hack to get the test working, but we know all the source
|
|
// addresses in this test are locals, so we can just treat them as
|
|
// paths in the filesystem.
|
|
|
|
addr := req.SourceAddr.String()
|
|
current := req.Parent
|
|
for current.SourceAddr != nil {
|
|
addr = path.Join(current.SourceAddr.String(), addr)
|
|
current = current.Parent
|
|
}
|
|
sourcePath := filepath.Join("testdata/valid-modules/with-tests-nested-module", addr)
|
|
|
|
mod, modDiags := parser.LoadConfigDir(sourcePath, req.Call)
|
|
version, _ := version.NewVersion("1.0.0")
|
|
return mod, version, modDiags
|
|
},
|
|
))
|
|
assertNoDiagnostics(t, diags)
|
|
if cfg == nil {
|
|
t.Fatal("got nil config; want non-nil")
|
|
}
|
|
|
|
// We should have loaded our test case, and one of the test runs should
|
|
// have loaded an alternate module.
|
|
|
|
if len(cfg.Module.Tests) != 1 {
|
|
t.Fatalf("expected exactly one test case but found %d", len(cfg.Module.Tests))
|
|
}
|
|
|
|
test := cfg.Module.Tests["main.tftest.hcl"]
|
|
if len(test.Runs) != 1 {
|
|
t.Fatalf("expected two test runs but found %d", len(test.Runs))
|
|
}
|
|
|
|
run := test.Runs[0]
|
|
if run.ConfigUnderTest == nil {
|
|
t.Fatalf("the first test run should have loaded config but did not")
|
|
}
|
|
|
|
if run.ConfigUnderTest.Parent != nil {
|
|
t.Errorf("config under test should not have a parent")
|
|
}
|
|
|
|
if run.ConfigUnderTest.Root != run.ConfigUnderTest {
|
|
t.Errorf("config under test root should be itself")
|
|
}
|
|
|
|
if len(run.ConfigUnderTest.Path) > 0 {
|
|
t.Errorf("config under test path should be the root module")
|
|
}
|
|
|
|
// We should also have loaded a single child underneath the config under
|
|
// test, and it should have valid paths.
|
|
|
|
child := run.ConfigUnderTest.Children["child"]
|
|
|
|
if child.Parent != run.ConfigUnderTest {
|
|
t.Errorf("child should point back to root")
|
|
}
|
|
|
|
if len(child.Path) != 1 || child.Path[0] != "child" {
|
|
t.Errorf("child should have rebased against virtual root")
|
|
}
|
|
|
|
if child.Root != run.ConfigUnderTest {
|
|
t.Errorf("child root should be main config under test")
|
|
}
|
|
}
|
|
|
|
func TestBuildConfig_WithTestModule(t *testing.T) {
|
|
parser := NewParser(nil)
|
|
mod, diags := parser.LoadConfigDirWithTests("testdata/valid-modules/with-tests-module", "tests", RootModuleCallForTesting())
|
|
assertNoDiagnostics(t, diags)
|
|
if mod == nil {
|
|
t.Fatal("got nil root module; want non-nil")
|
|
}
|
|
|
|
cfg, diags := BuildConfig(mod, ModuleWalkerFunc(
|
|
func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) {
|
|
// For the sake of this test we're going to just treat our
|
|
// SourceAddr as a path relative to our fixture directory.
|
|
// A "real" implementation of ModuleWalker should accept the
|
|
// various different source address syntaxes OpenTofu supports.
|
|
sourcePath := filepath.Join("testdata/valid-modules/with-tests-module", req.SourceAddr.String())
|
|
|
|
mod, modDiags := parser.LoadConfigDir(sourcePath, req.Call)
|
|
version, _ := version.NewVersion("1.0.0")
|
|
return mod, version, modDiags
|
|
},
|
|
))
|
|
assertNoDiagnostics(t, diags)
|
|
if cfg == nil {
|
|
t.Fatal("got nil config; want non-nil")
|
|
}
|
|
|
|
// We should have loaded our test case, and one of the test runs should
|
|
// have loaded an alternate module.
|
|
|
|
if len(cfg.Module.Tests) != 1 {
|
|
t.Fatalf("expected exactly one test case but found %d", len(cfg.Module.Tests))
|
|
}
|
|
|
|
test := cfg.Module.Tests["main.tftest.hcl"]
|
|
if len(test.Runs) != 2 {
|
|
t.Fatalf("expected two test runs but found %d", len(test.Runs))
|
|
}
|
|
|
|
run := test.Runs[0]
|
|
if run.ConfigUnderTest == nil {
|
|
t.Fatalf("the first test run should have loaded config but did not")
|
|
}
|
|
|
|
if run.ConfigUnderTest.Parent != nil {
|
|
t.Errorf("config under test should not have a parent")
|
|
}
|
|
|
|
if run.ConfigUnderTest.Root != run.ConfigUnderTest {
|
|
t.Errorf("config under test root should be itself")
|
|
}
|
|
|
|
if len(run.ConfigUnderTest.Path) > 0 {
|
|
t.Errorf("config under test path should be the root module")
|
|
}
|
|
}
|