github.com/haraldrudell/parl@v0.4.176/perrors/errorglue/chain-string_test.go (about)

     1  /*
     2  © 2020–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package errorglue
     7  
     8  import (
     9  	"errors"
    10  	"fmt"
    11  	"reflect"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/haraldrudell/parl/pruntime"
    16  )
    17  
    18  // tests a chain of errors
    19  func TestChainString(t *testing.T) {
    20  	//t.Error("logging on")
    21  	// errF is a fixture with a complex error graph
    22  	var errF = errFixture{
    23  		errorMessage:    "error-message",
    24  		errorMsg2:       "associated-error-message",
    25  		wrapText:        "Prefix: '%w'",
    26  		expectedMessage: "Prefix: 'error-message'",
    27  		key1:            "key1",
    28  		value1:          "value1",
    29  		// key2 is empty string
    30  		key2:   "",
    31  		value2: "value2",
    32  	}
    33  	const errorsWithStackCount = 2
    34  	const err1FrameLength = 1
    35  
    36  	var err, err1 error
    37  	var messageAct, actualString string
    38  	var errsWithStack []error
    39  	var stack pruntime.Stack
    40  
    41  	// err is errorStack written by FuncName goroutine
    42  	//	- error 1 is errorStack “Prefix…”
    43  	//	- error 2 is from [fmt.Errorf]: [fmt.wrapError]
    44  	//	- error 3 is related error: two errors
    45  	//	- — the associated error is errorStack “associated…”
    46  	//	- error 4 is errorData
    47  	//	- error 5 is errorData
    48  	//	- error 6 is erorrStack
    49  	//	- error 7 is errors.errorString
    50  	err = errF.createError()
    51  
    52  	// err should not be nil
    53  	if err == nil {
    54  		t.Fatal("FuncName did not update err")
    55  	}
    56  
    57  	// err.Error() should match
    58  	messageAct = err.Error()
    59  	if messageAct != errF.expectedMessage {
    60  		t.Errorf("bad error message %q expected: %q", messageAct, errF.expectedMessage)
    61  	}
    62  
    63  	// stack error count should match
    64  	errsWithStack = ErrorsWithStack(err) // error instances with stack in this error chain
    65  	if len(errsWithStack) != errorsWithStackCount {
    66  		t.Fatalf("FuncName did not add %d stack traces: %d", errorsWithStackCount, len(errsWithStack))
    67  	}
    68  
    69  	// LongFormat:
    70  	// error-message [*errorglue.errorStack]ID: 19 IsMain: false status: running
    71  	// github.com/haraldrudell/parl/perrors/errorglue.(*errFixture).FuncName(0x1400008e8f0)
    72  	// 	chain-string_test.go:251
    73  	// cre: github.com/haraldrudell/parl/perrors/errorglue.(*errFixture).Do-chain-string_test.go:215 in goroutine 18 18
    74  	// error-message [*errors.errorString]
    75  	err1 = errsWithStack[0]
    76  	t.Logf("LongFormat:\n%s", ChainString(err1, LongFormat))
    77  
    78  	// first error stack depth should match
    79  	stack = err1.(*errorStack).StackTrace()
    80  	if len(stack.Frames()) != err1FrameLength {
    81  		t.Errorf("Stack length not %d: %d", err1FrameLength, len(stack.Frames()))
    82  	}
    83  
    84  	// err: ‘Prefix: 'error-message'’
    85  	t.Logf("err: ‘%s’", err)
    86  
    87  	// err DumpChain:
    88  	// *errorglue.errorStack
    89  	// *fmt.wrapError
    90  	// *errorglue.relatedError
    91  	// *errorglue.errorData
    92  	// *errorglue.errorData
    93  	// *errorglue.errorStack
    94  	// *errors.errorString
    95  	t.Logf("err DumpChain: %s", DumpChain(err))
    96  
    97  	actualString = ChainString(err, DefaultFormat)
    98  
    99  	// DefaultFormat: ‘Prefix: 'error-message'’
   100  	t.Logf("DefaultFormat: ‘%s’", actualString)
   101  
   102  	// DefaultFormat should be same as Error()
   103  	if actualString != errF.expectedMessage {
   104  		t.Errorf("FAIL DefaultFormat: %q expected: %q", actualString, errF.expectedMessage)
   105  	}
   106  
   107  	actualString = ChainString(err, ShortFormat)
   108  
   109  	// ShortFormat:
   110  	// ‘Prefix: 'error-message' at errorglue.(*errFixture).FuncName()-chain-string_test.go:195
   111  	// 1[associated-error-message at errorglue.(*errFixture).FuncName()-chain-string_test.go:193]’
   112  	t.Logf("ShortFormat: ‘%s’", actualString)
   113  
   114  	// ShortFormat should be Error() and location
   115  	var expected = errF.expectedMessage + " at " + errF.stack2.Frames()[0].Loc().Short()
   116  	if !strings.HasPrefix(actualString, expected) {
   117  		t.Errorf("FAIL ShortFormat:\n%q expected:\n%q", actualString, expected)
   118  	}
   119  
   120  	actualString = ChainString(err, LongFormat)
   121  
   122  	//   error-message
   123  	//     github.com/haraldrudell/parl/error116.(*csTypeName).FuncName
   124  	//       /opt/sw/privates/parl/error116/chainstring_test.go:26
   125  	//     runtime.goexit
   126  	//       /opt/homebrew/Cellar/go/1.17.8/libexec/src/runtime/asm_arm64.s:1133
   127  	t.Logf("LongFormat:\n%s", actualString)
   128  }
   129  
   130  // tests [perrrors.AppendError]
   131  func TestAppended(t *testing.T) {
   132  	//t.Error("logging on")
   133  	var message1, message2 = "error1", "error2"
   134  	var err = NewErrorStack(errors.New(message1), pruntime.NewStack(0))
   135  	var err2 = NewRelatedError(err, NewErrorStack(errors.New(message2), pruntime.NewStack(0)))
   136  	var prefix1 = message1 + " at errorglue."
   137  	var contains2 = " 1[" + message2 + " at errorglue."
   138  
   139  	var stringAct string
   140  	_ = 1
   141  
   142  	stringAct = ChainString(err2, ShortFormat)
   143  
   144  	// stringAct:
   145  	// error1 at errorglue.TestAppended()-chain-string_test.go:134
   146  	// 1[error2 at errorglue.TestAppended()-chain-string_test.go:135]
   147  	t.Logf("stringAct: %s", stringAct)
   148  
   149  	// err2 shortFormat should begin with prefix1
   150  	if !strings.HasPrefix(stringAct, prefix1) {
   151  		t.Errorf("does not start with: %q: %q", prefix1, stringAct)
   152  	}
   153  
   154  	// err2 shortFormat should contain message2
   155  	if !strings.Contains(stringAct, contains2) {
   156  		t.Errorf("does not contain: %q: %q", contains2, stringAct)
   157  	}
   158  }
   159  
   160  func TestChainStringList(t *testing.T) {
   161  	var errNew = errors.New("new")
   162  	var errErrorf1 = fmt.Errorf("errorf1 %w", errNew)
   163  	var stack = shortStack()
   164  	// errorStack is a rich error
   165  	//	- does not modify error message
   166  	var errStack = NewErrorStack(errErrorf1, stack)
   167  	var errErrorf2 = fmt.Errorf("errorf2 %w", errStack)
   168  	var relatedErr = errors.New("related")
   169  	// err is an error graph
   170  	var err = NewRelatedError(errErrorf2, relatedErr)
   171  	// expected results for all formats
   172  	var formatExpMap = map[CSFormat]string{
   173  		DefaultFormat: err.Error(),
   174  		CodeLocation:  err.Error() + " at " + stack.Frames()[0].Loc().Short(),
   175  		ShortFormat:   err.Error() + " at " + stack.Frames()[0].Loc().Short() + " 1[related]",
   176  		LongFormat: strings.Join([]string{
   177  			err.Error() + " [" + reflect.TypeOf(err).String() + "]",
   178  			errErrorf2.Error() + " [" + reflect.TypeOf(errErrorf2).String() + "]",
   179  			errStack.Error() + " [" + reflect.TypeOf(errStack).String() + "]" + "\n" + stack.String(),
   180  			errErrorf1.Error() + " [" + reflect.TypeOf(errErrorf1).String() + "]",
   181  			errNew.Error() + " [" + reflect.TypeOf(errNew).String() + "]",
   182  			relatedErr.Error() + " [" + reflect.TypeOf(relatedErr).String() + "]",
   183  		}, "\n"),
   184  		ShortSuffix: stack.Frames()[0].Loc().Short(),
   185  		LongSuffix: strings.Join([]string{
   186  			err.Error() + " [" + reflect.TypeOf(err).String() + "]",
   187  			stack.String(),
   188  			relatedErr.Error() + " [" + reflect.TypeOf(relatedErr).String() + "]",
   189  		}, "\n"),
   190  	}
   191  
   192  	var formatAct, formatExp string
   193  	var ok bool
   194  
   195  	// err error-chain:
   196  	// *errorglue.relatedError *fmt.wrapError
   197  	// *errorglue.errorStack *fmt.wrapError *errors.errorString
   198  	t.Logf("err error-chain: %s", DumpChain(err))
   199  
   200  	for _, csFormat := range csFormatList {
   201  		if formatExp, ok = formatExpMap[csFormat]; !ok {
   202  			t.Errorf("no expected value for format: %s", csFormat)
   203  		}
   204  		formatAct = ChainString(err, csFormat)
   205  
   206  		// DefaultFormat: errorf2 errorf1 new
   207  		// ShortFormat: errorf2 errorf1 new at testing.tRunner()-testing.go:1595 1[related]
   208  		// LongFormat: errorf2 errorf1 new [*errorglue.relatedError]
   209  		// errorf2 errorf1 new [*fmt.wrapError]
   210  		// errorf1 new [*errorglue.errorStack]
   211  		// ID: 20 status: ‘running’
   212  		// testing.tRunner(0x14000082ea0, 0x102ea5950)
   213  		// 	/opt/homebrew/Cellar/go/1.21.7/libexec/src/testing/testing.go:1595
   214  		// Parent-ID: 1 go: testing.(*T).Run
   215  		// 	/opt/homebrew/Cellar/go/1.21.7/libexec/src/testing/testing.go:1648
   216  		// errorf1 new [*fmt.wrapError]
   217  		// new [*errors.errorString]
   218  		// related [*errors.errorString]
   219  		// ShortSuffix: testing.tRunner()-testing.go:1595
   220  		// LongSuffix: errorf2 errorf1 new [*errorglue.relatedError]
   221  		// ID: 20 status: ‘running’
   222  		// testing.tRunner(0x14000082ea0, 0x102ea5950)
   223  		// 	/opt/homebrew/Cellar/go/1.21.7/libexec/src/testing/testing.go:1595
   224  		// Parent-ID: 1 go: testing.(*T).Run
   225  		// 	/opt/homebrew/Cellar/go/1.21.7/libexec/src/testing/testing.go:1648
   226  		// related [*errors.errorString]
   227  		t.Logf("%s: %s", csFormat, formatAct)
   228  
   229  		// ChainString should match
   230  		if formatAct != formatExp {
   231  			t.Errorf("FAIL format: %s:\n%q exp\n%q",
   232  				csFormat, formatAct, formatExp,
   233  			)
   234  		}
   235  	}
   236  }
   237  
   238  // shortStack retruns a short stack slice
   239  func shortStack() (stack pruntime.Stack) { return pruntime.NewStack(2) }
   240  
   241  // uses a goroutine to create an err fixture including
   242  // errorStack and errorData
   243  type errFixture struct {
   244  	// “error-message”
   245  	errorMessage string
   246  	// “associated-error-message”
   247  	errorMsg2 string
   248  	// wrapText is first associated error
   249  	//	- “Prefix: '%w'”
   250  	//	- used with [fmt.Errorf]
   251  	wrapText string
   252  	// “Prefix: 'error-message'”
   253  	expectedMessage string
   254  	key1            string
   255  	value1          string
   256  	key2            string
   257  	value2          string
   258  	stack0          pruntime.Stack
   259  	stack1          pruntime.Stack
   260  	stack2          pruntime.Stack
   261  }
   262  
   263  // createError returns an error fixture
   264  func (n *errFixture) createError() (err error) {
   265  
   266  	// execute goroutine FuncName to end
   267  	var ch = make(chan struct{})
   268  	go n.FuncName(ch, &err)
   269  	<-ch
   270  
   271  	return
   272  }
   273  
   274  // goroutine that build n.err fixture
   275  func (n *errFixture) FuncName(ch chan struct{}, errp *error) {
   276  	defer close(ch)
   277  	n.stack0 = pruntime.NewStack(0)
   278  	n.stack1 = pruntime.NewStack(0)
   279  	n.stack2 = pruntime.NewStack(0)
   280  	// two stack traces
   281  	// one associated error
   282  	// a key-value string and a list string
   283  	*errp =
   284  		NewErrorStack(
   285  			fmt.Errorf(n.wrapText,
   286  				NewRelatedError(
   287  					NewErrorData(
   288  						NewErrorData(
   289  							NewErrorStack(errors.New(n.errorMessage), n.stack0),
   290  							n.key1, n.value1),
   291  						n.key2, n.value2),
   292  					NewErrorStack(errors.New(n.errorMsg2), n.stack1),
   293  				)),
   294  			n.stack2)
   295  }