|
7 | 7 | "bufio" |
8 | 8 | "bytes" |
9 | 9 | "fmt" |
| 10 | + "iter" |
10 | 11 | "sort" |
11 | 12 | "strings" |
12 | 13 |
|
@@ -77,7 +78,8 @@ func DiagnosticFromJSON(diag *viewsjson.Diagnostic, color *colorstring.Colorize, |
77 | 78 | // be pure text that lends itself well to word-wrapping. |
78 | 79 | fmt.Fprintf(&buf, color.Color("[bold]%s[reset]\n\n"), diag.Summary) |
79 | 80 |
|
80 | | - appendSourceSnippets(&buf, diag, color) |
| 81 | + f := &snippetFormatter{&buf, diag, color} |
| 82 | + f.write() |
81 | 83 |
|
82 | 84 | if diag.Detail != "" { |
83 | 85 | paraWidth := width - leftRuleWidth - 1 // leave room for the left rule |
@@ -151,7 +153,8 @@ func DiagnosticPlainFromJSON(diag *viewsjson.Diagnostic, width int) string { |
151 | 153 | // be pure text that lends itself well to word-wrapping. |
152 | 154 | fmt.Fprintf(&buf, "%s\n\n", diag.Summary) |
153 | 155 |
|
154 | | - appendSourceSnippets(&buf, diag, disabledColorize) |
| 156 | + f := &snippetFormatter{&buf, diag, disabledColorize} |
| 157 | + f.write() |
155 | 158 |
|
156 | 159 | if diag.Detail != "" { |
157 | 160 | if width > 1 { |
@@ -215,7 +218,17 @@ func DiagnosticWarningsCompact(diags tfdiags.Diagnostics, color *colorstring.Col |
215 | 218 | return b.String() |
216 | 219 | } |
217 | 220 |
|
218 | | -func appendSourceSnippets(buf *bytes.Buffer, diag *viewsjson.Diagnostic, color *colorstring.Colorize) { |
| 221 | +// snippetFormatter handles formatting diagnostic information with source snippets |
| 222 | +type snippetFormatter struct { |
| 223 | + buf *bytes.Buffer |
| 224 | + diag *viewsjson.Diagnostic |
| 225 | + color *colorstring.Colorize |
| 226 | +} |
| 227 | + |
| 228 | +func (f *snippetFormatter) write() { |
| 229 | + diag := f.diag |
| 230 | + buf := f.buf |
| 231 | + color := f.color |
219 | 232 | if diag.Address != "" { |
220 | 233 | fmt.Fprintf(buf, " with %s,\n", diag.Address) |
221 | 234 | } |
@@ -281,7 +294,7 @@ func appendSourceSnippets(buf *bytes.Buffer, diag *viewsjson.Diagnostic, color * |
281 | 294 | ) |
282 | 295 | } |
283 | 296 |
|
284 | | - if len(snippet.Values) > 0 || (snippet.FunctionCall != nil && snippet.FunctionCall.Signature != nil) { |
| 297 | + if len(snippet.Values) > 0 || (snippet.FunctionCall != nil && snippet.FunctionCall.Signature != nil) || snippet.TestAssertionExpr != nil { |
285 | 298 | // The diagnostic may also have information about the dynamic |
286 | 299 | // values of relevant variables at the point of evaluation. |
287 | 300 | // This is particularly useful for expressions that get evaluated |
@@ -312,11 +325,139 @@ func appendSourceSnippets(buf *bytes.Buffer, diag *viewsjson.Diagnostic, color * |
312 | 325 | } |
313 | 326 | buf.WriteString(")\n") |
314 | 327 | } |
315 | | - for _, value := range values { |
316 | | - fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] [bold]%s[reset] %s\n"), value.Traversal, value.Statement) |
| 328 | + |
| 329 | + // always print the values unless in the case of a test assertion, where we only print them if the user has requested verbose output |
| 330 | + printValues := snippet.TestAssertionExpr == nil || snippet.TestAssertionExpr.ShowVerbose |
| 331 | + |
| 332 | + // The diagnostic may also have information about failures from test assertions |
| 333 | + // in a `terraform test` run. This is useful for understanding the values that |
| 334 | + // were being compared when the assertion failed. |
| 335 | + // Also, we'll print a JSON diff of the two values to make it easier to see the |
| 336 | + // differences. |
| 337 | + if snippet.TestAssertionExpr != nil { |
| 338 | + f.printTestDiagOutput(snippet.TestAssertionExpr) |
| 339 | + } |
| 340 | + |
| 341 | + if printValues { |
| 342 | + for _, value := range values { |
| 343 | + // if the statement is one line, we'll just print it as is |
| 344 | + // otherwise, we have to ensure that each line is indented correctly |
| 345 | + // and that the first line has the traversal information |
| 346 | + valSlice := strings.Split(value.Statement, "\n") |
| 347 | + fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] [bold]%s[reset] %s\n"), |
| 348 | + value.Traversal, valSlice[0]) |
| 349 | + |
| 350 | + for _, line := range valSlice[1:] { |
| 351 | + fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] %s\n"), line) |
| 352 | + } |
| 353 | + } |
317 | 354 | } |
318 | 355 | } |
319 | 356 | } |
320 | 357 |
|
321 | 358 | buf.WriteByte('\n') |
322 | 359 | } |
| 360 | + |
| 361 | +func (f *snippetFormatter) printTestDiagOutput(diag *viewsjson.DiagnosticTestBinaryExpr) { |
| 362 | + buf := f.buf |
| 363 | + color := f.color |
| 364 | + // We only print the LHS and RHS if the user has requested verbose output |
| 365 | + // for the test assertion. |
| 366 | + if diag.ShowVerbose { |
| 367 | + fmt.Fprint(buf, color.Color(" [dark_gray]│[reset] [bold]LHS[reset]:\n")) |
| 368 | + for line := range strings.SplitSeq(diag.LHS, "\n") { |
| 369 | + fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] %s\n"), line) |
| 370 | + } |
| 371 | + fmt.Fprint(buf, color.Color(" [dark_gray]│[reset] [bold]RHS[reset]:\n")) |
| 372 | + for line := range strings.SplitSeq(diag.RHS, "\n") { |
| 373 | + fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] %s\n"), line) |
| 374 | + } |
| 375 | + } |
| 376 | + if diag.Warning != "" { |
| 377 | + fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] [bold]Warning[reset]: %s\n"), diag.Warning) |
| 378 | + } |
| 379 | + f.printJSONDiff(diag.LHS, diag.RHS) |
| 380 | + buf.WriteByte('\n') |
| 381 | +} |
| 382 | + |
| 383 | +// printJSONDiff prints a colorized line-by-line diff of the JSON values of the LHS and RHS expressions |
| 384 | +// in a test assertion. |
| 385 | +// It visually distinguishes removed and added lines, helping users identify |
| 386 | +// discrepancies between an "actual" (lhsStr) and an "expected" (rhsStr) JSON output. |
| 387 | +func (f *snippetFormatter) printJSONDiff(lhsStr, rhsStr string) { |
| 388 | + |
| 389 | + buf := f.buf |
| 390 | + color := f.color |
| 391 | + // No visible difference in the JSON, so we'll just return |
| 392 | + if lhsStr == rhsStr { |
| 393 | + return |
| 394 | + } |
| 395 | + fmt.Fprint(buf, color.Color(" [dark_gray]│[reset] [bold]Diff[reset]:\n")) |
| 396 | + fmt.Fprint(buf, color.Color(" [dark_gray]│[reset] [red][bold]--- actual[reset]\n")) |
| 397 | + fmt.Fprint(buf, color.Color(" [dark_gray]│[reset] [green][bold]+++ expected[reset]\n")) |
| 398 | + nextLhs, stopLhs := iter.Pull(strings.SplitSeq(lhsStr, "\n")) |
| 399 | + nextRhs, stopRhs := iter.Pull(strings.SplitSeq(rhsStr, "\n")) |
| 400 | + |
| 401 | + printLine := func(prefix, line string) { |
| 402 | + var colour string |
| 403 | + switch prefix { |
| 404 | + case "-": |
| 405 | + colour = "[red]" |
| 406 | + case "+": |
| 407 | + colour = "[green]" |
| 408 | + default: |
| 409 | + } |
| 410 | + msg := fmt.Sprintf(" [dark_gray]│[reset] %s%s[reset] %s\n", colour, prefix, line) |
| 411 | + fmt.Fprint(buf, color.Color(msg)) |
| 412 | + } |
| 413 | + |
| 414 | + // Collect differing lines separately for each side |
| 415 | + removedLines := []string{} |
| 416 | + addedLines := []string{} |
| 417 | + |
| 418 | + // Function to print collected diffs and reset buffers |
| 419 | + printDiffs := func() { |
| 420 | + for _, line := range removedLines { |
| 421 | + printLine("-", line) |
| 422 | + } |
| 423 | + for _, line := range addedLines { |
| 424 | + printLine("+", line) |
| 425 | + } |
| 426 | + removedLines = []string{} |
| 427 | + addedLines = []string{} |
| 428 | + } |
| 429 | + |
| 430 | + // We'll iterate over both sides of the expression and collect the differences |
| 431 | + // along the way. When a match is found, we'll then print all the collected diffs |
| 432 | + // and the matching line, and then reset the buffers. |
| 433 | + for { |
| 434 | + lhsLine, lhsOk := nextLhs() |
| 435 | + rhsLine, rhsOk := nextRhs() |
| 436 | + |
| 437 | + if !lhsOk && !rhsOk { // Both sides are done, so we'll print the diffs and break |
| 438 | + printDiffs() |
| 439 | + break |
| 440 | + } |
| 441 | + |
| 442 | + // If one side is done, we'll just print the remaining lines from the other side |
| 443 | + if !lhsOk { |
| 444 | + addedLines = append(addedLines, rhsLine) |
| 445 | + continue |
| 446 | + } |
| 447 | + if !rhsOk { |
| 448 | + removedLines = append(removedLines, lhsLine) |
| 449 | + continue |
| 450 | + } |
| 451 | + |
| 452 | + if lhsLine == rhsLine { |
| 453 | + printDiffs() |
| 454 | + printLine(" ", lhsLine) |
| 455 | + } else { |
| 456 | + removedLines = append(removedLines, lhsLine) |
| 457 | + addedLines = append(addedLines, rhsLine) |
| 458 | + } |
| 459 | + } |
| 460 | + |
| 461 | + stopLhs() |
| 462 | + stopRhs() |
| 463 | +} |
0 commit comments