github.com/cockroachdb/errors@v1.11.1/errbase/format_error_internal_test.go (about)

     1  // Copyright 2020 The Cockroach Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    12  // implied. See the License for the specific language governing
    13  // permissions and limitations under the License.
    14  
    15  package errbase
    16  
    17  import (
    18  	goErr "errors"
    19  	"fmt"
    20  	"strings"
    21  	"testing"
    22  
    23  	"github.com/cockroachdb/redact"
    24  )
    25  
    26  type wrapMini struct {
    27  	msg   string
    28  	cause error
    29  }
    30  
    31  func (e *wrapMini) Error() string {
    32  	return e.msg
    33  }
    34  
    35  func (e *wrapMini) Unwrap() error {
    36  	return e.cause
    37  }
    38  
    39  type wrapElideCauses struct {
    40  	override string
    41  	causes   []error
    42  }
    43  
    44  func NewWrapElideCauses(override string, errors ...error) error {
    45  	return &wrapElideCauses{
    46  		override: override,
    47  		causes:   errors,
    48  	}
    49  }
    50  
    51  func (e *wrapElideCauses) Unwrap() []error {
    52  	return e.causes
    53  }
    54  
    55  func (e *wrapElideCauses) SafeFormatError(p Printer) (next error) {
    56  	p.Print(e.override)
    57  	// Returning nil elides errors from remaining causal chain in the
    58  	// implementation of `formatErrorInternal`.
    59  	return nil
    60  }
    61  
    62  var _ SafeFormatter = &wrapElideCauses{}
    63  
    64  func (e *wrapElideCauses) Error() string {
    65  	b := strings.Builder{}
    66  	b.WriteString(e.override)
    67  	b.WriteString(": ")
    68  	for i, ee := range e.causes {
    69  		b.WriteString(ee.Error())
    70  		if i < len(e.causes)-1 {
    71  			b.WriteByte(' ')
    72  		}
    73  	}
    74  	return b.String()
    75  }
    76  
    77  type wrapNoElideCauses struct {
    78  	prefix string
    79  	causes []error
    80  }
    81  
    82  func NewWrapNoElideCauses(prefix string, errors ...error) error {
    83  	return &wrapNoElideCauses{
    84  		prefix: prefix,
    85  		causes: errors,
    86  	}
    87  }
    88  
    89  func (e *wrapNoElideCauses) Unwrap() []error {
    90  	return e.causes
    91  }
    92  
    93  func (e *wrapNoElideCauses) SafeFormatError(p Printer) (next error) {
    94  	p.Print(e.prefix)
    95  	return e.causes[0]
    96  }
    97  
    98  var _ SafeFormatter = &wrapNoElideCauses{}
    99  
   100  func (e *wrapNoElideCauses) Error() string {
   101  	b := strings.Builder{}
   102  	b.WriteString(e.prefix)
   103  	b.WriteString(": ")
   104  	for i, ee := range e.causes {
   105  		b.WriteString(ee.Error())
   106  		if i < len(e.causes)-1 {
   107  			b.WriteByte(' ')
   108  		}
   109  	}
   110  	return b.String()
   111  }
   112  
   113  // TestFormatErrorInternal attempts to highlight some idiosyncrasies of
   114  // the error formatting especially when used with multi-cause error
   115  // structures. Comments on specific cases below outline some gaps that
   116  // still require formatting tweaks.
   117  func TestFormatErrorInternal(t *testing.T) {
   118  	tests := []struct {
   119  		name            string
   120  		err             error
   121  		expectedSimple  string
   122  		expectedVerbose string
   123  	}{
   124  		{
   125  			name:           "single wrapper",
   126  			err:            fmt.Errorf("%w", fmt.Errorf("a%w", goErr.New("b"))),
   127  			expectedSimple: "ab",
   128  			expectedVerbose: `ab
   129  (1)
   130  Wraps: (2) ab
   131  Wraps: (3) b
   132  Error types: (1) *fmt.wrapError (2) *fmt.wrapError (3) *errors.errorString`,
   133  		},
   134  		{
   135  			name:           "simple multi-wrapper",
   136  			err:            goErr.Join(goErr.New("a"), goErr.New("b")),
   137  			expectedSimple: "a\nb",
   138  			// TODO(davidh): verbose test case should have line break
   139  			// between `a` and `b` on second line.
   140  			expectedVerbose: `a
   141  (1) ab
   142  Wraps: (2) b
   143  Wraps: (3) a
   144  Error types: (1) *errors.joinError (2) *errors.errorString (3) *errors.errorString`,
   145  		},
   146  		{
   147  			name: "multi-wrapper with custom formatter and partial elide",
   148  			err: NewWrapNoElideCauses("A",
   149  				NewWrapNoElideCauses("C", goErr.New("3"), goErr.New("4")),
   150  				NewWrapElideCauses("B", goErr.New("1"), goErr.New("2")),
   151  			),
   152  			expectedSimple: `A: B: C: 4: 3`, // 1 and 2 omitted because they are elided.
   153  			expectedVerbose: `A: B: C: 4: 3
   154  (1) A
   155  Wraps: (2) B
   156  └─ Wraps: (3) 2
   157  └─ Wraps: (4) 1
   158  Wraps: (5) C
   159  └─ Wraps: (6) 4
   160  └─ Wraps: (7) 3
   161  Error types: (1) *errbase.wrapNoElideCauses (2) *errbase.wrapElideCauses (3) *errors.errorString (4) *errors.errorString (5) *errbase.wrapNoElideCauses (6) *errors.errorString (7) *errors.errorString`,
   162  		},
   163  		{
   164  			name: "multi-wrapper with custom formatter and no elide",
   165  			// All errors in this example omit eliding their children.
   166  			err: NewWrapNoElideCauses("A",
   167  				NewWrapNoElideCauses("B", goErr.New("1"), goErr.New("2")),
   168  				NewWrapNoElideCauses("C", goErr.New("3"), goErr.New("4")),
   169  			),
   170  			expectedSimple: `A: C: 4: 3: B: 2: 1`,
   171  			expectedVerbose: `A: C: 4: 3: B: 2: 1
   172  (1) A
   173  Wraps: (2) C
   174  └─ Wraps: (3) 4
   175  └─ Wraps: (4) 3
   176  Wraps: (5) B
   177  └─ Wraps: (6) 2
   178  └─ Wraps: (7) 1
   179  Error types: (1) *errbase.wrapNoElideCauses (2) *errbase.wrapNoElideCauses (3) *errors.errorString (4) *errors.errorString (5) *errbase.wrapNoElideCauses (6) *errors.errorString (7) *errors.errorString`,
   180  		},
   181  		{
   182  			name:           "simple multi-line error",
   183  			err:            goErr.New("a\nb\nc\nd"),
   184  			expectedSimple: "a\nb\nc\nd",
   185  			// TODO(davidh): verbose test case should preserve all 3
   186  			// linebreaks in original error.
   187  			expectedVerbose: `a
   188  (1) ab
   189    |
   190    | c
   191    | d
   192  Error types: (1) *errors.errorString`,
   193  		},
   194  		{
   195  			name: "two-level multi-wrapper",
   196  			err: goErr.Join(
   197  				goErr.Join(goErr.New("a"), goErr.New("b")),
   198  				goErr.Join(goErr.New("c"), goErr.New("d")),
   199  			),
   200  			expectedSimple: "a\nb\nc\nd",
   201  			// TODO(davidh): verbose output should preserve line breaks after (1)
   202  			// and also after (2) and (5) in `c\nd` and `a\nb`.
   203  			expectedVerbose: `a
   204  (1) ab
   205    |
   206    | c
   207    | d
   208  Wraps: (2) cd
   209  └─ Wraps: (3) d
   210  └─ Wraps: (4) c
   211  Wraps: (5) ab
   212  └─ Wraps: (6) b
   213  └─ Wraps: (7) a
   214  Error types: (1) *errors.joinError (2) *errors.joinError (3) *errors.errorString (4) *errors.errorString (5) *errors.joinError (6) *errors.errorString (7) *errors.errorString`,
   215  		},
   216  		{
   217  			name: "simple multi-wrapper with single-cause chains inside",
   218  			err: goErr.Join(
   219  				fmt.Errorf("a%w", goErr.New("b")),
   220  				fmt.Errorf("c%w", goErr.New("d")),
   221  			),
   222  			expectedSimple: "ab\ncd",
   223  			expectedVerbose: `ab
   224  (1) ab
   225    | cd
   226  Wraps: (2) cd
   227  └─ Wraps: (3) d
   228  Wraps: (4) ab
   229  └─ Wraps: (5) b
   230  Error types: (1) *errors.joinError (2) *fmt.wrapError (3) *errors.errorString (4) *fmt.wrapError (5) *errors.errorString`,
   231  		},
   232  		{
   233  			name: "multi-cause wrapper with single-cause chains inside",
   234  			err: goErr.Join(
   235  				fmt.Errorf("a%w", fmt.Errorf("b%w", fmt.Errorf("c%w", goErr.New("d")))),
   236  				fmt.Errorf("e%w", fmt.Errorf("f%w", fmt.Errorf("g%w", goErr.New("h")))),
   237  			),
   238  			expectedSimple: `abcd
   239  efgh`,
   240  			expectedVerbose: `abcd
   241  (1) abcd
   242    | efgh
   243  Wraps: (2) efgh
   244  └─ Wraps: (3) fgh
   245    └─ Wraps: (4) gh
   246      └─ Wraps: (5) h
   247  Wraps: (6) abcd
   248  └─ Wraps: (7) bcd
   249    └─ Wraps: (8) cd
   250      └─ Wraps: (9) d
   251  Error types: (1) *errors.joinError (2) *fmt.wrapError (3) *fmt.wrapError (4) *fmt.wrapError (5) *errors.errorString (6) *fmt.wrapError (7) *fmt.wrapError (8) *fmt.wrapError (9) *errors.errorString`},
   252  		{
   253  			name: "single cause chain with multi-cause wrapper inside with single-cause chains inside",
   254  			err: fmt.Errorf(
   255  				"prefix1: %w",
   256  				fmt.Errorf(
   257  					"prefix2: %w",
   258  					goErr.Join(
   259  						fmt.Errorf("a%w", fmt.Errorf("b%w", fmt.Errorf("c%w", goErr.New("d")))),
   260  						fmt.Errorf("e%w", fmt.Errorf("f%w", fmt.Errorf("g%w", goErr.New("h")))),
   261  					))),
   262  			expectedSimple: `prefix1: prefix2: abcd
   263  efgh`,
   264  			expectedVerbose: `prefix1: prefix2: abcd
   265  (1) prefix1
   266  Wraps: (2) prefix2
   267  Wraps: (3) abcd
   268    | efgh
   269    └─ Wraps: (4) efgh
   270      └─ Wraps: (5) fgh
   271        └─ Wraps: (6) gh
   272          └─ Wraps: (7) h
   273    └─ Wraps: (8) abcd
   274      └─ Wraps: (9) bcd
   275        └─ Wraps: (10) cd
   276          └─ Wraps: (11) d
   277  Error types: (1) *fmt.wrapError (2) *fmt.wrapError (3) *errors.joinError (4) *fmt.wrapError (5) *fmt.wrapError (6) *fmt.wrapError (7) *errors.errorString (8) *fmt.wrapError (9) *fmt.wrapError (10) *fmt.wrapError (11) *errors.errorString`,
   278  		},
   279  		{
   280  			name:           "test wrapMini elides cause error string",
   281  			err:            &wrapMini{"whoa: d", goErr.New("d")},
   282  			expectedSimple: "whoa: d",
   283  			expectedVerbose: `whoa: d
   284  (1) whoa
   285  Wraps: (2) d
   286  Error types: (1) *errbase.wrapMini (2) *errors.errorString`,
   287  		},
   288  	}
   289  	for _, tt := range tests {
   290  		t.Run(tt.name, func(t *testing.T) {
   291  			fe := Formattable(tt.err)
   292  			s := fmt.Sprintf("%s", fe)
   293  			if s != tt.expectedSimple {
   294  				t.Errorf("\nexpected: \n%s\nbut got:\n%s\n", tt.expectedSimple, s)
   295  			}
   296  			s = fmt.Sprintf("%+v", fe)
   297  			if s != tt.expectedVerbose {
   298  				t.Errorf("\nexpected: \n%s\nbut got:\n%s\n", tt.expectedVerbose, s)
   299  			}
   300  		})
   301  	}
   302  }
   303  
   304  func TestPrintEntry(t *testing.T) {
   305  	b := func(s string) []byte { return []byte(s) }
   306  
   307  	testCases := []struct {
   308  		entry formatEntry
   309  		exp   string
   310  	}{
   311  		{formatEntry{}, ""},
   312  		{formatEntry{head: b("abc")}, " abc"},
   313  		{formatEntry{head: b("abc\nxyz")}, " abc\nxyz"},
   314  		{formatEntry{details: b("def")}, " def"},
   315  		{formatEntry{details: b("def\nxyz")}, " def\nxyz"},
   316  		{formatEntry{head: b("abc"), details: b("def")}, " abcdef"},
   317  		{formatEntry{head: b("abc\nxyz"), details: b("def")}, " abc\nxyzdef"},
   318  		{formatEntry{head: b("abc"), details: b("def\n  | xyz")}, " abcdef\n  | xyz"},
   319  		{formatEntry{head: b("abc\nxyz"), details: b("def\n  | xyz")}, " abc\nxyzdef\n  | xyz"},
   320  	}
   321  
   322  	for _, tc := range testCases {
   323  		s := state{}
   324  		s.printEntry(tc.entry)
   325  		if s.finalBuf.String() != tc.exp {
   326  			t.Errorf("%s: expected %q, got %q", tc.entry, tc.exp, s.finalBuf.String())
   327  		}
   328  	}
   329  }
   330  
   331  func TestFormatSingleLineOutput(t *testing.T) {
   332  	b := func(s string) []byte { return []byte(s) }
   333  	testCases := []struct {
   334  		entries []formatEntry
   335  		exp     string
   336  	}{
   337  		{[]formatEntry{{}}, ``},
   338  		{[]formatEntry{{head: b(`a`)}}, `a`},
   339  		{[]formatEntry{{head: b(`a`)}, {head: b(`b`)}, {head: b(`c`)}}, `c: b: a`},
   340  		{[]formatEntry{{}, {head: b(`b`)}}, `b`},
   341  		{[]formatEntry{{head: b(`a`)}, {}}, `a`},
   342  		{[]formatEntry{{head: b(`a`)}, {}, {head: b(`c`)}}, `c: a`},
   343  		{[]formatEntry{{head: b(`a`), elideShort: true}, {head: b(`b`)}}, `b`},
   344  		{[]formatEntry{{head: b("abc\ndef")}, {head: b("ghi\nklm")}}, "ghi\nklm: abc\ndef"},
   345  	}
   346  
   347  	for _, tc := range testCases {
   348  		s := state{entries: tc.entries}
   349  		s.formatSingleLineOutput()
   350  		if s.finalBuf.String() != tc.exp {
   351  			t.Errorf("%s: expected %q, got %q", tc.entries, tc.exp, s.finalBuf.String())
   352  		}
   353  	}
   354  }
   355  
   356  func TestPrintEntryRedactable(t *testing.T) {
   357  	sm := string(redact.StartMarker())
   358  	em := string(redact.EndMarker())
   359  	esc := string(redact.EscapeMarkers(redact.StartMarker()))
   360  	b := func(s string) []byte { return []byte(s) }
   361  	q := func(s string) string { return sm + s + em }
   362  
   363  	testCases := []struct {
   364  		entry formatEntry
   365  		exp   string
   366  	}{
   367  		// If the entries are not redactable, they may contain arbitrary
   368  		// characters; they get enclosed in redaction markers in the final output.
   369  		{formatEntry{}, ""},
   370  		{formatEntry{head: b("abc")}, " " + q("abc")},
   371  		{formatEntry{head: b("abc\nxyz")}, " " + q("abc") + "\n" + q("xyz")},
   372  		{formatEntry{details: b("def")}, " " + q("def")},
   373  		{formatEntry{details: b("def\nxyz")}, " " + q("def") + "\n" + q("xyz")},
   374  		{formatEntry{head: b("abc"), details: b("def")}, " " + q("abc") + q("def")},
   375  		{formatEntry{head: b("abc\nxyz"), details: b("def")}, " " + q("abc") + "\n" + q("xyz") + q("def")},
   376  		{formatEntry{head: b("abc"), details: b("def\n  | xyz")}, " " + q("abc") + q("def") + "\n" + q("  | xyz")},
   377  		{formatEntry{head: b("abc\nxyz"), details: b("def\n  | xyz")}, " " + q("abc") + "\n" + q("xyz") + q("def") + "\n" + q("  | xyz")},
   378  		// If there were markers in the entry, they get escaped in the output.
   379  		{formatEntry{head: b("abc" + em + sm), details: b("def" + em + sm)}, " " + q("abc"+esc+esc) + q("def"+esc+esc)},
   380  
   381  		// If the entries are redactable, then whatever characters they contain
   382  		// are assumed safe and copied as-is to the final output.
   383  		{formatEntry{redactable: true}, ""},
   384  		{formatEntry{redactable: true, head: b("abc")}, " abc"},
   385  		{formatEntry{redactable: true, head: b("abc\nxyz")}, " abc\nxyz"},
   386  		{formatEntry{redactable: true, details: b("def")}, " def"},
   387  		{formatEntry{redactable: true, details: b("def\nxyz")}, " def\nxyz"},
   388  		{formatEntry{redactable: true, head: b("abc"), details: b("def")}, " abcdef"},
   389  		{formatEntry{redactable: true, head: b("abc\nxyz"), details: b("def")}, " abc\nxyzdef"},
   390  		{formatEntry{redactable: true, head: b("abc"), details: b("def\n  | xyz")}, " abcdef\n  | xyz"},
   391  		{formatEntry{redactable: true, head: b("abc\nxyz"), details: b("def\n  | xyz")}, " abc\nxyzdef\n  | xyz"},
   392  		// Entry already contains some markers.
   393  		{formatEntry{redactable: true, head: b("a " + q("bc")), details: b("d " + q("ef"))}, " a " + q("bc") + "d " + q("ef")},
   394  	}
   395  
   396  	for _, tc := range testCases {
   397  		s := state{redactableOutput: true}
   398  		s.printEntry(tc.entry)
   399  		if s.finalBuf.String() != tc.exp {
   400  			t.Errorf("%s: expected %q, got %q", tc.entry, tc.exp, s.finalBuf.String())
   401  		}
   402  	}
   403  }
   404  
   405  func TestFormatSingleLineOutputRedactable(t *testing.T) {
   406  	sm := string(redact.StartMarker())
   407  	em := string(redact.EndMarker())
   408  	// 	esc := string(redact.EscapeMarkers(redact.StartMarker()))
   409  	b := func(s string) []byte { return []byte(s) }
   410  	q := func(s string) string { return sm + s + em }
   411  
   412  	testCases := []struct {
   413  		entries []formatEntry
   414  		exp     string
   415  	}{
   416  		// If the entries are not redactable, then whatever characters they contain
   417  		// get enclosed within redaction markers.
   418  		{[]formatEntry{{}}, ``},
   419  		{[]formatEntry{{head: b(`a`)}}, q(`a`)},
   420  		{[]formatEntry{{head: b(`a`)}, {head: b(`b`)}, {head: b(`c`)}}, q(`c`) + ": " + q(`b`) + ": " + q(`a`)},
   421  		{[]formatEntry{{}, {head: b(`b`)}}, q(`b`)},
   422  		{[]formatEntry{{head: b(`a`)}, {}}, q(`a`)},
   423  		{[]formatEntry{{head: b(`a`)}, {}, {head: b(`c`)}}, q(`c`) + ": " + q(`a`)},
   424  		{[]formatEntry{{head: b(`a`), elideShort: true}, {head: b(`b`)}}, q(`b`)},
   425  		{[]formatEntry{{head: b("abc\ndef")}, {head: b("ghi\nklm")}}, q("ghi") + "\n" + q("klm") + ": " + q("abc") + "\n" + q("def")},
   426  
   427  		// If some entries are redactable but not others, then
   428  		// only those that are redactable are passed through.
   429  		{[]formatEntry{{redactable: true}}, ``},
   430  		{[]formatEntry{{redactable: true, head: b(`a`)}}, `a`},
   431  		{[]formatEntry{{redactable: true, head: b(`a`)}, {head: b(`b`)}, {redactable: true, head: b(`c`)}}, `c: ` + q(`b`) + `: a`},
   432  
   433  		{[]formatEntry{{redactable: true}, {head: b(`b`)}}, q(`b`)},
   434  		{[]formatEntry{{}, {redactable: true, head: b(`b`)}}, `b`},
   435  		{[]formatEntry{{redactable: true, head: b(`a`)}, {}}, `a`},
   436  		{[]formatEntry{{head: b(`a`)}, {redactable: true}}, q(`a`)},
   437  
   438  		{[]formatEntry{{head: b(`a`)}, {}, {head: b(`c`)}}, q(`c`) + `: ` + q(`a`)},
   439  		{[]formatEntry{{head: b(`a`)}, {redactable: true}, {head: b(`c`)}}, q(`c`) + `: ` + q(`a`)},
   440  		{[]formatEntry{{head: b(`a`), elideShort: true, redactable: true}, {head: b(`b`)}}, q(`b`)},
   441  		{[]formatEntry{{redactable: true, head: b("abc\ndef")}, {head: b("ghi\nklm")}}, q("ghi") + "\n" + q("klm") + ": abc\ndef"},
   442  		{[]formatEntry{{head: b("abc\ndef")}, {redactable: true, head: b("ghi\nklm")}}, "ghi\nklm: " + q("abc") + "\n" + q("def")},
   443  		// Entry already contains some markers.
   444  		{[]formatEntry{{redactable: true, head: b(`a` + q(" b"))}, {redactable: true, head: b(`c ` + q("d"))}}, `c ` + q(`d`) + `: a` + q(` b`)},
   445  	}
   446  
   447  	for _, tc := range testCases {
   448  		s := state{entries: tc.entries, redactableOutput: true}
   449  		s.formatSingleLineOutput()
   450  		if s.finalBuf.String() != tc.exp {
   451  			t.Errorf("%s: expected %q, got %q", tc.entries, tc.exp, s.finalBuf.String())
   452  		}
   453  	}
   454  }