-
Notifications
You must be signed in to change notification settings - Fork 451
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
AVM: report structure txn failure info (#5875)
- Loading branch information
Showing
18 changed files
with
758 additions
and
141 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
// Copyright (C) 2019-2024 Algorand, Inc. | ||
// This file is part of go-algorand | ||
// | ||
// go-algorand is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU Affero General Public License as | ||
// published by the Free Software Foundation, either version 3 of the | ||
// License, or (at your option) any later version. | ||
// | ||
// go-algorand is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU Affero General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU Affero General Public License | ||
// along with go-algorand. If not, see <https://www.gnu.org/licenses/>. | ||
|
||
package basics | ||
|
||
import ( | ||
"errors" | ||
"strings" | ||
|
||
"golang.org/x/exp/slog" | ||
) | ||
|
||
// SError is a structured error object. It contains a message and an arbitrary | ||
// set of attributes. If the message contains "%A", it will be replaced by the | ||
// attributes (in no guaranteed order), when SError() is called. | ||
// | ||
//msgp:ignore SError | ||
type SError struct { | ||
Msg string | ||
Attrs map[string]any | ||
Wrapped error | ||
} | ||
|
||
// New creates a new structured error object using the supplied message and | ||
// attributes. If the message contains "%A", it will be replaced by the | ||
// attributes when Error() is called. | ||
func New(msg string, pairs ...any) *SError { | ||
attrs := make(map[string]any, len(pairs)/2) | ||
for i := 0; i < len(pairs); i += 2 { | ||
attrs[pairs[i].(string)] = pairs[i+1] | ||
} | ||
return &SError{Msg: msg, Attrs: attrs} | ||
} | ||
|
||
// Error returns either the exact supplied message, or the serialized attributes if | ||
// the supplied message was blank, or substituted for %A. | ||
func (e *SError) Error() string { | ||
if e.Msg == "" { | ||
return e.AttributesAsString() | ||
} | ||
// imperfect because we replace \%A as well | ||
if strings.Contains(e.Msg, "%A") { | ||
return strings.Replace(e.Msg, "%A", e.AttributesAsString(), -1) | ||
} | ||
return e.Msg | ||
} | ||
|
||
// AttributesAsString returns the attributes the same way that slog serializes | ||
// attributes to text in a log message, in no guaranteed order. | ||
func (e *SError) AttributesAsString() string { | ||
var buf strings.Builder | ||
args := make([]any, 0, 2*len(e.Attrs)) | ||
for key, val := range e.Attrs { | ||
args = append(args, key) | ||
args = append(args, val) | ||
} | ||
l := slog.New(slog.NewTextHandler(&buf, nil)) | ||
l.Info("", args...) | ||
return strings.TrimSuffix(strings.SplitN(buf.String(), " ", 4)[3], "\n") | ||
} | ||
|
||
// Annotate adds additional attributes to an existing error, even if the error | ||
// is deep in the error chain. If the supplied error is nil, nil is returned so | ||
// that callers can annotate errors without checking if they are non-nil. If | ||
// the error is not a structured error, it is wrapped in one using its existing | ||
// message and the new attributes. Just like append() for slices, callers should | ||
// re-assign, like this `err = serr.Annotate(err, "x", 100)` | ||
func Annotate(err error, pairs ...any) error { | ||
if err == nil { | ||
return nil | ||
} | ||
var serr *SError | ||
if ok := errors.As(err, &serr); ok { | ||
for i := 0; i < len(pairs); i += 2 { | ||
serr.Attrs[pairs[i].(string)] = pairs[i+1] | ||
} | ||
return err | ||
} | ||
// Since we don't have a structured error, we wrap the existing error in one. | ||
serr = New(err.Error(), pairs...) | ||
serr.Wrapped = err | ||
return serr | ||
} | ||
|
||
// Wrap is used to "demote" an existing error to a field in a new structured | ||
// error. The wrapped error message is added as $field-msg, and if the error is | ||
// structured, the attributes are added under $field-attrs. | ||
func Wrap(err error, msg string, field string, pairs ...any) error { | ||
serr := New(msg, field+"-msg", err.Error()) | ||
for i := 0; i < len(pairs); i += 2 { | ||
serr.Attrs[pairs[i].(string)] = pairs[i+1] | ||
} | ||
serr.Wrapped = err | ||
|
||
var inner *SError | ||
if ok := errors.As(err, &inner); ok { | ||
attributes := make(map[string]any, len(inner.Attrs)) | ||
for key, val := range inner.Attrs { | ||
attributes[key] = val | ||
} | ||
serr.Attrs[field+"-attrs"] = attributes | ||
} | ||
|
||
return serr | ||
} | ||
|
||
// Unwrap returns the inner error, if it exists. | ||
func (e *SError) Unwrap() error { | ||
return e.Wrapped | ||
} | ||
|
||
// Attributes returns the attributes of a structured error, or nil/empty. | ||
func Attributes(err error) map[string]any { | ||
var se *SError | ||
if errors.As(err, &se) { | ||
return se.Attrs | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
// Copyright (C) 2019-2024 Algorand, Inc. | ||
// This file is part of go-algorand | ||
// | ||
// go-algorand is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU Affero General Public License as | ||
// published by the Free Software Foundation, either version 3 of the | ||
// License, or (at your option) any later version. | ||
// | ||
// go-algorand is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU Affero General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU Affero General Public License | ||
// along with go-algorand. If not, see <https://www.gnu.org/licenses/>. | ||
|
||
package basics | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/algorand/go-algorand/test/partitiontest" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestNew(t *testing.T) { | ||
partitiontest.PartitionTest(t) | ||
t.Parallel() | ||
|
||
err := New("test") | ||
assert.Equal(t, "test", err.Error()) | ||
} | ||
|
||
func TestNewWithPairs(t *testing.T) { | ||
partitiontest.PartitionTest(t) | ||
t.Parallel() | ||
|
||
err := New("test", "a", 7, "b", []byte{3, 4}) | ||
assert.Equal(t, "test", err.Error()) | ||
assert.Equal(t, 7, err.Attrs["a"]) | ||
assert.Equal(t, []byte{3, 4}, err.Attrs["b"]) | ||
|
||
err.Msg = "" | ||
assert.ErrorContains(t, err, `a=7`) | ||
assert.ErrorContains(t, err, `b="\x03\x04"`) | ||
|
||
err.Msg = "check it: %A" | ||
assert.ErrorContains(t, err, ` a=7`) | ||
assert.ErrorContains(t, err, ` b="\x03\x04"`) | ||
assert.Equal(t, `check it: `, err.Error()[:10]) | ||
|
||
} | ||
|
||
func TestAnnotate(t *testing.T) { | ||
partitiontest.PartitionTest(t) | ||
t.Parallel() | ||
|
||
err := New("test", "a", 7, "b", []byte{3, 4}) | ||
assert.Equal(t, 7, err.Attrs["a"]) | ||
assert.Equal(t, nil, err.Attrs["c"]) | ||
Annotate(err, "c", true, "a", false) | ||
assert.Equal(t, true, err.Attrs["c"]) | ||
assert.Equal(t, false, err.Attrs["a"]) | ||
} | ||
|
||
func attribute(err error, name string) any { | ||
var serr *SError | ||
if ok := errors.As(err, &serr); ok { | ||
return serr.Attrs[name] | ||
} | ||
return nil | ||
} | ||
|
||
func TestAnnotateUnstructured(t *testing.T) { | ||
partitiontest.PartitionTest(t) | ||
t.Parallel() | ||
|
||
err := errors.New("hello") | ||
err = Annotate(err, "c", true, "a", false) | ||
assert.Equal(t, true, attribute(err, "c")) | ||
} | ||
|
||
func TestReannotateEmbedded(t *testing.T) { | ||
partitiontest.PartitionTest(t) | ||
t.Parallel() | ||
|
||
var err error | ||
err = New("test", "a", 7, "b", []byte{3, 4}) | ||
err = fmt.Errorf("embed the above here %w", err) | ||
assert.Equal(t, 7, attribute(err, "a")) | ||
assert.Equal(t, nil, attribute(err, "c")) | ||
Annotate(err, "c", true, "a", false) | ||
assert.Equal(t, true, attribute(err, "c")) | ||
assert.Equal(t, false, attribute(err, "a")) | ||
// "b" is still visible. It would not be is we had _wrapped_ the fmt.Error | ||
assert.Equal(t, []byte{3, 4}, attribute(err, "b")) | ||
} | ||
|
||
func TestWrapBare(t *testing.T) { | ||
partitiontest.PartitionTest(t) | ||
t.Parallel() | ||
|
||
var err error | ||
err = errors.New("inner thingy") | ||
err = Wrap(err, "outer stuff", "xxx") | ||
assert.Equal(t, "inner thingy", attribute(err, "xxx-msg")) | ||
assert.Equal(t, nil, attribute(err, "xxx-attrs")) | ||
} | ||
|
||
func TestWrapStructured(t *testing.T) { | ||
partitiontest.PartitionTest(t) | ||
t.Parallel() | ||
|
||
var err error | ||
err = New("test", "a", 7, "b", []byte{3, 4}) | ||
err = Wrap(err, "outer stuff", "yyy") | ||
assert.Equal(t, "test", attribute(err, "yyy-msg")) | ||
assert.NotNil(t, attribute(err, "yyy-attrs")) | ||
|
||
// these are deeper now, not here | ||
assert.Equal(t, nil, attribute(err, "a")) | ||
assert.Equal(t, nil, attribute(err, "b")) | ||
|
||
// here they are | ||
attrs := attribute(err, "yyy-attrs").(map[string]any) | ||
assert.Equal(t, 7, attrs["a"]) | ||
assert.Equal(t, []byte{3, 4}, attrs["b"]) | ||
|
||
// deeper, with a new attribute | ||
err = Wrap(err, "further out", "again", "name", "jj") | ||
assert.Nil(t, attribute(err, "yyy-msg")) | ||
assert.Equal(t, "outer stuff", attribute(err, "again-msg")) | ||
} |
Oops, something went wrong.