github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/tfdiags/diagnostics_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package tfdiags
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"reflect"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/hashicorp/go-multierror"
    14  
    15  	"github.com/davecgh/go-spew/spew"
    16  	"github.com/hashicorp/hcl/v2"
    17  )
    18  
    19  func TestBuild(t *testing.T) {
    20  	type diagFlat struct {
    21  		Severity Severity
    22  		Summary  string
    23  		Detail   string
    24  		Subject  *SourceRange
    25  		Context  *SourceRange
    26  	}
    27  
    28  	tests := map[string]struct {
    29  		Cons func(Diagnostics) Diagnostics
    30  		Want []diagFlat
    31  	}{
    32  		"nil": {
    33  			func(diags Diagnostics) Diagnostics {
    34  				return diags
    35  			},
    36  			nil,
    37  		},
    38  		"fmt.Errorf": {
    39  			func(diags Diagnostics) Diagnostics {
    40  				diags = diags.Append(fmt.Errorf("oh no bad"))
    41  				return diags
    42  			},
    43  			[]diagFlat{
    44  				{
    45  					Severity: Error,
    46  					Summary:  "oh no bad",
    47  				},
    48  			},
    49  		},
    50  		"errors.New": {
    51  			func(diags Diagnostics) Diagnostics {
    52  				diags = diags.Append(errors.New("oh no bad"))
    53  				return diags
    54  			},
    55  			[]diagFlat{
    56  				{
    57  					Severity: Error,
    58  					Summary:  "oh no bad",
    59  				},
    60  			},
    61  		},
    62  		"hcl.Diagnostic": {
    63  			func(diags Diagnostics) Diagnostics {
    64  				diags = diags.Append(&hcl.Diagnostic{
    65  					Severity: hcl.DiagError,
    66  					Summary:  "Something bad happened",
    67  					Detail:   "It was really, really bad.",
    68  					Subject: &hcl.Range{
    69  						Filename: "foo.tf",
    70  						Start:    hcl.Pos{Line: 1, Column: 10, Byte: 9},
    71  						End:      hcl.Pos{Line: 2, Column: 3, Byte: 25},
    72  					},
    73  					Context: &hcl.Range{
    74  						Filename: "foo.tf",
    75  						Start:    hcl.Pos{Line: 1, Column: 1, Byte: 0},
    76  						End:      hcl.Pos{Line: 3, Column: 1, Byte: 30},
    77  					},
    78  				})
    79  				return diags
    80  			},
    81  			[]diagFlat{
    82  				{
    83  					Severity: Error,
    84  					Summary:  "Something bad happened",
    85  					Detail:   "It was really, really bad.",
    86  					Subject: &SourceRange{
    87  						Filename: "foo.tf",
    88  						Start:    SourcePos{Line: 1, Column: 10, Byte: 9},
    89  						End:      SourcePos{Line: 2, Column: 3, Byte: 25},
    90  					},
    91  					Context: &SourceRange{
    92  						Filename: "foo.tf",
    93  						Start:    SourcePos{Line: 1, Column: 1, Byte: 0},
    94  						End:      SourcePos{Line: 3, Column: 1, Byte: 30},
    95  					},
    96  				},
    97  			},
    98  		},
    99  		"hcl.Diagnostics": {
   100  			func(diags Diagnostics) Diagnostics {
   101  				diags = diags.Append(hcl.Diagnostics{
   102  					{
   103  						Severity: hcl.DiagError,
   104  						Summary:  "Something bad happened",
   105  						Detail:   "It was really, really bad.",
   106  					},
   107  					{
   108  						Severity: hcl.DiagWarning,
   109  						Summary:  "Also, somebody sneezed",
   110  						Detail:   "How rude!",
   111  					},
   112  				})
   113  				return diags
   114  			},
   115  			[]diagFlat{
   116  				{
   117  					Severity: Error,
   118  					Summary:  "Something bad happened",
   119  					Detail:   "It was really, really bad.",
   120  				},
   121  				{
   122  					Severity: Warning,
   123  					Summary:  "Also, somebody sneezed",
   124  					Detail:   "How rude!",
   125  				},
   126  			},
   127  		},
   128  		"multierror.Error": {
   129  			func(diags Diagnostics) Diagnostics {
   130  				err := multierror.Append(nil, errors.New("bad thing A"))
   131  				err = multierror.Append(err, errors.New("bad thing B"))
   132  				diags = diags.Append(err)
   133  				return diags
   134  			},
   135  			[]diagFlat{
   136  				{
   137  					Severity: Error,
   138  					Summary:  "bad thing A",
   139  				},
   140  				{
   141  					Severity: Error,
   142  					Summary:  "bad thing B",
   143  				},
   144  			},
   145  		},
   146  		"concat Diagnostics": {
   147  			func(diags Diagnostics) Diagnostics {
   148  				var moreDiags Diagnostics
   149  				moreDiags = moreDiags.Append(errors.New("bad thing A"))
   150  				moreDiags = moreDiags.Append(errors.New("bad thing B"))
   151  				return diags.Append(moreDiags)
   152  			},
   153  			[]diagFlat{
   154  				{
   155  					Severity: Error,
   156  					Summary:  "bad thing A",
   157  				},
   158  				{
   159  					Severity: Error,
   160  					Summary:  "bad thing B",
   161  				},
   162  			},
   163  		},
   164  		"single Diagnostic": {
   165  			func(diags Diagnostics) Diagnostics {
   166  				return diags.Append(SimpleWarning("Don't forget your toothbrush!"))
   167  			},
   168  			[]diagFlat{
   169  				{
   170  					Severity: Warning,
   171  					Summary:  "Don't forget your toothbrush!",
   172  				},
   173  			},
   174  		},
   175  		"multiple appends": {
   176  			func(diags Diagnostics) Diagnostics {
   177  				diags = diags.Append(SimpleWarning("Don't forget your toothbrush!"))
   178  				diags = diags.Append(fmt.Errorf("exploded"))
   179  				return diags
   180  			},
   181  			[]diagFlat{
   182  				{
   183  					Severity: Warning,
   184  					Summary:  "Don't forget your toothbrush!",
   185  				},
   186  				{
   187  					Severity: Error,
   188  					Summary:  "exploded",
   189  				},
   190  			},
   191  		},
   192  	}
   193  
   194  	for name, test := range tests {
   195  		t.Run(name, func(t *testing.T) {
   196  			gotDiags := test.Cons(nil)
   197  			var got []diagFlat
   198  			for _, item := range gotDiags {
   199  				desc := item.Description()
   200  				source := item.Source()
   201  				got = append(got, diagFlat{
   202  					Severity: item.Severity(),
   203  					Summary:  desc.Summary,
   204  					Detail:   desc.Detail,
   205  					Subject:  source.Subject,
   206  					Context:  source.Context,
   207  				})
   208  			}
   209  
   210  			if !reflect.DeepEqual(got, test.Want) {
   211  				t.Errorf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(test.Want))
   212  			}
   213  		})
   214  	}
   215  }
   216  
   217  func TestDiagnosticsErr(t *testing.T) {
   218  	t.Run("empty", func(t *testing.T) {
   219  		var diags Diagnostics
   220  		err := diags.Err()
   221  		if err != nil {
   222  			t.Errorf("got non-nil error %#v; want nil", err)
   223  		}
   224  	})
   225  	t.Run("warning only", func(t *testing.T) {
   226  		var diags Diagnostics
   227  		diags = diags.Append(SimpleWarning("bad"))
   228  		err := diags.Err()
   229  		if err != nil {
   230  			t.Errorf("got non-nil error %#v; want nil", err)
   231  		}
   232  	})
   233  	t.Run("one error", func(t *testing.T) {
   234  		var diags Diagnostics
   235  		diags = diags.Append(errors.New("didn't work"))
   236  		err := diags.Err()
   237  		if err == nil {
   238  			t.Fatalf("got nil error %#v; want non-nil", err)
   239  		}
   240  		if got, want := err.Error(), "didn't work"; got != want {
   241  			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
   242  		}
   243  	})
   244  	t.Run("two errors", func(t *testing.T) {
   245  		var diags Diagnostics
   246  		diags = diags.Append(errors.New("didn't work"))
   247  		diags = diags.Append(errors.New("didn't work either"))
   248  		err := diags.Err()
   249  		if err == nil {
   250  			t.Fatalf("got nil error %#v; want non-nil", err)
   251  		}
   252  		want := strings.TrimSpace(`
   253  2 problems:
   254  
   255  - didn't work
   256  - didn't work either
   257  `)
   258  		if got := err.Error(); got != want {
   259  			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
   260  		}
   261  	})
   262  	t.Run("error and warning", func(t *testing.T) {
   263  		var diags Diagnostics
   264  		diags = diags.Append(errors.New("didn't work"))
   265  		diags = diags.Append(SimpleWarning("didn't work either"))
   266  		err := diags.Err()
   267  		if err == nil {
   268  			t.Fatalf("got nil error %#v; want non-nil", err)
   269  		}
   270  		// Since this "as error" mode is just a fallback for
   271  		// non-diagnostics-aware situations like tests, we don't actually
   272  		// distinguish warnings and errors here since the point is to just
   273  		// get the messages rendered. User-facing code should be printing
   274  		// each diagnostic separately, so won't enter this codepath,
   275  		want := strings.TrimSpace(`
   276  2 problems:
   277  
   278  - didn't work
   279  - didn't work either
   280  `)
   281  		if got := err.Error(); got != want {
   282  			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
   283  		}
   284  	})
   285  }
   286  
   287  func TestDiagnosticsErrWithWarnings(t *testing.T) {
   288  	t.Run("empty", func(t *testing.T) {
   289  		var diags Diagnostics
   290  		err := diags.ErrWithWarnings()
   291  		if err != nil {
   292  			t.Errorf("got non-nil error %#v; want nil", err)
   293  		}
   294  	})
   295  	t.Run("warning only", func(t *testing.T) {
   296  		var diags Diagnostics
   297  		diags = diags.Append(SimpleWarning("bad"))
   298  		err := diags.ErrWithWarnings()
   299  		if err == nil {
   300  			t.Errorf("got nil error; want NonFatalError")
   301  			return
   302  		}
   303  		if _, ok := err.(NonFatalError); !ok {
   304  			t.Errorf("got %T; want NonFatalError", err)
   305  		}
   306  	})
   307  	t.Run("one error", func(t *testing.T) {
   308  		var diags Diagnostics
   309  		diags = diags.Append(errors.New("didn't work"))
   310  		err := diags.ErrWithWarnings()
   311  		if err == nil {
   312  			t.Fatalf("got nil error %#v; want non-nil", err)
   313  		}
   314  		if got, want := err.Error(), "didn't work"; got != want {
   315  			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
   316  		}
   317  	})
   318  	t.Run("two errors", func(t *testing.T) {
   319  		var diags Diagnostics
   320  		diags = diags.Append(errors.New("didn't work"))
   321  		diags = diags.Append(errors.New("didn't work either"))
   322  		err := diags.ErrWithWarnings()
   323  		if err == nil {
   324  			t.Fatalf("got nil error %#v; want non-nil", err)
   325  		}
   326  		want := strings.TrimSpace(`
   327  2 problems:
   328  
   329  - didn't work
   330  - didn't work either
   331  `)
   332  		if got := err.Error(); got != want {
   333  			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
   334  		}
   335  	})
   336  	t.Run("error and warning", func(t *testing.T) {
   337  		var diags Diagnostics
   338  		diags = diags.Append(errors.New("didn't work"))
   339  		diags = diags.Append(SimpleWarning("didn't work either"))
   340  		err := diags.ErrWithWarnings()
   341  		if err == nil {
   342  			t.Fatalf("got nil error %#v; want non-nil", err)
   343  		}
   344  		// Since this "as error" mode is just a fallback for
   345  		// non-diagnostics-aware situations like tests, we don't actually
   346  		// distinguish warnings and errors here since the point is to just
   347  		// get the messages rendered. User-facing code should be printing
   348  		// each diagnostic separately, so won't enter this codepath,
   349  		want := strings.TrimSpace(`
   350  2 problems:
   351  
   352  - didn't work
   353  - didn't work either
   354  `)
   355  		if got := err.Error(); got != want {
   356  			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
   357  		}
   358  	})
   359  }
   360  
   361  func TestDiagnosticsNonFatalErr(t *testing.T) {
   362  	t.Run("empty", func(t *testing.T) {
   363  		var diags Diagnostics
   364  		err := diags.NonFatalErr()
   365  		if err != nil {
   366  			t.Errorf("got non-nil error %#v; want nil", err)
   367  		}
   368  	})
   369  	t.Run("warning only", func(t *testing.T) {
   370  		var diags Diagnostics
   371  		diags = diags.Append(SimpleWarning("bad"))
   372  		err := diags.NonFatalErr()
   373  		if err == nil {
   374  			t.Errorf("got nil error; want NonFatalError")
   375  			return
   376  		}
   377  		if _, ok := err.(NonFatalError); !ok {
   378  			t.Errorf("got %T; want NonFatalError", err)
   379  		}
   380  	})
   381  	t.Run("one error", func(t *testing.T) {
   382  		var diags Diagnostics
   383  		diags = diags.Append(errors.New("didn't work"))
   384  		err := diags.NonFatalErr()
   385  		if err == nil {
   386  			t.Fatalf("got nil error %#v; want non-nil", err)
   387  		}
   388  		if got, want := err.Error(), "didn't work"; got != want {
   389  			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
   390  		}
   391  		if _, ok := err.(NonFatalError); !ok {
   392  			t.Errorf("got %T; want NonFatalError", err)
   393  		}
   394  	})
   395  	t.Run("two errors", func(t *testing.T) {
   396  		var diags Diagnostics
   397  		diags = diags.Append(errors.New("didn't work"))
   398  		diags = diags.Append(errors.New("didn't work either"))
   399  		err := diags.NonFatalErr()
   400  		if err == nil {
   401  			t.Fatalf("got nil error %#v; want non-nil", err)
   402  		}
   403  		want := strings.TrimSpace(`
   404  2 problems:
   405  
   406  - didn't work
   407  - didn't work either
   408  `)
   409  		if got := err.Error(); got != want {
   410  			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
   411  		}
   412  		if _, ok := err.(NonFatalError); !ok {
   413  			t.Errorf("got %T; want NonFatalError", err)
   414  		}
   415  	})
   416  	t.Run("error and warning", func(t *testing.T) {
   417  		var diags Diagnostics
   418  		diags = diags.Append(errors.New("didn't work"))
   419  		diags = diags.Append(SimpleWarning("didn't work either"))
   420  		err := diags.NonFatalErr()
   421  		if err == nil {
   422  			t.Fatalf("got nil error %#v; want non-nil", err)
   423  		}
   424  		// Since this "as error" mode is just a fallback for
   425  		// non-diagnostics-aware situations like tests, we don't actually
   426  		// distinguish warnings and errors here since the point is to just
   427  		// get the messages rendered. User-facing code should be printing
   428  		// each diagnostic separately, so won't enter this codepath,
   429  		want := strings.TrimSpace(`
   430  2 problems:
   431  
   432  - didn't work
   433  - didn't work either
   434  `)
   435  		if got := err.Error(); got != want {
   436  			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
   437  		}
   438  		if _, ok := err.(NonFatalError); !ok {
   439  			t.Errorf("got %T; want NonFatalError", err)
   440  		}
   441  	})
   442  }