mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-25 01:00:16 -05:00
Better output for test assertions (#3009)
Signed-off-by: Diogenes Fernandes <diofeher@gmail.com>
This commit is contained in:
committed by
GitHub
parent
b6b1573482
commit
f95ca42871
@@ -14,6 +14,10 @@ import (
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/opentofu/opentofu/internal/command/jsonentities"
|
||||
"github.com/opentofu/opentofu/internal/command/jsonformat/computed"
|
||||
"github.com/opentofu/opentofu/internal/command/jsonformat/differ"
|
||||
"github.com/opentofu/opentofu/internal/command/jsonformat/structured"
|
||||
"github.com/opentofu/opentofu/internal/command/jsonformat/structured/attribute_path"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
|
||||
"github.com/mitchellh/colorstring"
|
||||
@@ -123,6 +127,31 @@ func DiagnosticFromJSON(diag *jsonentities.Diagnostic, color *colorstring.Colori
|
||||
return ruleBuf.String()
|
||||
}
|
||||
|
||||
// appendDifferenceOutput is used to create colored and aligned lines to be used
|
||||
// on the test suite assertions
|
||||
func appendDifferenceOutput(buf *bytes.Buffer, diag *jsonentities.Diagnostic, color *colorstring.Colorize) {
|
||||
if diag.Difference == nil {
|
||||
return
|
||||
}
|
||||
|
||||
change := structured.FromJsonChange(*diag.Difference, attribute_path.AlwaysMatcher())
|
||||
differed := differ.ComputeDiffForOutput(change)
|
||||
out := differed.RenderHuman(0, computed.RenderHumanOpts{Colorize: color})
|
||||
// The next line is needed in order to make the output aligned, since the first line
|
||||
// rendered by RenderHuman is on column 0, but all the subsequent lines are having 4 spaces before
|
||||
// the actual content.
|
||||
out = fmt.Sprintf(" %s", out)
|
||||
|
||||
fmt.Fprint(buf, color.Color(" [dark_gray]├────────────────[reset]\n"))
|
||||
fmt.Fprint(buf, color.Color(" [dark_gray]│[reset] [bold]Diff: [reset]"))
|
||||
lines := strings.Split(out, "\n")
|
||||
for line := range lines {
|
||||
buf.WriteString(color.Color("\n [dark_gray]│[reset] "))
|
||||
buf.WriteString(lines[line])
|
||||
}
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
|
||||
// DiagnosticPlain is an alternative to Diagnostic which minimises the use of
|
||||
// virtual terminal formatting sequences.
|
||||
//
|
||||
@@ -319,7 +348,7 @@ func appendSourceSnippets(buf *bytes.Buffer, diag *jsonentities.Diagnostic, colo
|
||||
fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] [bold]%s[reset] %s\n"), value.Traversal, value.Statement)
|
||||
}
|
||||
}
|
||||
appendDifferenceOutput(buf, diag, color)
|
||||
}
|
||||
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
|
||||
@@ -265,12 +265,107 @@ func TestDiagnostic(t *testing.T) {
|
||||
[red]│[reset]
|
||||
[red]│[reset] Whatever shall we do?
|
||||
[red]╵[reset]
|
||||
`,
|
||||
},
|
||||
"test number assertion difference": {
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Bad testing",
|
||||
Detail: "Number testing went wrong.",
|
||||
Expression: &hclsyntax.BinaryOpExpr{
|
||||
LHS: &hclsyntax.LiteralValueExpr{
|
||||
Val: cty.StringVal("3"),
|
||||
},
|
||||
RHS: &hclsyntax.LiteralValueExpr{
|
||||
Val: cty.StringVal("5"),
|
||||
},
|
||||
Op: hclsyntax.OpEqual,
|
||||
},
|
||||
Subject: &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{},
|
||||
},
|
||||
`[red]╷[reset]
|
||||
[red]│[reset] [bold][red]Error: [reset][bold]Bad testing[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]Diff: [reset]
|
||||
[red]│[reset] [dark_gray]│[reset] "3" [yellow]->[reset] "5"
|
||||
[red]│[reset]
|
||||
[red]│[reset] Number testing went wrong.
|
||||
[red]╵[reset]
|
||||
`,
|
||||
},
|
||||
"test object assertion difference": {
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Bad testing",
|
||||
Detail: "Test object assertion!",
|
||||
Expression: &hclsyntax.BinaryOpExpr{
|
||||
LHS: &hclsyntax.ScopeTraversalExpr{
|
||||
Traversal: hcl.Traversal{
|
||||
hcl.TraverseRoot{Name: "var"},
|
||||
hcl.TraverseAttr{Name: "json_headers"},
|
||||
},
|
||||
},
|
||||
RHS: &hclsyntax.LiteralValueExpr{
|
||||
Val: cty.ObjectVal(map[string]cty.Value{
|
||||
"Test-Header-1": cty.StringVal("foo"),
|
||||
"Test-Header-2": cty.StringVal("bar"),
|
||||
}),
|
||||
},
|
||||
Op: hclsyntax.OpEqual,
|
||||
},
|
||||
Subject: &hcl.Range{
|
||||
Filename: "json_encode.tf",
|
||||
Start: hcl.Pos{Line: 1, Column: 12, Byte: 12},
|
||||
End: hcl.Pos{Line: 4, Column: 20, Byte: 150},
|
||||
},
|
||||
EvalContext: &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"var": cty.ObjectVal(map[string]cty.Value{
|
||||
"json_headers": cty.ObjectVal(map[string]cty.Value{
|
||||
"Test-Header-1": cty.StringVal("foo"),
|
||||
"Test-Header-2": cty.StringVal("foo"),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
`[red]╷[reset]
|
||||
[red]│[reset] [bold][red]Error: [reset][bold]Bad testing[reset]
|
||||
[red]│[reset]
|
||||
[red]│[reset] on json_encode.tf line 1:
|
||||
[red]│[reset] 1: condition = [underline]jsonencode(var.json_headers) == jsonencode([
|
||||
[red]│[reset] 2: "Test-Header-1: foo",
|
||||
[red]│[reset] 3: "Test-Header-2: bar"
|
||||
[red]│[reset] 4: ])[reset]
|
||||
[red]│[reset] [dark_gray]├────────────────[reset]
|
||||
[red]│[reset] [dark_gray]│[reset] [bold]var.json_headers[reset] is object with 2 attributes
|
||||
[red]│[reset] [dark_gray]├────────────────[reset]
|
||||
[red]│[reset] [dark_gray]│[reset] [bold]Diff: [reset]
|
||||
[red]│[reset] [dark_gray]│[reset] {
|
||||
[red]│[reset] [dark_gray]│[reset] [yellow]~[reset] Test-Header-2 = "foo" [yellow]->[reset] "bar"
|
||||
[red]│[reset] [dark_gray]│[reset] [dark_gray]# (1 unchanged attribute hidden)[reset]
|
||||
[red]│[reset] [dark_gray]│[reset] }
|
||||
[red]│[reset]
|
||||
[red]│[reset] Test object assertion!
|
||||
[red]╵[reset]
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
sources := map[string]*hcl.File{
|
||||
"test.tf": {Bytes: []byte(`test source code`)},
|
||||
"json_encode.tf": {Bytes: []byte(`condition = jsonencode(var.json_headers) == jsonencode([
|
||||
"Test-Header-1: foo",
|
||||
"Test-Header-2: bar"
|
||||
])`)},
|
||||
}
|
||||
|
||||
// This empty Colorize just passes through all of the formatting codes
|
||||
@@ -284,8 +379,8 @@ func TestDiagnostic(t *testing.T) {
|
||||
diag := diags[0]
|
||||
got := strings.TrimSpace(Diagnostic(diag, sources, 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)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("diff:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -588,8 +683,8 @@ Whatever shall we do?
|
||||
diag := diags[0]
|
||||
got := strings.TrimSpace(DiagnosticPlain(diag, sources, 40))
|
||||
want := strings.TrimSpace(test.Want)
|
||||
if got != want {
|
||||
t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\n", got, want)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("diff:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -702,8 +797,8 @@ func TestDiagnostic_nonOverlappingHighlightContext(t *testing.T) {
|
||||
`
|
||||
output := Diagnostic(diags[0], sources, color, 80)
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected)
|
||||
if diff := cmp.Diff(output, expected); diff != "" {
|
||||
t.Errorf("diff:\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -750,8 +845,8 @@ func TestDiagnostic_emptyOverlapHighlightContext(t *testing.T) {
|
||||
`
|
||||
output := Diagnostic(diags[0], sources, color, 80)
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected)
|
||||
if diff := cmp.Diff(output, expected); diff != "" {
|
||||
t.Errorf("diff:\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -793,8 +888,8 @@ Error: Some error
|
||||
`
|
||||
output := DiagnosticPlain(diags[0], sources, 80)
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected)
|
||||
if diff := cmp.Diff(output, expected); diff != "" {
|
||||
t.Errorf("diff:\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,8 +921,8 @@ func TestDiagnostic_wrapDetailIncludingCommand(t *testing.T) {
|
||||
`
|
||||
output := Diagnostic(diags[0], nil, color, 76)
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected)
|
||||
if diff := cmp.Diff(output, expected); diff != "" {
|
||||
t.Errorf("diff:\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -854,8 +949,8 @@ 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)
|
||||
if diff := cmp.Diff(output, expected); diff != "" {
|
||||
t.Errorf("diff:\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -905,8 +1000,8 @@ func TestDiagnosticFromJSON_invalid(t *testing.T) {
|
||||
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)
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Errorf("wrong result\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/command/jsonplan"
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
@@ -36,12 +37,13 @@ const (
|
||||
// range field.
|
||||
|
||||
type Diagnostic struct {
|
||||
Severity string `json:"severity"`
|
||||
Summary string `json:"summary"`
|
||||
Detail string `json:"detail"`
|
||||
Address string `json:"address,omitempty"`
|
||||
Range *DiagnosticRange `json:"range,omitempty"`
|
||||
Snippet *DiagnosticSnippet `json:"snippet,omitempty"`
|
||||
Severity string `json:"severity"`
|
||||
Summary string `json:"summary"`
|
||||
Detail string `json:"detail"`
|
||||
Address string `json:"address,omitempty"`
|
||||
Range *DiagnosticRange `json:"range,omitempty"`
|
||||
Snippet *DiagnosticSnippet `json:"snippet,omitempty"`
|
||||
Difference *jsonplan.Change `json:"difference,omitempty"`
|
||||
}
|
||||
|
||||
// Pos represents a position in the source code.
|
||||
@@ -162,14 +164,17 @@ func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string]*hcl.File) *Diagn
|
||||
snippet.FunctionCall = newDiagnosticSnippetFunctionCall(diag)
|
||||
}
|
||||
|
||||
difference := newDiagnosticDifference(diag)
|
||||
|
||||
desc := diag.Description()
|
||||
return &Diagnostic{
|
||||
Severity: sev,
|
||||
Summary: desc.Summary,
|
||||
Detail: desc.Detail,
|
||||
Address: desc.Address,
|
||||
Range: newDiagnosticRange(highlightRange),
|
||||
Snippet: snippet,
|
||||
Severity: sev,
|
||||
Summary: desc.Summary,
|
||||
Detail: desc.Detail,
|
||||
Address: desc.Address,
|
||||
Range: newDiagnosticRange(highlightRange),
|
||||
Snippet: snippet,
|
||||
Difference: difference,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +345,35 @@ func newDiagnosticSnippet(snippetRange, highlightRange *tfdiags.SourceRange, sou
|
||||
return ret
|
||||
}
|
||||
|
||||
// newDiagnosticDifference covers expressions in the binary
|
||||
// expression operation format of x == y.
|
||||
// It's used to create a pretty and descriptive output for the test suite assertions.
|
||||
// For context: https://github.com/opentofu/opentofu/issues/2545
|
||||
func newDiagnosticDifference(diag tfdiags.Diagnostic) *jsonplan.Change {
|
||||
fromExpr := diag.FromExpr()
|
||||
if fromExpr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
expr := fromExpr.Expression
|
||||
ctx := fromExpr.EvalContext
|
||||
binExpr, ok := expr.(*hclsyntax.BinaryOpExpr)
|
||||
if !ok || binExpr.Op != hclsyntax.OpEqual {
|
||||
// We're not dealing with a binary op expr, return it early
|
||||
return nil
|
||||
}
|
||||
|
||||
lhs, _ := binExpr.LHS.Value(ctx)
|
||||
rhs, _ := binExpr.RHS.Value(ctx)
|
||||
change, err := jsonplan.GenerateChange(lhs, rhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return change
|
||||
|
||||
}
|
||||
|
||||
func newDiagnosticExpressionValues(diag tfdiags.Diagnostic) []DiagnosticExpressionValue {
|
||||
fromExpr := diag.FromExpr()
|
||||
if fromExpr == nil {
|
||||
|
||||
@@ -16,7 +16,9 @@ import (
|
||||
|
||||
"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/opentofu/opentofu/internal/command/jsonplan"
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
@@ -50,6 +52,25 @@ func TestNewDiagnostic(t *testing.T) {
|
||||
var.k,
|
||||
]
|
||||
`)},
|
||||
"test.tftest.hcl": {Bytes: []byte(`run "fails_without_useful_diff" {
|
||||
command = plan
|
||||
plan_options {
|
||||
refresh = false
|
||||
}
|
||||
assert {
|
||||
condition = 3 == 5
|
||||
error_message = "Error."
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = jsonencode(var.json_headers) == jsonencode([
|
||||
"Test-Header-1: foo",
|
||||
"Test-Header-2: bar",
|
||||
])
|
||||
error_message = "Error."
|
||||
}
|
||||
}
|
||||
`)},
|
||||
}
|
||||
testCases := map[string]struct {
|
||||
diag interface{} // allow various kinds of diags
|
||||
@@ -806,6 +827,136 @@ func TestNewDiagnostic(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
"error with integer binary comparisons": {
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Test assertion failed",
|
||||
Subject: &hcl.Range{
|
||||
Filename: "test.tftest.hcl",
|
||||
Start: hcl.Pos{Line: 7, Column: 17, Byte: 118},
|
||||
End: hcl.Pos{Line: 7, Column: 23, Byte: 125},
|
||||
},
|
||||
Expression: &hclsyntax.BinaryOpExpr{
|
||||
LHS: &hclsyntax.LiteralValueExpr{
|
||||
Val: cty.StringVal("3"),
|
||||
},
|
||||
RHS: &hclsyntax.LiteralValueExpr{
|
||||
Val: cty.StringVal("5"),
|
||||
},
|
||||
Op: hclsyntax.OpEqual,
|
||||
},
|
||||
EvalContext: &hcl.EvalContext{},
|
||||
},
|
||||
&Diagnostic{
|
||||
Severity: "error",
|
||||
Summary: "Test assertion failed",
|
||||
Range: &DiagnosticRange{
|
||||
Filename: "test.tftest.hcl",
|
||||
Start: Pos{
|
||||
Line: 7,
|
||||
Column: 17,
|
||||
Byte: 118,
|
||||
},
|
||||
End: Pos{
|
||||
Line: 7,
|
||||
Column: 23,
|
||||
Byte: 125,
|
||||
},
|
||||
},
|
||||
Snippet: &DiagnosticSnippet{
|
||||
Context: strPtr(`run "fails_without_useful_diff"`),
|
||||
Code: (" condition = 3 == 5"),
|
||||
StartLine: (7),
|
||||
HighlightStartOffset: (15),
|
||||
HighlightEndOffset: (22),
|
||||
Values: []DiagnosticExpressionValue{},
|
||||
},
|
||||
Difference: &jsonplan.Change{
|
||||
Before: json.RawMessage(`"3"`),
|
||||
After: json.RawMessage(`"5"`),
|
||||
AfterUnknown: json.RawMessage(`false`),
|
||||
AfterSensitive: json.RawMessage(`false`),
|
||||
BeforeSensitive: json.RawMessage(`false`),
|
||||
ReplacePaths: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
"error with object binary comparisons": {
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "object assertion failed",
|
||||
Subject: &hcl.Range{
|
||||
Filename: "test.tftest.hcl",
|
||||
Start: hcl.Pos{Line: 12, Column: 17, Byte: 171},
|
||||
End: hcl.Pos{Line: 20, Column: 6, Byte: 288},
|
||||
},
|
||||
Expression: &hclsyntax.BinaryOpExpr{
|
||||
LHS: &hclsyntax.ScopeTraversalExpr{
|
||||
Traversal: hcl.Traversal{
|
||||
hcl.TraverseRoot{Name: "var"},
|
||||
hcl.TraverseAttr{Name: "json_headers"},
|
||||
},
|
||||
},
|
||||
RHS: &hclsyntax.LiteralValueExpr{
|
||||
Val: cty.ObjectVal(map[string]cty.Value{
|
||||
"Test-Header-1": cty.StringVal("foo"),
|
||||
"Test-Header-2": cty.StringVal("bar"),
|
||||
}),
|
||||
},
|
||||
Op: hclsyntax.OpEqual,
|
||||
},
|
||||
EvalContext: &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"var": cty.ObjectVal(map[string]cty.Value{
|
||||
"json_headers": cty.ObjectVal(map[string]cty.Value{
|
||||
"Test-Header-1": cty.StringVal("foo"),
|
||||
"Test-Header-2": cty.StringVal("foo"),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
&Diagnostic{
|
||||
Severity: "error",
|
||||
Summary: "object assertion failed",
|
||||
Range: &DiagnosticRange{
|
||||
Filename: "test.tftest.hcl",
|
||||
Start: Pos{
|
||||
Line: 12,
|
||||
Column: 17,
|
||||
Byte: 171,
|
||||
},
|
||||
End: Pos{
|
||||
Line: 20,
|
||||
Column: 6,
|
||||
Byte: 288,
|
||||
},
|
||||
},
|
||||
Snippet: &DiagnosticSnippet{
|
||||
Context: strPtr(`run "fails_without_useful_diff"`),
|
||||
Code: (` condition = jsonencode(var.json_headers) == jsonencode([
|
||||
"Test-Header-1: foo",
|
||||
"Test-Header-2: bar",
|
||||
])`),
|
||||
StartLine: (12),
|
||||
HighlightStartOffset: (0),
|
||||
HighlightEndOffset: (117),
|
||||
Values: []DiagnosticExpressionValue{
|
||||
{
|
||||
Traversal: "var.json_headers", Statement: "is object with 2 attributes",
|
||||
},
|
||||
},
|
||||
},
|
||||
Difference: &jsonplan.Change{
|
||||
Before: json.RawMessage(`{"Test-Header-1":"foo","Test-Header-2":"foo"}`),
|
||||
After: json.RawMessage(`{"Test-Header-1":"foo","Test-Header-2":"bar"}`),
|
||||
AfterUnknown: json.RawMessage(`false`),
|
||||
AfterSensitive: json.RawMessage(`{}`),
|
||||
BeforeSensitive: json.RawMessage(`{}`),
|
||||
ReplacePaths: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
"error with source code subject with multiple expression values": {
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
|
||||
33
internal/command/jsonentities/testdata/diagnostic/error-with-integer-binary-comparisons.json
vendored
Normal file
33
internal/command/jsonentities/testdata/diagnostic/error-with-integer-binary-comparisons.json
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"severity": "error",
|
||||
"summary": "Test assertion failed",
|
||||
"detail": "",
|
||||
"range": {
|
||||
"filename": "test.tftest.hcl",
|
||||
"start": {
|
||||
"line": 7,
|
||||
"column": 17,
|
||||
"byte": 118
|
||||
},
|
||||
"end": {
|
||||
"line": 7,
|
||||
"column": 23,
|
||||
"byte": 125
|
||||
}
|
||||
},
|
||||
"snippet": {
|
||||
"context": "run \"fails_without_useful_diff\"",
|
||||
"code": " condition = 3 == 5",
|
||||
"start_line": 7,
|
||||
"highlight_start_offset": 15,
|
||||
"highlight_end_offset": 22,
|
||||
"values": []
|
||||
},
|
||||
"difference": {
|
||||
"before": "3",
|
||||
"after": "5",
|
||||
"after_unknown": false,
|
||||
"before_sensitive": false,
|
||||
"after_sensitive": false
|
||||
}
|
||||
}
|
||||
44
internal/command/jsonentities/testdata/diagnostic/error-with-object-binary-comparisons.json
vendored
Normal file
44
internal/command/jsonentities/testdata/diagnostic/error-with-object-binary-comparisons.json
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"severity": "error",
|
||||
"summary": "object assertion failed",
|
||||
"detail": "",
|
||||
"range": {
|
||||
"filename": "test.tftest.hcl",
|
||||
"start": {
|
||||
"line": 12,
|
||||
"column": 17,
|
||||
"byte": 171
|
||||
},
|
||||
"end": {
|
||||
"line": 20,
|
||||
"column": 6,
|
||||
"byte": 288
|
||||
}
|
||||
},
|
||||
"snippet": {
|
||||
"context": "run \"fails_without_useful_diff\"",
|
||||
"code": " condition = jsonencode(var.json_headers) == jsonencode([\n \"Test-Header-1: foo\",\n \"Test-Header-2: bar\",\n ])",
|
||||
"start_line": 12,
|
||||
"highlight_start_offset": 0,
|
||||
"highlight_end_offset": 117,
|
||||
"values": [
|
||||
{
|
||||
"traversal": "var.json_headers",
|
||||
"statement": "is object with 2 attributes"
|
||||
}
|
||||
]
|
||||
},
|
||||
"difference": {
|
||||
"before": {
|
||||
"Test-Header-1": "foo",
|
||||
"Test-Header-2": "foo"
|
||||
},
|
||||
"after": {
|
||||
"Test-Header-1": "foo",
|
||||
"Test-Header-2": "bar"
|
||||
},
|
||||
"after_unknown": false,
|
||||
"before_sensitive": {},
|
||||
"after_sensitive": {}
|
||||
}
|
||||
}
|
||||
@@ -562,6 +562,69 @@ func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schema
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// GenerateChange is used to receive two values and calculate the difference
|
||||
// between them in order to return a Change struct
|
||||
func GenerateChange(beforeVal, afterVal cty.Value) (*Change, error) {
|
||||
var err error
|
||||
beforeVal, marks := beforeVal.UnmarkDeepWithPaths()
|
||||
bs := jsonstate.SensitiveAsBoolWithPathValueMarks(beforeVal, marks)
|
||||
beforeSensitive, err := ctyjson.Marshal(bs, bs.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
afterVal, marks = afterVal.UnmarkDeepWithPaths()
|
||||
as := jsonstate.SensitiveAsBoolWithPathValueMarks(afterVal, marks)
|
||||
afterSensitive, err := ctyjson.Marshal(as, as.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var before, after []byte
|
||||
var afterUnknown cty.Value
|
||||
|
||||
if beforeVal != cty.NilVal {
|
||||
before, err = ctyjson.Marshal(beforeVal, beforeVal.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if afterVal != cty.NilVal {
|
||||
if afterVal.IsWhollyKnown() {
|
||||
after, err = ctyjson.Marshal(afterVal, afterVal.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
afterUnknown = cty.False
|
||||
} else {
|
||||
filteredAfter := omitUnknowns(afterVal)
|
||||
if filteredAfter.IsNull() {
|
||||
after = nil
|
||||
} else {
|
||||
after, err = ctyjson.Marshal(filteredAfter, filteredAfter.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
afterUnknown = unknownAsBool(afterVal)
|
||||
}
|
||||
}
|
||||
|
||||
a, _ := ctyjson.Marshal(afterUnknown, afterUnknown.Type())
|
||||
|
||||
return &Change{
|
||||
Before: json.RawMessage(before),
|
||||
After: json.RawMessage(after),
|
||||
AfterUnknown: a,
|
||||
|
||||
BeforeSensitive: json.RawMessage(beforeSensitive),
|
||||
AfterSensitive: json.RawMessage(afterSensitive),
|
||||
// Just to be explicit, outputs cannot be imported so this is always
|
||||
// nil.
|
||||
Importing: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MarshalOutputChanges converts the provided internal representation of
|
||||
// Changes objects into the structured JSON representation.
|
||||
//
|
||||
@@ -589,40 +652,6 @@ func MarshalOutputChanges(changes *plans.Changes) (map[string]Change, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// We drop the marks from the change, as decoding is only an
|
||||
// intermediate step to re-encode the values as json
|
||||
changeV.Before, _ = changeV.Before.UnmarkDeep()
|
||||
changeV.After, _ = changeV.After.UnmarkDeep()
|
||||
|
||||
var before, after []byte
|
||||
var afterUnknown cty.Value
|
||||
|
||||
if changeV.Before != cty.NilVal {
|
||||
before, err = ctyjson.Marshal(changeV.Before, changeV.Before.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if changeV.After != cty.NilVal {
|
||||
if changeV.After.IsWhollyKnown() {
|
||||
after, err = ctyjson.Marshal(changeV.After, changeV.After.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
afterUnknown = cty.False
|
||||
} else {
|
||||
filteredAfter := omitUnknowns(changeV.After)
|
||||
if filteredAfter.IsNull() {
|
||||
after = nil
|
||||
} else {
|
||||
after, err = ctyjson.Marshal(filteredAfter, filteredAfter.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
afterUnknown = unknownAsBool(changeV.After)
|
||||
}
|
||||
}
|
||||
|
||||
// The only information we have in the plan about output sensitivity is
|
||||
// a boolean which is true if the output was or is marked sensitive. As
|
||||
@@ -637,22 +666,16 @@ func MarshalOutputChanges(changes *plans.Changes) (map[string]Change, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a, _ := ctyjson.Marshal(afterUnknown, afterUnknown.Type())
|
||||
|
||||
c := Change{
|
||||
Actions: actionString(oc.Action.String()),
|
||||
Before: json.RawMessage(before),
|
||||
After: json.RawMessage(after),
|
||||
AfterUnknown: a,
|
||||
BeforeSensitive: json.RawMessage(sensitive),
|
||||
AfterSensitive: json.RawMessage(sensitive),
|
||||
|
||||
// Just to be explicit, outputs cannot be imported so this is always
|
||||
// nil.
|
||||
Importing: nil,
|
||||
change, err := GenerateChange(changeV.Before, changeV.After)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outputChanges[oc.Addr.OutputValue.Name] = c
|
||||
change.Actions = actionString(oc.Action.String())
|
||||
change.BeforeSensitive = json.RawMessage(sensitive)
|
||||
change.AfterSensitive = json.RawMessage(sensitive)
|
||||
|
||||
outputChanges[oc.Addr.OutputValue.Name] = *change
|
||||
}
|
||||
|
||||
return outputChanges, nil
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/opentofu/opentofu/internal/plans"
|
||||
)
|
||||
|
||||
@@ -514,3 +515,50 @@ func BenchmarkUnknownAsBool_9(b *testing.B) {
|
||||
unknownAsBool(value)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenerateChange covers sensitivity tests for GenerateChange.
|
||||
// TestOutputs test cases covered by outputs, but since is invalid to
|
||||
// have outputs with sensitivity on the root module, we're creating this test
|
||||
// to cover the remaining edge cases.
|
||||
func TestGenerateChange(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
val1 cty.Value
|
||||
val2 cty.Value
|
||||
expected *Change
|
||||
}{
|
||||
"basic change": {
|
||||
val1: cty.StringVal("test0"),
|
||||
val2: cty.StringVal("test1"),
|
||||
expected: &Change{
|
||||
Before: json.RawMessage("\"test0\""),
|
||||
After: json.RawMessage("\"test1\""),
|
||||
AfterUnknown: json.RawMessage("false"),
|
||||
BeforeSensitive: json.RawMessage("false"),
|
||||
AfterSensitive: json.RawMessage("false"),
|
||||
},
|
||||
},
|
||||
"handles sensitivity": {
|
||||
val1: cty.NumberIntVal(3).Mark(marks.Sensitive),
|
||||
val2: cty.NumberIntVal(5).Mark(marks.Sensitive),
|
||||
expected: &Change{
|
||||
Before: json.RawMessage("3"),
|
||||
After: json.RawMessage("5"),
|
||||
AfterUnknown: json.RawMessage("false"),
|
||||
BeforeSensitive: json.RawMessage("true"),
|
||||
AfterSensitive: json.RawMessage("true"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
change, err := GenerateChange(test.val1, test.val2)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %s", err)
|
||||
}
|
||||
|
||||
if !cmp.Equal(change, test.expected) {
|
||||
t.Errorf("wrong result:\n %v\n", cmp.Diff(change, test.expected))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user