| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package format | |
| import ( | |
| "bytes" | |
| "fmt" | |
| "regexp" | |
| "strings" | |
| "testing" | |
| "github.com/google/go-cmp/cmp" | |
| "github.com/hashicorp/hcl/v2" | |
| "github.com/hashicorp/hcl/v2/hclsyntax" | |
| "github.com/hashicorp/hcl/v2/hcltest" | |
| "github.com/mitchellh/colorstring" | |
| "github.com/zclconf/go-cty/cty" | |
| "github.com/zclconf/go-cty/cty/function" | |
| viewsjson "github.com/hashicorp/terraform/internal/command/views/json" | |
| "github.com/hashicorp/terraform/internal/lang/marks" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| ) | |
| func TestDiagnostic(t *testing.T) { | |
| tests := map[string]struct { | |
| Diag interface{} | |
| Want string | |
| }{ | |
| "sourceless error": { | |
| tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "A sourceless error", | |
| "It has no source references but it does have a pretty long detail that should wrap over multiple lines.", | |
| ), | |
| `[red]╷[reset] | |
| [red]│[reset] [bold][red]Error: [reset][bold]A sourceless error[reset] | |
| [red]│[reset] | |
| [red]│[reset] It has no source references but it | |
| [red]│[reset] does have a pretty long detail that | |
| [red]│[reset] should wrap over multiple lines. | |
| [red]╵[reset] | |
| `, | |
| }, | |
| "sourceless warning": { | |
| tfdiags.Sourceless( | |
| tfdiags.Warning, | |
| "A sourceless warning", | |
| "It has no source references but it does have a pretty long detail that should wrap over multiple lines.", | |
| ), | |
| `[yellow]╷[reset] | |
| [yellow]│[reset] [bold][yellow]Warning: [reset][bold]A sourceless warning[reset] | |
| [yellow]│[reset] | |
| [yellow]│[reset] It has no source references but it | |
| [yellow]│[reset] does have a pretty long detail that | |
| [yellow]│[reset] should wrap over multiple lines. | |
| [yellow]╵[reset] | |
| `, | |
| }, | |
| "error with source code subject": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Bad bad bad", | |
| Detail: "Whatever shall we do?", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| }, | |
| `[red]╷[reset] | |
| [red]│[reset] [bold][red]Error: [reset][bold]Bad bad bad[reset] | |
| [red]│[reset] | |
| [red]│[reset] on test.tf line 1: | |
| [red]│[reset] 1: test [underline]source[reset] code | |
| [red]│[reset] | |
| [red]│[reset] Whatever shall we do? | |
| [red]╵[reset] | |
| `, | |
| }, | |
| "error with source code subject and known expression": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Bad bad bad", | |
| Detail: "Whatever shall we do?", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| Expression: hcltest.MockExprTraversal(hcl.Traversal{ | |
| hcl.TraverseRoot{Name: "boop"}, | |
| hcl.TraverseAttr{Name: "beep"}, | |
| }), | |
| EvalContext: &hcl.EvalContext{ | |
| Variables: map[string]cty.Value{ | |
| "boop": cty.ObjectVal(map[string]cty.Value{ | |
| "beep": cty.StringVal("blah"), | |
| }), | |
| }, | |
| }, | |
| }, | |
| `[red]╷[reset] | |
| [red]│[reset] [bold][red]Error: [reset][bold]Bad bad bad[reset] | |
| [red]│[reset] | |
| [red]│[reset] on test.tf line 1: | |
| [red]│[reset] 1: test [underline]source[reset] code | |
| [red]│[reset] [dark_gray]├────────────────[reset] | |
| [red]│[reset] [dark_gray]│[reset] [bold]boop.beep[reset] is "blah" | |
| [red]│[reset] | |
| [red]│[reset] Whatever shall we do? | |
| [red]╵[reset] | |
| `, | |
| }, | |
| "error with source code subject and expression referring to sensitive value": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Bad bad bad", | |
| Detail: "Whatever shall we do?", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| Expression: hcltest.MockExprTraversal(hcl.Traversal{ | |
| hcl.TraverseRoot{Name: "boop"}, | |
| hcl.TraverseAttr{Name: "beep"}, | |
| }), | |
| EvalContext: &hcl.EvalContext{ | |
| Variables: map[string]cty.Value{ | |
| "boop": cty.ObjectVal(map[string]cty.Value{ | |
| "beep": cty.StringVal("blah").Mark(marks.Sensitive), | |
| }), | |
| }, | |
| }, | |
| Extra: diagnosticCausedBySensitive(true), | |
| }, | |
| `[red]╷[reset] | |
| [red]│[reset] [bold][red]Error: [reset][bold]Bad bad bad[reset] | |
| [red]│[reset] | |
| [red]│[reset] on test.tf line 1: | |
| [red]│[reset] 1: test [underline]source[reset] code | |
| [red]│[reset] [dark_gray]├────────────────[reset] | |
| [red]│[reset] [dark_gray]│[reset] [bold]boop.beep[reset] has a sensitive value | |
| [red]│[reset] | |
| [red]│[reset] Whatever shall we do? | |
| [red]╵[reset] | |
| `, | |
| }, | |
| "error with source code subject and unknown string expression": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Bad bad bad", | |
| Detail: "Whatever shall we do?", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| Expression: hcltest.MockExprTraversal(hcl.Traversal{ | |
| hcl.TraverseRoot{Name: "boop"}, | |
| hcl.TraverseAttr{Name: "beep"}, | |
| }), | |
| EvalContext: &hcl.EvalContext{ | |
| Variables: map[string]cty.Value{ | |
| "boop": cty.ObjectVal(map[string]cty.Value{ | |
| "beep": cty.UnknownVal(cty.String), | |
| }), | |
| }, | |
| }, | |
| Extra: diagnosticCausedByUnknown(true), | |
| }, | |
| `[red]╷[reset] | |
| [red]│[reset] [bold][red]Error: [reset][bold]Bad bad bad[reset] | |
| [red]│[reset] | |
| [red]│[reset] on test.tf line 1: | |
| [red]│[reset] 1: test [underline]source[reset] code | |
| [red]│[reset] [dark_gray]├────────────────[reset] | |
| [red]│[reset] [dark_gray]│[reset] [bold]boop.beep[reset] is a string, known only after apply | |
| [red]│[reset] | |
| [red]│[reset] Whatever shall we do? | |
| [red]╵[reset] | |
| `, | |
| }, | |
| "error with source code subject and unknown expression of unknown type": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Bad bad bad", | |
| Detail: "Whatever shall we do?", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| Expression: hcltest.MockExprTraversal(hcl.Traversal{ | |
| hcl.TraverseRoot{Name: "boop"}, | |
| hcl.TraverseAttr{Name: "beep"}, | |
| }), | |
| EvalContext: &hcl.EvalContext{ | |
| Variables: map[string]cty.Value{ | |
| "boop": cty.ObjectVal(map[string]cty.Value{ | |
| "beep": cty.UnknownVal(cty.DynamicPseudoType), | |
| }), | |
| }, | |
| }, | |
| Extra: diagnosticCausedByUnknown(true), | |
| }, | |
| `[red]╷[reset] | |
| [red]│[reset] [bold][red]Error: [reset][bold]Bad bad bad[reset] | |
| [red]│[reset] | |
| [red]│[reset] on test.tf line 1: | |
| [red]│[reset] 1: test [underline]source[reset] code | |
| [red]│[reset] [dark_gray]├────────────────[reset] | |
| [red]│[reset] [dark_gray]│[reset] [bold]boop.beep[reset] will be known only after apply | |
| [red]│[reset] | |
| [red]│[reset] Whatever shall we do? | |
| [red]╵[reset] | |
| `, | |
| }, | |
| "error with source code subject and function call annotation": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Bad bad bad", | |
| Detail: "Whatever shall we do?", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| Expression: hcltest.MockExprLiteral(cty.True), | |
| EvalContext: &hcl.EvalContext{ | |
| Functions: map[string]function.Function{ | |
| "beep": function.New(&function.Spec{ | |
| Params: []function.Parameter{ | |
| { | |
| Name: "pos_param_0", | |
| Type: cty.String, | |
| }, | |
| { | |
| Name: "pos_param_1", | |
| Type: cty.Number, | |
| }, | |
| }, | |
| VarParam: &function.Parameter{ | |
| Name: "var_param", | |
| Type: cty.Bool, | |
| }, | |
| }), | |
| }, | |
| }, | |
| // This is simulating what the HCL function call expression | |
| // type would generate on evaluation, by implementing the | |
| // same interface it uses. | |
| Extra: fakeDiagFunctionCallExtra("beep"), | |
| }, | |
| `[red]╷[reset] | |
| [red]│[reset] [bold][red]Error: [reset][bold]Bad bad bad[reset] | |
| [red]│[reset] | |
| [red]│[reset] on test.tf line 1: | |
| [red]│[reset] 1: test [underline]source[reset] code | |
| [red]│[reset] [dark_gray]├────────────────[reset] | |
| [red]│[reset] [dark_gray]│[reset] while calling [bold]beep[reset](pos_param_0, pos_param_1, var_param...) | |
| [red]│[reset] | |
| [red]│[reset] Whatever shall we do? | |
| [red]╵[reset] | |
| `, | |
| }, | |
| "error origination from failed test assertion": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Test assertion failed", | |
| Detail: "LHS not equal to RHS", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| Expression: &hclsyntax.BinaryOpExpr{ | |
| Op: hclsyntax.OpEqual, | |
| LHS: &hclsyntax.LiteralValueExpr{ | |
| Val: cty.ObjectVal(map[string]cty.Value{ | |
| "inner": cty.StringVal("str1"), | |
| "extra": cty.StringVal("str2"), | |
| }), | |
| }, | |
| RHS: &hclsyntax.LiteralValueExpr{ | |
| Val: cty.ObjectVal(map[string]cty.Value{ | |
| "inner": cty.StringVal("str11"), | |
| "extra": cty.StringVal("str21"), | |
| }), | |
| }, | |
| SrcRange: hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| }, | |
| EvalContext: &hcl.EvalContext{ | |
| Variables: map[string]cty.Value{ | |
| "foo": cty.ObjectVal(map[string]cty.Value{ | |
| "inner": cty.StringVal("str1"), | |
| }), | |
| "bar": cty.ObjectVal(map[string]cty.Value{ | |
| "inner": cty.StringVal("str2"), | |
| }), | |
| }, | |
| }, | |
| // This is simulating what the test assertion expression | |
| // type would generate on evaluation, by implementing the | |
| // same interface it uses. | |
| Extra: diagnosticCausedByTestFailure{true}, | |
| }, | |
| `[red]╷[reset] | |
| [red]│[reset] [bold][red]Error: [reset][bold]Test assertion failed[reset] | |
| [red]│[reset] | |
| [red]│[reset] on test.tf line 1: | |
| [red]│[reset] 1: test [underline]source[reset] code | |
| [red]│[reset] [dark_gray]├────────────────[reset] | |
| [red]│[reset] [dark_gray]│[reset] [bold]LHS[reset]: | |
| [red]│[reset] [dark_gray]│[reset] { | |
| [red]│[reset] [dark_gray]│[reset] "extra": "str2", | |
| [red]│[reset] [dark_gray]│[reset] "inner": "str1" | |
| [red]│[reset] [dark_gray]│[reset] } | |
| [red]│[reset] [dark_gray]│[reset] [bold]RHS[reset]: | |
| [red]│[reset] [dark_gray]│[reset] { | |
| [red]│[reset] [dark_gray]│[reset] "extra": "str21", | |
| [red]│[reset] [dark_gray]│[reset] "inner": "str11" | |
| [red]│[reset] [dark_gray]│[reset] } | |
| [red]│[reset] [dark_gray]│[reset] [bold]Diff[reset]: | |
| [red]│[reset] [dark_gray]│[reset] [red][bold]--- actual[reset] | |
| [red]│[reset] [dark_gray]│[reset] [green][bold]+++ expected[reset] | |
| [red]│[reset] [dark_gray]│[reset] [reset] { | |
| [red]│[reset] [dark_gray]│[reset] [red]-[reset] "extra": "str2", | |
| [red]│[reset] [dark_gray]│[reset] [red]-[reset] "inner": "str1" | |
| [red]│[reset] [dark_gray]│[reset] [green]+[reset] "extra": "str21", | |
| [red]│[reset] [dark_gray]│[reset] [green]+[reset] "inner": "str11" | |
| [red]│[reset] [dark_gray]│[reset] [reset] } | |
| [red]│[reset] | |
| [red]│[reset] | |
| [red]│[reset] LHS not equal to RHS | |
| [red]╵[reset] | |
| `, | |
| }, | |
| "error originating from failed wrapped test assertion by function": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Test assertion failed", | |
| Detail: "Example crash", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| Expression: &hclsyntax.FunctionCallExpr{ | |
| Name: "tobool", | |
| Args: []hclsyntax.Expression{ | |
| &hclsyntax.BinaryOpExpr{ | |
| Op: hclsyntax.OpEqual, | |
| LHS: &hclsyntax.LiteralValueExpr{ | |
| Val: cty.ObjectVal(map[string]cty.Value{ | |
| "inner": cty.StringVal("str1"), | |
| "extra": cty.StringVal("str2"), | |
| }), | |
| }, | |
| RHS: &hclsyntax.LiteralValueExpr{ | |
| Val: cty.ObjectVal(map[string]cty.Value{ | |
| "inner": cty.StringVal("str11"), | |
| "extra": cty.StringVal("str21"), | |
| }), | |
| }, | |
| SrcRange: hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| EvalContext: &hcl.EvalContext{ | |
| Variables: map[string]cty.Value{}, | |
| Functions: map[string]function.Function{ | |
| "tobool": function.New(&function.Spec{ | |
| Params: []function.Parameter{ | |
| { | |
| Name: "param_0", | |
| Type: cty.String, | |
| }, | |
| }, | |
| }), | |
| }, | |
| }, | |
| // This is simulating what the test assertion expression | |
| // type would generate on evaluation, by implementing the | |
| // same interface it uses. | |
| Extra: diagnosticCausedByTestFailure{true}, | |
| }, | |
| `[red]╷[reset] | |
| [red]│[reset] [bold][red]Error: [reset][bold]Test assertion failed[reset] | |
| [red]│[reset] | |
| [red]│[reset] on test.tf line 1: | |
| [red]│[reset] 1: test [underline]source[reset] code | |
| [red]│[reset] | |
| [red]│[reset] Example crash | |
| [red]╵[reset] | |
| `, | |
| }, | |
| "warning from deprecation": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagWarning, | |
| Summary: "Deprecation detected", | |
| Detail: "Countermeasures must be taken.", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| Extra: &tfdiags.DeprecationOriginDiagnosticExtra{ | |
| OriginDescription: "module.foo.bar", | |
| }, | |
| }, | |
| `[yellow]╷[reset] | |
| [yellow]│[reset] [bold][yellow]Warning: [reset][bold]Deprecation detected[reset] | |
| [yellow]│[reset] | |
| [yellow]│[reset] on test.tf line 1: | |
| [yellow]│[reset] 1: test [underline]source[reset] code | |
| [yellow]│[reset] | |
| [yellow]│[reset] The deprecation originates from module.foo.bar | |
| [yellow]│[reset] | |
| [yellow]│[reset] Countermeasures must be taken. | |
| [yellow]╵[reset] | |
| `, | |
| }, | |
| } | |
| sources := map[string][]byte{ | |
| "test.tf": []byte(`test source code`), | |
| "deprecated.tf": []byte(`source of deprecation`), | |
| } | |
| // This empty Colorize just passes through all of the formatting codes | |
| // untouched, because it doesn't define any formatting keywords. | |
| colorize := &colorstring.Colorize{} | |
| for name, test := range tests { | |
| t.Run(name, func(t *testing.T) { | |
| var diags tfdiags.Diagnostics | |
| diags = diags.Append(test.Diag) // to normalize it into a tfdiag.Diagnostic | |
| diag := diags[0] | |
| got := strings.TrimSpace(Diagnostic(diag, sources, colorize, 40)) | |
| want := strings.TrimSpace(test.Want) | |
| if diff := cmp.Diff(got, want); diff != "" { | |
| t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\ndiff:\n%s\n\n", got, want, diff) | |
| } | |
| }) | |
| } | |
| } | |
| func TestDiagnosticPlain(t *testing.T) { | |
| tests := map[string]struct { | |
| Diag interface{} | |
| Want string | |
| }{ | |
| "sourceless error": { | |
| tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "A sourceless error", | |
| "It has no source references but it does have a pretty long detail that should wrap over multiple lines.", | |
| ), | |
| ` | |
| Error: A sourceless error | |
| It has no source references but it does | |
| have a pretty long detail that should | |
| wrap over multiple lines. | |
| `, | |
| }, | |
| "sourceless warning": { | |
| tfdiags.Sourceless( | |
| tfdiags.Warning, | |
| "A sourceless warning", | |
| "It has no source references but it does have a pretty long detail that should wrap over multiple lines.", | |
| ), | |
| ` | |
| Warning: A sourceless warning | |
| It has no source references but it does | |
| have a pretty long detail that should | |
| wrap over multiple lines. | |
| `, | |
| }, | |
| "error with source code subject": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Bad bad bad", | |
| Detail: "Whatever shall we do?", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| }, | |
| ` | |
| Error: Bad bad bad | |
| on test.tf line 1: | |
| 1: test source code | |
| Whatever shall we do? | |
| `, | |
| }, | |
| "error with source code subject and known expression": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Bad bad bad", | |
| Detail: "Whatever shall we do?", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| Expression: hcltest.MockExprTraversal(hcl.Traversal{ | |
| hcl.TraverseRoot{Name: "boop"}, | |
| hcl.TraverseAttr{Name: "beep"}, | |
| }), | |
| EvalContext: &hcl.EvalContext{ | |
| Variables: map[string]cty.Value{ | |
| "boop": cty.ObjectVal(map[string]cty.Value{ | |
| "beep": cty.StringVal("blah"), | |
| }), | |
| }, | |
| }, | |
| }, | |
| ` | |
| Error: Bad bad bad | |
| on test.tf line 1: | |
| 1: test source code | |
| ├──────────────── | |
| │ boop.beep is "blah" | |
| Whatever shall we do? | |
| `, | |
| }, | |
| "error with source code subject and expression referring to sensitive value": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Bad bad bad", | |
| Detail: "Whatever shall we do?", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| Expression: hcltest.MockExprTraversal(hcl.Traversal{ | |
| hcl.TraverseRoot{Name: "boop"}, | |
| hcl.TraverseAttr{Name: "beep"}, | |
| }), | |
| EvalContext: &hcl.EvalContext{ | |
| Variables: map[string]cty.Value{ | |
| "boop": cty.ObjectVal(map[string]cty.Value{ | |
| "beep": cty.StringVal("blah").Mark(marks.Sensitive), | |
| }), | |
| }, | |
| }, | |
| Extra: diagnosticCausedBySensitive(true), | |
| }, | |
| ` | |
| Error: Bad bad bad | |
| on test.tf line 1: | |
| 1: test source code | |
| ├──────────────── | |
| │ boop.beep has a sensitive value | |
| Whatever shall we do? | |
| `, | |
| }, | |
| "error with source code subject and expression referring to sensitive value when not related to sensitivity": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Bad bad bad", | |
| Detail: "Whatever shall we do?", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| Expression: hcltest.MockExprTraversal(hcl.Traversal{ | |
| hcl.TraverseRoot{Name: "boop"}, | |
| hcl.TraverseAttr{Name: "beep"}, | |
| }), | |
| EvalContext: &hcl.EvalContext{ | |
| Variables: map[string]cty.Value{ | |
| "boop": cty.ObjectVal(map[string]cty.Value{ | |
| "beep": cty.StringVal("blah").Mark(marks.Sensitive), | |
| }), | |
| }, | |
| }, | |
| }, | |
| ` | |
| Error: Bad bad bad | |
| on test.tf line 1: | |
| 1: test source code | |
| Whatever shall we do? | |
| `, | |
| }, | |
| "error with source code subject and unknown string expression": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Bad bad bad", | |
| Detail: "Whatever shall we do?", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| Expression: hcltest.MockExprTraversal(hcl.Traversal{ | |
| hcl.TraverseRoot{Name: "boop"}, | |
| hcl.TraverseAttr{Name: "beep"}, | |
| }), | |
| EvalContext: &hcl.EvalContext{ | |
| Variables: map[string]cty.Value{ | |
| "boop": cty.ObjectVal(map[string]cty.Value{ | |
| "beep": cty.UnknownVal(cty.String), | |
| }), | |
| }, | |
| }, | |
| Extra: diagnosticCausedByUnknown(true), | |
| }, | |
| ` | |
| Error: Bad bad bad | |
| on test.tf line 1: | |
| 1: test source code | |
| ├──────────────── | |
| │ boop.beep is a string, known only after apply | |
| Whatever shall we do? | |
| `, | |
| }, | |
| "error with source code subject and unknown string expression when problem isn't unknown-related": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Bad bad bad", | |
| Detail: "Whatever shall we do?", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| Expression: hcltest.MockExprTraversal(hcl.Traversal{ | |
| hcl.TraverseRoot{Name: "boop"}, | |
| hcl.TraverseAttr{Name: "beep"}, | |
| }), | |
| EvalContext: &hcl.EvalContext{ | |
| Variables: map[string]cty.Value{ | |
| "boop": cty.ObjectVal(map[string]cty.Value{ | |
| "beep": cty.UnknownVal(cty.String), | |
| }), | |
| }, | |
| }, | |
| }, | |
| ` | |
| Error: Bad bad bad | |
| on test.tf line 1: | |
| 1: test source code | |
| ├──────────────── | |
| │ boop.beep is a string | |
| Whatever shall we do? | |
| `, | |
| }, | |
| "error with source code subject and unknown expression of unknown type": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Bad bad bad", | |
| Detail: "Whatever shall we do?", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| Expression: hcltest.MockExprTraversal(hcl.Traversal{ | |
| hcl.TraverseRoot{Name: "boop"}, | |
| hcl.TraverseAttr{Name: "beep"}, | |
| }), | |
| EvalContext: &hcl.EvalContext{ | |
| Variables: map[string]cty.Value{ | |
| "boop": cty.ObjectVal(map[string]cty.Value{ | |
| "beep": cty.UnknownVal(cty.DynamicPseudoType), | |
| }), | |
| }, | |
| }, | |
| Extra: diagnosticCausedByUnknown(true), | |
| }, | |
| ` | |
| Error: Bad bad bad | |
| on test.tf line 1: | |
| 1: test source code | |
| ├──────────────── | |
| │ boop.beep will be known only after apply | |
| Whatever shall we do? | |
| `, | |
| }, | |
| "error with source code subject and unknown expression of unknown type when problem isn't unknown-related": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Bad bad bad", | |
| Detail: "Whatever shall we do?", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| Expression: hcltest.MockExprTraversal(hcl.Traversal{ | |
| hcl.TraverseRoot{Name: "boop"}, | |
| hcl.TraverseAttr{Name: "beep"}, | |
| }), | |
| EvalContext: &hcl.EvalContext{ | |
| Variables: map[string]cty.Value{ | |
| "boop": cty.ObjectVal(map[string]cty.Value{ | |
| "beep": cty.UnknownVal(cty.DynamicPseudoType), | |
| }), | |
| }, | |
| }, | |
| }, | |
| ` | |
| Error: Bad bad bad | |
| on test.tf line 1: | |
| 1: test source code | |
| Whatever shall we do? | |
| `, | |
| }, | |
| "warning from deprecation": { | |
| &hcl.Diagnostic{ | |
| Severity: hcl.DiagWarning, | |
| Summary: "Deprecation detected", | |
| Detail: "Countermeasures must be taken.", | |
| Subject: &hcl.Range{ | |
| Filename: "test.tf", | |
| Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, | |
| }, | |
| Extra: &tfdiags.DeprecationOriginDiagnosticExtra{ | |
| OriginDescription: "module.foo.bar", | |
| }, | |
| }, | |
| ` | |
| Warning: Deprecation detected | |
| on test.tf line 1: | |
| 1: test source code | |
| The deprecation originates from module.foo.bar | |
| Countermeasures must be taken. | |
| `, | |
| }, | |
| } | |
| sources := map[string][]byte{ | |
| "test.tf": []byte(`test source code`), | |
| "deprecated.tf": []byte(`source of deprecation`), | |
| } | |
| for name, test := range tests { | |
| t.Run(name, func(t *testing.T) { | |
| var diags tfdiags.Diagnostics | |
| diags = diags.Append(test.Diag) // to normalize it into a tfdiag.Diagnostic | |
| diag := diags[0] | |
| got := strings.TrimSpace(DiagnosticPlain(diag, sources, 40)) | |
| want := strings.TrimSpace(test.Want) | |
| if diff := cmp.Diff(got, want); diff != "" { | |
| t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\n,diff:\n%s\n\n", got, want, diff) | |
| } | |
| }) | |
| } | |
| } | |
| func TestDiagnosticWarningsCompact(t *testing.T) { | |
| var diags tfdiags.Diagnostics | |
| diags = diags.Append(tfdiags.SimpleWarning("foo")) | |
| diags = diags.Append(tfdiags.SimpleWarning("foo")) | |
| diags = diags.Append(tfdiags.SimpleWarning("bar")) | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagWarning, | |
| Summary: "source foo", | |
| Detail: "...", | |
| Subject: &hcl.Range{ | |
| Filename: "source.tf", | |
| Start: hcl.Pos{Line: 2, Column: 1, Byte: 5}, | |
| End: hcl.Pos{Line: 2, Column: 1, Byte: 5}, | |
| }, | |
| }) | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagWarning, | |
| Summary: "source foo", | |
| Detail: "...", | |
| Subject: &hcl.Range{ | |
| Filename: "source.tf", | |
| Start: hcl.Pos{Line: 3, Column: 1, Byte: 7}, | |
| End: hcl.Pos{Line: 3, Column: 1, Byte: 7}, | |
| }, | |
| }) | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagWarning, | |
| Summary: "source bar", | |
| Detail: "...", | |
| Subject: &hcl.Range{ | |
| Filename: "source2.tf", | |
| Start: hcl.Pos{Line: 1, Column: 1, Byte: 1}, | |
| End: hcl.Pos{Line: 1, Column: 1, Byte: 1}, | |
| }, | |
| }) | |
| // ConsolidateWarnings groups together the ones | |
| // that have source location information and that | |
| // have the same summary text. | |
| diags = diags.ConsolidateWarnings(1) | |
| // A zero-value Colorize just passes all the formatting | |
| // codes back to us, so we can test them literally. | |
| got := DiagnosticWarningsCompact(diags, &colorstring.Colorize{}) | |
| want := `[bold][yellow]Warnings:[reset] | |
| - foo | |
| - foo | |
| - bar | |
| - source foo | |
| on source.tf line 2 (and 1 more) | |
| - source bar | |
| on source2.tf line 1 | |
| ` | |
| if got != want { | |
| t.Errorf( | |
| "wrong result\ngot:\n%s\n\nwant:\n%s\n\ndiff:\n%s", | |
| got, want, cmp.Diff(want, got), | |
| ) | |
| } | |
| } | |
| // Test case via https://github.com/hashicorp/terraform/issues/21359 | |
| func TestDiagnostic_nonOverlappingHighlightContext(t *testing.T) { | |
| var diags tfdiags.Diagnostics | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Some error", | |
| Detail: "...", | |
| Subject: &hcl.Range{ | |
| Filename: "source.tf", | |
| Start: hcl.Pos{Line: 1, Column: 5, Byte: 5}, | |
| End: hcl.Pos{Line: 1, Column: 5, Byte: 5}, | |
| }, | |
| Context: &hcl.Range{ | |
| Filename: "source.tf", | |
| Start: hcl.Pos{Line: 1, Column: 5, Byte: 5}, | |
| End: hcl.Pos{Line: 4, Column: 2, Byte: 60}, | |
| }, | |
| }) | |
| sources := map[string][]byte{ | |
| "source.tf": []byte(`x = somefunc("testing", { | |
| alpha = "foo" | |
| beta = "bar" | |
| }) | |
| `), | |
| } | |
| color := &colorstring.Colorize{ | |
| Colors: colorstring.DefaultColors, | |
| Reset: true, | |
| Disable: true, | |
| } | |
| expected := `╷ | |
| │ Error: Some error | |
| │ | |
| │ on source.tf line 1: | |
| │ 1: x = somefunc("testing", { | |
| │ 2: alpha = "foo" | |
| │ 3: beta = "bar" | |
| │ 4: }) | |
| │ | |
| │ ... | |
| ╵ | |
| ` | |
| output := Diagnostic(diags[0], sources, color, 80) | |
| if output != expected { | |
| t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected) | |
| } | |
| } | |
| func TestDiagnostic_emptyOverlapHighlightContext(t *testing.T) { | |
| var diags tfdiags.Diagnostics | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Some error", | |
| Detail: "...", | |
| Subject: &hcl.Range{ | |
| Filename: "source.tf", | |
| Start: hcl.Pos{Line: 3, Column: 10, Byte: 38}, | |
| End: hcl.Pos{Line: 4, Column: 1, Byte: 39}, | |
| }, | |
| Context: &hcl.Range{ | |
| Filename: "source.tf", | |
| Start: hcl.Pos{Line: 2, Column: 13, Byte: 27}, | |
| End: hcl.Pos{Line: 4, Column: 1, Byte: 39}, | |
| }, | |
| }) | |
| sources := map[string][]byte{ | |
| "source.tf": []byte(`variable "x" { | |
| default = { | |
| "foo" | |
| } | |
| `), | |
| } | |
| color := &colorstring.Colorize{ | |
| Colors: colorstring.DefaultColors, | |
| Reset: true, | |
| Disable: true, | |
| } | |
| expected := `╷ | |
| │ Error: Some error | |
| │ | |
| │ on source.tf line 3, in variable "x": | |
| │ 2: default = { | |
| │ 3: "foo" | |
| │ 4: } | |
| │ | |
| │ ... | |
| ╵ | |
| ` | |
| output := Diagnostic(diags[0], sources, color, 80) | |
| if output != expected { | |
| t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected) | |
| } | |
| } | |
| func TestDiagnosticPlain_emptyOverlapHighlightContext(t *testing.T) { | |
| var diags tfdiags.Diagnostics | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Some error", | |
| Detail: "...", | |
| Subject: &hcl.Range{ | |
| Filename: "source.tf", | |
| Start: hcl.Pos{Line: 3, Column: 10, Byte: 38}, | |
| End: hcl.Pos{Line: 4, Column: 1, Byte: 39}, | |
| }, | |
| Context: &hcl.Range{ | |
| Filename: "source.tf", | |
| Start: hcl.Pos{Line: 2, Column: 13, Byte: 27}, | |
| End: hcl.Pos{Line: 4, Column: 1, Byte: 39}, | |
| }, | |
| }) | |
| sources := map[string][]byte{ | |
| "source.tf": []byte(`variable "x" { | |
| default = { | |
| "foo" | |
| } | |
| `), | |
| } | |
| expected := ` | |
| Error: Some error | |
| on source.tf line 3, in variable "x": | |
| 2: default = { | |
| 3: "foo" | |
| 4: } | |
| ... | |
| ` | |
| output := DiagnosticPlain(diags[0], sources, 80) | |
| if output != expected { | |
| t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected) | |
| } | |
| } | |
| func TestDiagnostic_wrapDetailIncludingCommand(t *testing.T) { | |
| var diags tfdiags.Diagnostics | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Everything went wrong", | |
| Detail: "This is a very long sentence about whatever went wrong which is supposed to wrap onto multiple lines. Thank-you very much for listening.\n\nTo fix this, run this very long command:\n terraform read-my-mind -please -thanks -but-do-not-wrap-this-line-because-it-is-prefixed-with-spaces\n\nHere is a coda which is also long enough to wrap and so it should eventually make it onto multiple lines. THE END", | |
| }) | |
| color := &colorstring.Colorize{ | |
| Colors: colorstring.DefaultColors, | |
| Reset: true, | |
| Disable: true, | |
| } | |
| expected := `╷ | |
| │ Error: Everything went wrong | |
| │ | |
| │ This is a very long sentence about whatever went wrong which is supposed | |
| │ to wrap onto multiple lines. Thank-you very much for listening. | |
| │ | |
| │ To fix this, run this very long command: | |
| │ terraform read-my-mind -please -thanks -but-do-not-wrap-this-line-because-it-is-prefixed-with-spaces | |
| │ | |
| │ Here is a coda which is also long enough to wrap and so it should | |
| │ eventually make it onto multiple lines. THE END | |
| ╵ | |
| ` | |
| output := Diagnostic(diags[0], nil, color, 76) | |
| if output != expected { | |
| t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected) | |
| } | |
| } | |
| func TestDiagnosticPlain_wrapDetailIncludingCommand(t *testing.T) { | |
| var diags tfdiags.Diagnostics | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Everything went wrong", | |
| Detail: "This is a very long sentence about whatever went wrong which is supposed to wrap onto multiple lines. Thank-you very much for listening.\n\nTo fix this, run this very long command:\n terraform read-my-mind -please -thanks -but-do-not-wrap-this-line-because-it-is-prefixed-with-spaces\n\nHere is a coda which is also long enough to wrap and so it should eventually make it onto multiple lines. THE END", | |
| }) | |
| expected := ` | |
| Error: Everything went wrong | |
| This is a very long sentence about whatever went wrong which is supposed to | |
| wrap onto multiple lines. Thank-you very much for listening. | |
| To fix this, run this very long command: | |
| terraform read-my-mind -please -thanks -but-do-not-wrap-this-line-because-it-is-prefixed-with-spaces | |
| Here is a coda which is also long enough to wrap and so it should | |
| eventually make it onto multiple lines. THE END | |
| ` | |
| output := DiagnosticPlain(diags[0], nil, 76) | |
| if output != expected { | |
| t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected) | |
| } | |
| } | |
| func TestDiagnosticPlainFromJSON_policySnippet(t *testing.T) { | |
| tests := map[string]struct { | |
| diag *viewsjson.Diagnostic | |
| want string | |
| }{ | |
| "with policy range, policy snippet, and range": { | |
| diag: &viewsjson.Diagnostic{ | |
| Severity: viewsjson.DiagnosticSeverityError, | |
| Summary: "policy denied", | |
| PolicyRange: &viewsjson.DiagnosticRange{ | |
| Filename: "policy_file.tfpolicy.hcl", | |
| Start: viewsjson.Pos{Line: 1}, | |
| }, | |
| PolicySnippet: &viewsjson.DiagnosticSnippet{ | |
| Code: `resource_policy "resource_type" "policy_name" {`, | |
| StartLine: 1, | |
| HighlightStartOffset: 0, | |
| HighlightEndOffset: 48, | |
| }, | |
| Range: &viewsjson.DiagnosticRange{ | |
| Filename: "main.tf", | |
| Start: viewsjson.Pos{Line: 1}, | |
| }, | |
| Snippet: &viewsjson.DiagnosticSnippet{ | |
| Code: `resource "test_instance" "foo" {`, | |
| StartLine: 1, | |
| HighlightStartOffset: 0, | |
| HighlightEndOffset: 30, | |
| }, | |
| }, | |
| want: ` | |
| Error: policy denied | |
| on policy_file.tfpolicy.hcl line 1: | |
| 1: resource_policy "resource_type" "policy_name" { | |
| while evaluating policy for main.tf line 1: | |
| 1: resource "test_instance" "foo" { | |
| `, | |
| }, | |
| "with policy range and policy snippet but no range": { | |
| diag: &viewsjson.Diagnostic{ | |
| Severity: viewsjson.DiagnosticSeverityError, | |
| Summary: "policy denied", | |
| PolicyRange: &viewsjson.DiagnosticRange{ | |
| Filename: "policy_file.tfpolicy.hcl", | |
| Start: viewsjson.Pos{Line: 1}, | |
| }, | |
| PolicySnippet: &viewsjson.DiagnosticSnippet{ | |
| Code: `resource_policy "resource_type" "policy_name" {`, | |
| StartLine: 1, | |
| HighlightStartOffset: 0, | |
| HighlightEndOffset: 48, | |
| }, | |
| }, | |
| want: ` | |
| Error: policy denied | |
| on policy_file.tfpolicy.hcl line 1: | |
| 1: resource_policy "resource_type" "policy_name" { | |
| `, | |
| }, | |
| } | |
| for name, tc := range tests { | |
| t.Run(name, func(t *testing.T) { | |
| got := strings.TrimSpace(DiagnosticPlainFromJSON(tc.diag, 0)) | |
| want := strings.TrimSpace(tc.want) | |
| if diff := cmp.Diff(want, got); diff != "" { | |
| t.Fatalf("unexpected output diff:\n%s", diff) | |
| } | |
| }) | |
| } | |
| } | |
| // Test cases covering invalid JSON diagnostics which should still render | |
| // correctly. These JSON diagnostic values cannot be generated from the | |
| // json.NewDiagnostic code path, but we may read and display JSON diagnostics | |
| // in future from other sources. | |
| func TestDiagnosticFromJSON_invalid(t *testing.T) { | |
| tests := map[string]struct { | |
| Diag *viewsjson.Diagnostic | |
| Want string | |
| }{ | |
| "zero-value end range and highlight end byte": { | |
| &viewsjson.Diagnostic{ | |
| Severity: viewsjson.DiagnosticSeverityError, | |
| Summary: "Bad end", | |
| Detail: "It all went wrong.", | |
| Range: &viewsjson.DiagnosticRange{ | |
| Filename: "ohno.tf", | |
| Start: viewsjson.Pos{Line: 1, Column: 23, Byte: 22}, | |
| End: viewsjson.Pos{Line: 0, Column: 0, Byte: 0}, | |
| }, | |
| Snippet: &viewsjson.DiagnosticSnippet{ | |
| Code: `resource "foo_bar "baz" {`, | |
| StartLine: 1, | |
| HighlightStartOffset: 22, | |
| HighlightEndOffset: 0, | |
| }, | |
| }, | |
| `[red]╷[reset] | |
| [red]│[reset] [bold][red]Error: [reset][bold]Bad end[reset] | |
| [red]│[reset] | |
| [red]│[reset] on ohno.tf line 1: | |
| [red]│[reset] 1: resource "foo_bar "baz[underline]"[reset] { | |
| [red]│[reset] | |
| [red]│[reset] It all went wrong. | |
| [red]╵[reset] | |
| `, | |
| }, | |
| } | |
| // This empty Colorize just passes through all of the formatting codes | |
| // untouched, because it doesn't define any formatting keywords. | |
| colorize := &colorstring.Colorize{} | |
| for name, test := range tests { | |
| t.Run(name, func(t *testing.T) { | |
| got := strings.TrimSpace(DiagnosticFromJSON(test.Diag, colorize, 40)) | |
| want := strings.TrimSpace(test.Want) | |
| if got != want { | |
| t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\n", got, want) | |
| } | |
| }) | |
| } | |
| } | |
| func TestJsonDiff(t *testing.T) { | |
| f := &snippetFormatter{ | |
| buf: &bytes.Buffer{}, | |
| color: &colorstring.Colorize{ | |
| Reset: true, | |
| Disable: true, | |
| }, | |
| } | |
| tests := []struct { | |
| name string | |
| strA string | |
| strB string | |
| diff string | |
| }{ | |
| { | |
| name: "Basic different fields", | |
| strA: `{ | |
| "field1": "value1", | |
| "field2": "value2", | |
| "field3": "value3", | |
| "field4": "value4" | |
| }`, | |
| strB: `{ | |
| "field1": "value1", | |
| "field2": "different", | |
| "field3": "value3", | |
| "field4": "value4" | |
| }`, | |
| diff: ` │ Diff: | |
| │ --- actual | |
| │ +++ expected | |
| │ { | |
| │ "field1": "value1", | |
| │ - "field2": "value2", | |
| │ + "field2": "different", | |
| │ "field3": "value3", | |
| │ "field4": "value4" | |
| │ } | |
| `, | |
| }, | |
| { | |
| name: "Unequal number of fields", | |
| strA: `{ | |
| "field1": "value1", | |
| "field2": "value2", | |
| "field3": "value3", | |
| "extraField": "extraValue", | |
| "field4": "value4" | |
| }`, | |
| strB: `{ | |
| "field1": "value1", | |
| "fieldX": "valueX", | |
| "fieldY": "valueY", | |
| "field4": "value4" | |
| }`, | |
| diff: ` │ Diff: | |
| │ --- actual | |
| │ +++ expected | |
| │ { | |
| │ "field1": "value1", | |
| │ - "field2": "value2", | |
| │ - "field3": "value3", | |
| │ - "extraField": "extraValue", | |
| │ - "field4": "value4" | |
| │ - } | |
| │ + "fieldX": "valueX", | |
| │ + "fieldY": "valueY", | |
| │ + "field4": "value4" | |
| │ + } | |
| `, | |
| }, | |
| { | |
| name: "Empty vs non-empty JSON", | |
| strA: `{}`, | |
| strB: `{ | |
| "field1": "value1", | |
| "field2": "value2" | |
| }`, | |
| diff: ` │ Diff: | |
| │ --- actual | |
| │ +++ expected | |
| │ - {} | |
| │ + { | |
| │ + "field1": "value1", | |
| │ + "field2": "value2" | |
| │ + } | |
| `, | |
| }, | |
| { | |
| name: "Completely different JSONs", | |
| strA: `{ | |
| "a": 1, | |
| "b": 2 | |
| }`, | |
| strB: `{ | |
| "c": 3, | |
| "d": 4 | |
| }`, | |
| diff: ` │ Diff: | |
| │ --- actual | |
| │ +++ expected | |
| │ { | |
| │ - "a": 1, | |
| │ - "b": 2 | |
| │ + "c": 3, | |
| │ + "d": 4 | |
| │ } | |
| `, | |
| }, | |
| { | |
| name: "Nested objects with differences", | |
| strA: `{ | |
| "outer": { | |
| "inner1": "value1", | |
| "inner2": "value2" | |
| } | |
| }`, | |
| strB: `{ | |
| "outer": { | |
| "inner1": "changed", | |
| "inner2": "value2" | |
| } | |
| }`, | |
| diff: ` │ Diff: | |
| │ --- actual | |
| │ +++ expected | |
| │ { | |
| │ "outer": { | |
| │ - "inner1": "value1", | |
| │ + "inner1": "changed", | |
| │ "inner2": "value2" | |
| │ } | |
| │ } | |
| `, | |
| }, | |
| { | |
| name: "Multiple separate diff blocks", | |
| strA: `{ | |
| "block1": "original1", | |
| "unchanged1": "same", | |
| "block2": "original2", | |
| "unchanged2": "same", | |
| "block3": "original3" | |
| }`, | |
| strB: `{ | |
| "block1": "changed1", | |
| "unchanged1": "same", | |
| "block2": "changed2", | |
| "unchanged2": "same", | |
| "block3": "changed3" | |
| }`, | |
| diff: ` │ Diff: | |
| │ --- actual | |
| │ +++ expected | |
| │ { | |
| │ - "block1": "original1", | |
| │ + "block1": "changed1", | |
| │ "unchanged1": "same", | |
| │ - "block2": "original2", | |
| │ + "block2": "changed2", | |
| │ "unchanged2": "same", | |
| │ - "block3": "original3" | |
| │ + "block3": "changed3" | |
| │ } | |
| `, | |
| }, | |
| { | |
| name: "Large number of differences", | |
| strA: `{ | |
| "item1": "a", | |
| "item2": "b", | |
| "item3": "c", | |
| "item4": "d", | |
| "item5": "e", | |
| "item6": "f", | |
| "item7": "g" | |
| }`, | |
| strB: `{ | |
| "item1": "a", | |
| "item2": "B", | |
| "item3": "C", | |
| "item4": "D", | |
| "item5": "e", | |
| "item6": "F", | |
| "item7": "g" | |
| }`, | |
| diff: ` │ Diff: | |
| │ --- actual | |
| │ +++ expected | |
| │ { | |
| │ "item1": "a", | |
| │ - "item2": "b", | |
| │ - "item3": "c", | |
| │ - "item4": "d", | |
| │ + "item2": "B", | |
| │ + "item3": "C", | |
| │ + "item4": "D", | |
| │ "item5": "e", | |
| │ - "item6": "f", | |
| │ + "item6": "F", | |
| │ "item7": "g" | |
| │ } | |
| `, | |
| }, | |
| { | |
| name: "Identical JSONs", | |
| strA: `{"field": "value"}`, | |
| strB: `{"field": "value"}`, | |
| diff: ``, // No output expected for identical JSONs | |
| }, | |
| { | |
| name: "simple: no matches", | |
| strA: `1`, | |
| strB: `2`, | |
| diff: ` │ Diff: | |
| │ --- actual | |
| │ +++ expected | |
| │ - 1 | |
| │ + 2 | |
| `, | |
| }, | |
| } | |
| for _, test := range tests { | |
| t.Run(test.name, func(t *testing.T) { | |
| f.buf.Reset() | |
| f.printJSONDiff(test.strA, test.strB) | |
| diff := regexp.MustCompile(`\[[^\]]+\]`).ReplaceAllString(f.buf.String(), "") | |
| fmt.Println(diff) | |
| if d := cmp.Diff(diff, test.diff); d != "" { | |
| t.Errorf("diff mismatch: got %s\n, want %s\n: diff: %s\n", diff, test.diff, d) | |
| } | |
| }) | |
| } | |
| } | |
| // fakeDiagFunctionCallExtra is a fake implementation of the interface that | |
| // HCL uses to provide "extra information" associated with diagnostics that | |
| // describe errors during a function call. | |
| type fakeDiagFunctionCallExtra string | |
| var _ hclsyntax.FunctionCallDiagExtra = fakeDiagFunctionCallExtra("") | |
| func (e fakeDiagFunctionCallExtra) CalledFunctionName() string { | |
| return string(e) | |
| } | |
| func (e fakeDiagFunctionCallExtra) FunctionCallError() error { | |
| return nil | |
| } | |
| // diagnosticCausedByUnknown is a testing helper for exercising our logic | |
| // for selectively showing unknown values alongside our source snippets for | |
| // diagnostics that are explicitly marked as being caused by unknown values. | |
| type diagnosticCausedByUnknown bool | |
| var _ tfdiags.DiagnosticExtraBecauseUnknown = diagnosticCausedByUnknown(true) | |
| func (e diagnosticCausedByUnknown) DiagnosticCausedByUnknown() bool { | |
| return bool(e) | |
| } | |
| // diagnosticCausedBySensitive is a testing helper for exercising our logic | |
| // for selectively showing sensitive values alongside our source snippets for | |
| // diagnostics that are explicitly marked as being caused by sensitive values. | |
| type diagnosticCausedBySensitive bool | |
| var _ tfdiags.DiagnosticExtraBecauseSensitive = diagnosticCausedBySensitive(true) | |
| func (e diagnosticCausedBySensitive) DiagnosticCausedBySensitive() bool { | |
| return bool(e) | |
| } | |
| var _ tfdiags.DiagnosticExtraCausedByTestFailure = diagnosticCausedByTestFailure{} | |
| type diagnosticCausedByTestFailure struct { | |
| Verbose bool | |
| } | |
| func (e diagnosticCausedByTestFailure) DiagnosticCausedByTestFailure() bool { | |
| return true | |
| } | |
| func (e diagnosticCausedByTestFailure) IsTestVerboseMode() bool { | |
| return e.Verbose | |
| } |