Files
opentf/internal/configs/parser_config_test.go
Diogenes Fernandes 774224a2dd Early return when having errors and additional tests (#3042)
Co-authored-by: Andrei Ciobanu <andrei.ciobanu@opentofu.org>
Signed-off-by: Diogenes Fernandes <diofeher@gmail.com>
2025-09-26 08:44:39 -03:00

340 lines
10 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 (
"bufio"
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
)
// TestParseLoadConfigFileSuccess is a simple test that just verifies that
// a number of test configuration files (in testdata/valid-files) can
// be parsed without raising any diagnostics.
//
// This test does not verify that reading these files produces the correct
// file element contents. More detailed assertions may be made on some subset
// of these configuration files in other tests.
func TestParserLoadConfigFileSuccess(t *testing.T) {
files, err := os.ReadDir("testdata/valid-files")
if err != nil {
t.Fatal(err)
}
for _, info := range files {
name := info.Name()
t.Run(name, func(t *testing.T) {
src, err := os.ReadFile(filepath.Join("testdata/valid-files", name))
if err != nil {
t.Fatal(err)
}
parser := testParser(map[string]string{
name: string(src),
})
_, diags := parser.LoadConfigFile(name)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics")
for _, diag := range diags {
t.Logf("- %s", diag)
}
}
})
}
}
// TestParseLoadConfigFileFailure is a simple test that just verifies that
// a number of test configuration files (in testdata/invalid-files)
// produce errors as expected.
//
// This test does not verify specific error messages, so more detailed
// assertions should be made on some subset of these configuration files in
// other tests.
func TestParserLoadConfigFileFailure(t *testing.T) {
files, err := os.ReadDir("testdata/invalid-files")
if err != nil {
t.Fatal(err)
}
for _, info := range files {
name := info.Name()
t.Run(name, func(t *testing.T) {
src, err := os.ReadFile(filepath.Join("testdata/invalid-files", name))
if err != nil {
t.Fatal(err)
}
parser := testParser(map[string]string{
name: string(src),
})
_, diags := parser.LoadConfigFile(name)
if !diags.HasErrors() {
t.Errorf("LoadConfigFile succeeded; want errors")
}
for _, diag := range diags {
t.Logf("- %s", diag)
}
})
}
}
// This test uses a subset of the same fixture files as
// TestParserLoadConfigFileFailure, but additionally verifies that each
// file produces the expected diagnostic summary and detail.
func TestParserLoadConfigFileFailureMessages(t *testing.T) {
tests := []struct {
Filename string
WantSeverity hcl.DiagnosticSeverity
WantDiag string
WantDetail string
}{
{
"invalid-files/data-resource-lifecycle.tf",
hcl.DiagError,
"Invalid data resource lifecycle argument",
`The lifecycle argument "ignore_changes" is defined only for managed resources ("resource" blocks), and is not valid for data resources.`,
},
{
"invalid-files/variable-type-unknown.tf",
hcl.DiagError,
"Invalid type specification",
`The keyword "notatype" is not a valid type specification.`,
},
{
"invalid-files/unexpected-attr.tf",
hcl.DiagError,
"Unsupported argument",
`An argument named "foo" is not expected here.`,
},
{
"invalid-files/unexpected-block.tf",
hcl.DiagError,
"Unsupported block type",
`Blocks of type "varyable" are not expected here. Did you mean "variable"?`,
},
{
"invalid-files/ephemeral-count-and-for_each-and-enabled.tf",
hcl.DiagError,
`Invalid combination of "count", "enabled", and "for_each"`,
`The "count", "enabled", and "for_each" meta-arguments are mutually-exclusive. Only one may be used to be explicit about the number of ephemeral resources to be created.`,
},
{
"invalid-files/resource-count-and-for_each.tf",
hcl.DiagError,
`Invalid combination of "count" and "for_each"`,
`The "count" and "for_each" meta-arguments are mutually-exclusive. Only one may be used to be explicit about the number of resources to be created.`,
},
{
"invalid-files/resource-count-and-for_each-and-enabled.tf",
hcl.DiagError,
`Invalid combination of "count", "enabled", and "for_each"`,
`The "count", "enabled", and "for_each" meta-arguments are mutually-exclusive. Only one may be used to be explicit about the number of resources to be created.`,
},
{
"invalid-files/data-count-and-for_each.tf",
hcl.DiagError,
`Invalid combination of "count" and "for_each"`,
`The "count" and "for_each" meta-arguments are mutually-exclusive. Only one may be used to be explicit about the number of data sources to be created.`,
},
{
"invalid-files/data-count-and-for_each-and-enabled.tf",
hcl.DiagError,
`Invalid combination of "count", "enabled", and "for_each"`,
`The "count", "enabled", and "for_each" meta-arguments are mutually-exclusive. Only one may be used to be explicit about the number of data sources to be created.`,
},
{
"invalid-files/resource-lifecycle-badbool.tf",
hcl.DiagError,
"Unsuitable value type",
`Unsuitable value: a bool is required`,
},
{
"invalid-files/variable-complex-bad-default-inner-obj.tf",
hcl.DiagError,
"Invalid default value for variable",
`This default value is not compatible with the variable's type constraint: ["mykey"].field: a bool is required.`,
},
{
"invalid-files/empty-deprecated-output-attr.tf",
hcl.DiagError,
"Invalid `deprecated` attribute",
`The "deprecated" argument must not be empty, and should provide instructions on how to migrate away from usage of this deprecated output value.`,
},
}
for _, test := range tests {
t.Run(test.Filename, func(t *testing.T) {
src, err := os.ReadFile(filepath.Join("testdata", test.Filename))
if err != nil {
t.Fatal(err)
}
parser := testParser(map[string]string{
test.Filename: string(src),
})
_, diags := parser.LoadConfigFile(test.Filename)
if len(diags) != 1 {
t.Errorf("Wrong number of diagnostics %d; want 1", len(diags))
for _, diag := range diags {
t.Logf("- %s", diag)
}
return
}
if diags[0].Severity != test.WantSeverity {
t.Errorf("Wrong diagnostic severity %#v; want %#v", diags[0].Severity, test.WantSeverity)
}
if diags[0].Summary != test.WantDiag {
t.Errorf("Wrong diagnostic summary\ngot: %s\nwant: %s", diags[0].Summary, test.WantDiag)
}
if diags[0].Detail != test.WantDetail {
t.Errorf("Wrong diagnostic detail\ngot: %s\nwant: %s", diags[0].Detail, test.WantDetail)
}
})
}
}
// TestParseLoadConfigFileWarning is a test that verifies files from
// testdata/warning-files produce particular warnings.
//
// This test does not verify that reading these files produces the correct
// file element contents in spite of those warnings. More detailed assertions
// may be made on some subset of these configuration files in other tests.
func TestParserLoadConfigFileWarning(t *testing.T) {
files, err := os.ReadDir("testdata/warning-files")
if err != nil {
t.Fatal(err)
}
for _, info := range files {
name := info.Name()
t.Run(name, func(t *testing.T) {
src, err := os.ReadFile(filepath.Join("testdata/warning-files", name))
if err != nil {
t.Fatal(err)
}
// First we'll scan the file to see what warnings are expected.
// That's declared inside the files themselves by using the
// string "WARNING: " somewhere on each line that is expected
// to produce a warning, followed by the expected warning summary
// text. A single-line comment (with #) is the main way to do that.
const marker = "WARNING: "
sc := bufio.NewScanner(bytes.NewReader(src))
wantWarnings := make(map[int]string)
lineNum := 1
for sc.Scan() {
lineText := sc.Text()
if idx := strings.Index(lineText, marker); idx != -1 {
summaryText := lineText[idx+len(marker):]
wantWarnings[lineNum] = summaryText
}
lineNum++
}
parser := testParser(map[string]string{
name: string(src),
})
_, diags := parser.LoadConfigFile(name)
if diags.HasErrors() {
t.Errorf("unexpected error diagnostics")
for _, diag := range diags {
t.Logf("- %s", diag)
}
}
gotWarnings := make(map[int]string)
for _, diag := range diags {
if diag.Severity != hcl.DiagWarning || diag.Subject == nil {
continue
}
gotWarnings[diag.Subject.Start.Line] = diag.Summary
}
if diff := cmp.Diff(wantWarnings, gotWarnings); diff != "" {
t.Errorf("wrong warnings\n%s", diff)
}
})
}
}
// TestParseLoadConfigFileError is a test that verifies files from
// testdata/warning-files produce particular errors.
//
// This test does not verify that reading these files produces the correct
// file element contents in spite of those errors. More detailed assertions
// may be made on some subset of these configuration files in other tests.
func TestParserLoadConfigFileError(t *testing.T) {
files, err := os.ReadDir("testdata/error-files")
if err != nil {
t.Fatal(err)
}
for _, info := range files {
name := info.Name()
t.Run(name, func(t *testing.T) {
src, err := os.ReadFile(filepath.Join("testdata/error-files", name))
if err != nil {
t.Fatal(err)
}
// First we'll scan the file to see what warnings are expected.
// That's declared inside the files themselves by using the
// string "ERROR: " somewhere on each line that is expected
// to produce a warning, followed by the expected warning summary
// text. A single-line comment (with #) is the main way to do that.
const marker = "ERROR: "
sc := bufio.NewScanner(bytes.NewReader(src))
wantErrors := make(map[int]string)
lineNum := 1
for sc.Scan() {
lineText := sc.Text()
if idx := strings.Index(lineText, marker); idx != -1 {
summaryText := lineText[idx+len(marker):]
wantErrors[lineNum] = summaryText
}
lineNum++
}
parser := testParser(map[string]string{
name: string(src),
})
file, diags := parser.LoadConfigFile(name)
// TODO many of these errors are now deferred until module loading
// This is a structural issue which existed before static evaluation, but has been made worse by it
// See https://github.com/opentofu/opentofu/issues/1467 for more details
eval := NewStaticEvaluator(nil, RootModuleCallForTesting())
for _, mc := range file.ModuleCalls {
mDiags := mc.decodeStaticFields(t.Context(), eval)
diags = append(diags, mDiags...)
}
gotErrors := make(map[int]string)
for _, diag := range diags {
if diag.Severity != hcl.DiagError || diag.Subject == nil {
continue
}
gotErrors[diag.Subject.Start.Line] = diag.Summary
}
if diff := cmp.Diff(wantErrors, gotErrors); diff != "" {
t.Errorf("wrong errors\n%s", diff)
}
})
}
}