// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package backend import ( "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tofu" ) func TestUnparsedValue(t *testing.T) { vv := map[string]UnparsedVariableValue{ "undeclared0": testUnparsedVariableValue("0"), "undeclared1": testUnparsedVariableValue("1"), "undeclared2": testUnparsedVariableValue("2"), "undeclared3": testUnparsedVariableValue("3"), "undeclared4": testUnparsedVariableValue("4"), "declared1": testUnparsedVariableValue("5"), } decls := map[string]*configs.Variable{ "declared1": { Name: "declared1", Type: cty.String, ConstraintType: cty.String, ParsingMode: configs.VariableParseLiteral, DeclRange: hcl.Range{ Filename: "fake.tf", Start: hcl.Pos{Line: 2, Column: 1, Byte: 0}, End: hcl.Pos{Line: 2, Column: 1, Byte: 0}, }, }, "missing1": { Name: "missing1", Type: cty.String, ConstraintType: cty.String, ParsingMode: configs.VariableParseLiteral, DeclRange: hcl.Range{ Filename: "fake.tf", Start: hcl.Pos{Line: 3, Column: 1, Byte: 0}, End: hcl.Pos{Line: 3, Column: 1, Byte: 0}, }, }, "missing2": { Name: "missing1", Type: cty.String, ConstraintType: cty.String, ParsingMode: configs.VariableParseLiteral, Default: cty.StringVal("default for missing2"), DeclRange: hcl.Range{ Filename: "fake.tf", Start: hcl.Pos{Line: 4, Column: 1, Byte: 0}, End: hcl.Pos{Line: 4, Column: 1, Byte: 0}, }, }, } const undeclSingular = `Value for undeclared variable` const undeclPlural = `Values for undeclared variables` t.Run("ParseDeclaredVariableValues", func(t *testing.T) { gotVals, diags := ParseDeclaredVariableValues(vv, decls) if got, want := len(diags), 0; got != want { t.Fatalf("wrong number of diagnostics %d; want %d", got, want) } wantVals := tofu.InputValues{ "declared1": { Value: cty.StringVal("5"), SourceType: tofu.ValueFromNamedFile, SourceRange: tfdiags.SourceRange{ Filename: "fake.tfvars", Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, }, }, } if diff := cmp.Diff(wantVals, gotVals, cmp.Comparer(cty.Value.RawEquals)); diff != "" { t.Errorf("wrong result\n%s", diff) } }) t.Run("ParseUndeclaredVariableValues", func(t *testing.T) { gotVals, diags := ParseUndeclaredVariableValues(vv, decls) if got, want := len(diags), 3; got != want { t.Fatalf("wrong number of diagnostics %d; want %d", got, want) } if got, want := diags[0].Description().Summary, undeclSingular; got != want { t.Errorf("wrong summary for diagnostic 0\ngot: %s\nwant: %s", got, want) } if got, want := diags[1].Description().Summary, undeclSingular; got != want { t.Errorf("wrong summary for diagnostic 1\ngot: %s\nwant: %s", got, want) } if got, want := diags[2].Description().Summary, undeclPlural; got != want { t.Errorf("wrong summary for diagnostic 2\ngot: %s\nwant: %s", got, want) } wantVals := tofu.InputValues{ "undeclared0": { Value: cty.StringVal("0"), SourceType: tofu.ValueFromNamedFile, SourceRange: tfdiags.SourceRange{ Filename: "fake.tfvars", Start: tfdiags.SourcePos{Line: 1, Column: 1}, End: tfdiags.SourcePos{Line: 1, Column: 1}, }, }, "undeclared1": { Value: cty.StringVal("1"), SourceType: tofu.ValueFromNamedFile, SourceRange: tfdiags.SourceRange{ Filename: "fake.tfvars", Start: tfdiags.SourcePos{Line: 1, Column: 1}, End: tfdiags.SourcePos{Line: 1, Column: 1}, }, }, "undeclared2": { Value: cty.StringVal("2"), SourceType: tofu.ValueFromNamedFile, SourceRange: tfdiags.SourceRange{ Filename: "fake.tfvars", Start: tfdiags.SourcePos{Line: 1, Column: 1}, End: tfdiags.SourcePos{Line: 1, Column: 1}, }, }, "undeclared3": { Value: cty.StringVal("3"), SourceType: tofu.ValueFromNamedFile, SourceRange: tfdiags.SourceRange{ Filename: "fake.tfvars", Start: tfdiags.SourcePos{Line: 1, Column: 1}, End: tfdiags.SourcePos{Line: 1, Column: 1}, }, }, "undeclared4": { Value: cty.StringVal("4"), SourceType: tofu.ValueFromNamedFile, SourceRange: tfdiags.SourceRange{ Filename: "fake.tfvars", Start: tfdiags.SourcePos{Line: 1, Column: 1}, End: tfdiags.SourcePos{Line: 1, Column: 1}, }, }, } if diff := cmp.Diff(wantVals, gotVals, cmp.Comparer(cty.Value.RawEquals)); diff != "" { t.Errorf("wrong result\n%s", diff) } }) t.Run("ParseVariableValues", func(t *testing.T) { gotVals, diags := ParseVariableValues(vv, decls) for _, diag := range diags { t.Logf("%s: %s", diag.Description().Summary, diag.Description().Detail) } if got, want := len(diags), 4; got != want { t.Fatalf("wrong number of diagnostics %d; want %d", got, want) } const missingRequired = `No value for required variable` if got, want := diags[0].Description().Summary, undeclSingular; got != want { t.Errorf("wrong summary for diagnostic 0\ngot: %s\nwant: %s", got, want) } if got, want := diags[1].Description().Summary, undeclSingular; got != want { t.Errorf("wrong summary for diagnostic 1\ngot: %s\nwant: %s", got, want) } if got, want := diags[2].Description().Summary, undeclPlural; got != want { t.Errorf("wrong summary for diagnostic 2\ngot: %s\nwant: %s", got, want) } if got, want := diags[2].Description().Detail, "3 other variable(s)"; !strings.Contains(got, want) { t.Errorf("wrong detail for diagnostic 2\ngot: %s\nmust contain: %s", got, want) } if got, want := diags[3].Description().Summary, missingRequired; got != want { t.Errorf("wrong summary for diagnostic 3\ngot: %s\nwant: %s", got, want) } wantVals := tofu.InputValues{ "declared1": { Value: cty.StringVal("5"), SourceType: tofu.ValueFromNamedFile, SourceRange: tfdiags.SourceRange{ Filename: "fake.tfvars", Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, }, }, "missing1": { Value: cty.DynamicVal, SourceType: tofu.ValueFromConfig, SourceRange: tfdiags.SourceRange{ Filename: "fake.tf", Start: tfdiags.SourcePos{Line: 3, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 3, Column: 1, Byte: 0}, }, }, "missing2": { Value: cty.NilVal, // OpenTofu Core handles substituting the default SourceType: tofu.ValueFromConfig, SourceRange: tfdiags.SourceRange{ Filename: "fake.tf", Start: tfdiags.SourcePos{Line: 4, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 4, Column: 1, Byte: 0}, }, }, } if diff := cmp.Diff(wantVals, gotVals, cmp.Comparer(cty.Value.RawEquals)); diff != "" { t.Errorf("wrong result\n%s", diff) } }) } type testUnparsedVariableValue string func (v testUnparsedVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*tofu.InputValue, tfdiags.Diagnostics) { return &tofu.InputValue{ Value: cty.StringVal(string(v)), SourceType: tofu.ValueFromNamedFile, SourceRange: tfdiags.SourceRange{ Filename: "fake.tfvars", Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, }, }, nil }