github.com/opentofu/opentofu@v1.7.1/internal/tfdiags/diagnostics_test.go (about)

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