From f1346c9dc0eb01023b8e6dc9d9bc0fc0726f5378 Mon Sep 17 00:00:00 2001 From: wood-jp <3757184+wood-jp@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:05:54 -0400 Subject: [PATCH] test(examples): Add example tests for godocs --- README.md | 38 +++++++++++++++++++++-- errclass/example_test.go | 63 ++++++++++++++++++++++++++++++++++++++ errcontext/example_test.go | 32 +++++++++++++++++++ example_test.go | 43 ++++++++++++++++++++++++++ stacktrace/example_test.go | 50 ++++++++++++++++++++++++++++++ stacktrace/stacktrace.go | 5 ++- 6 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 errclass/example_test.go create mode 100644 errcontext/example_test.go create mode 100644 example_test.go create mode 100644 stacktrace/example_test.go diff --git a/README.md b/README.md index adfe894..dca18e2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # xerrors +[![Go Reference](https://pkg.go.dev/badge/github.com/wood-jp/xerrors.svg)](https://pkg.go.dev/github.com/wood-jp/xerrors) [![CI](https://github.com/wood-jp/xerrors/actions/workflows/ci.yml/badge.svg)](https://github.com/wood-jp/xerrors/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/wood-jp/xerrors/badge.svg?branch=main)](https://coveralls.io/github/wood-jp/xerrors?branch=main) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) @@ -168,11 +169,42 @@ github.com/wood-jp/xerrors/stacktrace Captures a stack trace where `Wrap` is called and attaches it to the error. If the error already has a trace, `Wrap` is a no-op. +`StackTrace` implements `slog.LogValuer`, and appears as a `"stacktrace"` array in flat log output. For example: + ```go -err = stacktrace.Wrap(err) +var errTest = errors.New("something went wrong") + +func c() error { + return stacktrace.Wrap(errclass.WrapAs(errTest, errclass.Transient)) +} + +func b() error { return c() } +func a() error { return b() } + +err := a() +logger.Error("request failed", xerrors.Log(err)) ``` -Most likely, stack traces are only used in logging. `StackTrace` implements `slog.LogValuer`, and appears as a `"stacktrace"` array in flat log output. +Outputs a log similar to: + +```json +{ + "level": "ERROR", + "msg": "request failed", + "error": { + "error": "something went wrong", + "error_detail": { + "class": "transient", + "stacktrace": [ + {"func": "main.c", "line": 16, "source": "main.go"}, + {"func": "main.b", "line": 20, "source": "main.go"}, + {"func": "main.a", "line": 24, "source": "main.go"}, + {"func": "main.main", "line": 31, "source": "main.go"} + ] + } + } +} +``` However, if you wish to directly get at the stack trace data, you can pull the trace back out with `Extract`: @@ -192,4 +224,4 @@ This results in all `Wrap` calls becoming no-ops. ## Attribution -*Originally written by [wood-jp](https://github.com/wood-jp) at [Zircuit](https://www.zircuit.com/). Based on [zkr-go-common](https://github.com/zircuit-labs/zkr-go-common-public), MIT license.* +*Originally written by [wood-jp](https://github.com/wood-jp) at [Zircuit](https://www.zircuit.com/). Based on [zkr-go-common-public](https://github.com/zircuit-labs/zkr-go-common-public), MIT license.* diff --git a/errclass/example_test.go b/errclass/example_test.go new file mode 100644 index 0000000..6a533ca --- /dev/null +++ b/errclass/example_test.go @@ -0,0 +1,63 @@ +package errclass_test + +import ( + "errors" + "fmt" + "log/slog" + "os" + + "github.com/wood-jp/xerrors" + "github.com/wood-jp/xerrors/errclass" +) + +func newLogger() *slog.Logger { + return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey && len(groups) == 0 { + return slog.Attr{} + } + return a + }, + })) +} + +func ExampleWrapAs() { + err := errclass.WrapAs(errors.New("connection timed out"), errclass.Transient) + newLogger().Error("operation failed", xerrors.Log(err)) + // Output: + // {"level":"ERROR","msg":"operation failed","error":{"error":"connection timed out","error_detail":{"class":"transient"}}} +} + +func ExampleGetClass() { + err := errors.New("disk full") + err = errclass.WrapAs(err, errclass.Persistent) + fmt.Println(errclass.GetClass(err)) + // Output: + // persistent +} + +func ExampleGetClass_nil() { + fmt.Println(errclass.GetClass(nil)) + // Output: + // nil +} + +func ExampleWithOnlyMoreSevere() { + err := errors.New("failure") + err = errclass.WrapAs(err, errclass.Persistent) + // Transient is less severe than Persistent, so the class is unchanged. + err = errclass.WrapAs(err, errclass.Transient, errclass.WithOnlyMoreSevere()) + fmt.Println(errclass.GetClass(err)) + // Output: + // persistent +} + +func ExampleWithOnlyUnknown() { + err := errors.New("failure") + err = errclass.WrapAs(err, errclass.Transient) + // Error already has a class, so WithOnlyUnknown leaves it unchanged. + err = errclass.WrapAs(err, errclass.Persistent, errclass.WithOnlyUnknown()) + fmt.Println(errclass.GetClass(err)) + // Output: + // transient +} diff --git a/errcontext/example_test.go b/errcontext/example_test.go new file mode 100644 index 0000000..332f022 --- /dev/null +++ b/errcontext/example_test.go @@ -0,0 +1,32 @@ +package errcontext_test + +import ( + "errors" + "log/slog" + "os" + + "github.com/wood-jp/xerrors" + "github.com/wood-jp/xerrors/errcontext" +) + +func newLogger() *slog.Logger { + return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey && len(groups) == 0 { + return slog.Attr{} + } + return a + }, + })) +} + +func ExampleAdd() { + err := errors.New("request failed") + err = errcontext.Add(err, + slog.String("user_id", "u123"), + slog.Int("status", 503), + ) + newLogger().Error("handler error", xerrors.Log(err)) + // Output: + // {"level":"ERROR","msg":"handler error","error":{"error":"request failed","error_detail":{"context":{"status":503,"user_id":"u123"}}}} +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..ebf4545 --- /dev/null +++ b/example_test.go @@ -0,0 +1,43 @@ +package xerrors_test + +import ( + "errors" + "fmt" + "log/slog" + "os" + + "github.com/wood-jp/xerrors" +) + +func newLogger() *slog.Logger { + return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey && len(groups) == 0 { + return slog.Attr{} + } + return a + }, + })) +} + +func ExampleLog() { + type RequestInfo struct { + UserID string + Path string + } + + err := xerrors.Extend(RequestInfo{UserID: "u123", Path: "/api/items"}, errors.New("unauthorized")) + newLogger().Error("request failed", xerrors.Log(err)) + // Output: + // {"level":"ERROR","msg":"request failed","error":{"error":"unauthorized","error_detail":{"data":{"UserID":"u123","Path":"/api/items"}}}} +} + +func ExampleExtract() { + err := errors.New("database error") + err = xerrors.Extend(503, err) + + code, ok := xerrors.Extract[int](err) + fmt.Println(ok, code) + // Output: + // true 503 +} diff --git a/stacktrace/example_test.go b/stacktrace/example_test.go new file mode 100644 index 0000000..4da9b08 --- /dev/null +++ b/stacktrace/example_test.go @@ -0,0 +1,50 @@ +package stacktrace_test + +import ( + "bytes" + "errors" + "fmt" + "log/slog" + "regexp" + + "github.com/wood-jp/xerrors" + "github.com/wood-jp/xerrors/stacktrace" +) + +func newLogger(buf *bytes.Buffer) *slog.Logger { + return slog.New(slog.NewJSONHandler(buf, &slog.HandlerOptions{ + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey && len(groups) == 0 { + return slog.Attr{} + } + return a + }, + })) +} + +var ( + reSource = regexp.MustCompile(`"source":"[^"]*"`) + reLine = regexp.MustCompile(`"line":\d+`) +) + +func normalizeStack(s string) string { + s = reSource.ReplaceAllString(s, `"source":"..."`) + s = reLine.ReplaceAllString(s, `"line":0`) + return s +} + +func ExampleWrap() { + var buf bytes.Buffer + err := stacktrace.Wrap(errors.New("something failed")) + newLogger(&buf).Error("operation failed", xerrors.Log(err)) + fmt.Print(normalizeStack(buf.String())) + // Output: + // {"level":"ERROR","msg":"operation failed","error":{"error":"something failed","error_detail":{"stacktrace":[{"func":"github.com/wood-jp/xerrors/stacktrace_test.ExampleWrap","line":0,"source":"..."},{"func":"main.main","line":0,"source":"..."}]}}} +} + +func ExampleExtract_noStack() { + err := errors.New("plain error") + fmt.Println(stacktrace.Extract(err) == nil) + // Output: + // true +} diff --git a/stacktrace/stacktrace.go b/stacktrace/stacktrace.go index 5bac8c8..6e77ad5 100644 --- a/stacktrace/stacktrace.go +++ b/stacktrace/stacktrace.go @@ -1,4 +1,7 @@ -// Package stacktrace uses the go runtime to capture stack trace data. +// Package stacktrace captures and formats call stack information using the Go runtime. +// It provides [GetStack] to capture the current program stack, and [Wrap] / [Extract] +// to attach a [StackTrace] to an error. [StackTrace] implements [slog.LogValuer] for +// structured logging integration. package stacktrace import (