Added canonical sexps sans parsing
This commit is contained in:
79
shortcircuit/shortcircuit.go
Normal file
79
shortcircuit/shortcircuit.go
Normal file
@ -0,0 +1,79 @@
|
||||
package shortcircuit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Error indicates that a write was not attempted because
|
||||
// an earlier write failed due to Err.
|
||||
type Error struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (a Error) Error() string {
|
||||
return fmt.Sprintf("writer already failed: %v", a.Err)
|
||||
}
|
||||
|
||||
// Unwrap returns the error from the earlier failed write.
|
||||
func (e Error) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// Is returns true if target is an Error.
|
||||
func (e Error) Is(target error) bool {
|
||||
_, ok := target.(Error)
|
||||
return ok
|
||||
}
|
||||
|
||||
type Writer struct {
|
||||
err error
|
||||
n int64
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func EnsureWriter(w io.Writer) *Writer {
|
||||
scw, ok := w.(*Writer)
|
||||
if !ok {
|
||||
scw = NewWriter(w)
|
||||
}
|
||||
return scw
|
||||
}
|
||||
|
||||
func NewWriter(w io.Writer) *Writer {
|
||||
return &Writer{
|
||||
w: w,
|
||||
}
|
||||
}
|
||||
|
||||
// Write attempts to write p to the underlying io.Writer if no earlier call to
|
||||
// Write has failed. It returns the number of bytes written and an error if
|
||||
// the write failed. If an earlier call did fail, no bytes will be written and
|
||||
// err will be an Error containing the error from the failed call.
|
||||
func (w *Writer) Write(p []byte) (n int, err error) {
|
||||
if w.Failed() {
|
||||
return 0, Error{Err: w.err}
|
||||
}
|
||||
n, err = w.w.Write(p)
|
||||
w.n += int64(n)
|
||||
if err != nil {
|
||||
w.err = err
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Status returns the total bytes written to the underlying writer,
|
||||
// and any error resulting from a call to Write.
|
||||
func (w *Writer) Status() (written int64, err error) {
|
||||
return w.n, w.err
|
||||
}
|
||||
|
||||
func (w *Writer) Failed() bool {
|
||||
return w.err != nil
|
||||
}
|
||||
|
||||
// ClearError makes this Writer forget any earlier error from Write, so that
|
||||
// future calls to Write will attempt to write to the underlying io.Writer.
|
||||
func (s *Writer) ClearError() {
|
||||
s.err = nil
|
||||
}
|
127
shortcircuit/shortcircuit_test.go
Normal file
127
shortcircuit/shortcircuit_test.go
Normal file
@ -0,0 +1,127 @@
|
||||
package shortcircuit_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"git.codemonkeysoftware.net/b/peachy-go/shortcircuit"
|
||||
"github.com/shoenig/test"
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
var ErrFakeFailure = errors.New("operation successfully failed")
|
||||
|
||||
type FailingWriter struct {
|
||||
fail bool
|
||||
failAfter int
|
||||
writeCalls int
|
||||
}
|
||||
|
||||
func (f *FailingWriter) Fail() {
|
||||
f.FailAfter(0)
|
||||
}
|
||||
|
||||
func (f *FailingWriter) FailAfter(nBytes int) {
|
||||
f.failAfter = nBytes
|
||||
f.fail = true
|
||||
}
|
||||
|
||||
func (f *FailingWriter) Succeed() {
|
||||
f.fail = false
|
||||
}
|
||||
|
||||
func (f *FailingWriter) Write(p []byte) (int, error) {
|
||||
f.writeCalls++
|
||||
if !f.fail {
|
||||
return len(p), nil
|
||||
}
|
||||
if f.failAfter > len(p) {
|
||||
f.failAfter -= len(p)
|
||||
return len(p), nil
|
||||
}
|
||||
n := f.failAfter
|
||||
f.failAfter = 0
|
||||
return n, ErrFakeFailure
|
||||
}
|
||||
|
||||
func (f *FailingWriter) WriteCalls() int {
|
||||
return f.writeCalls
|
||||
}
|
||||
|
||||
func TestWriter(t *testing.T) {
|
||||
t.Run("writes multiple times and counts total bytes written", func(t *testing.T) {
|
||||
inputs := [][]byte{
|
||||
[]byte("abcdefghi"),
|
||||
[]byte("jklmnopqr"),
|
||||
[]byte("stuvwxyz"),
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
w := shortcircuit.NewWriter(&buf)
|
||||
for _, b := range inputs {
|
||||
n, err := w.Write(slices.Clone(b))
|
||||
must.NoError(t, err)
|
||||
must.False(t, w.Failed())
|
||||
must.EqOp(t, len(b), n, must.Sprint("expected Write to return length of byte slice"))
|
||||
}
|
||||
allInput := slices.Concat(inputs...)
|
||||
test.EqFunc(t, allInput, buf.Bytes(), bytes.Equal, test.Sprintf("expected written bytes to equal original bytes"))
|
||||
n, err := w.Status()
|
||||
test.EqOp(t, int64(len(allInput)), n, test.Sprint("expected total bytes written to equal total length of inputs"))
|
||||
test.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("fails without writing if error was already returned, but tries again after clearing error", func(t *testing.T) {
|
||||
w := new(FailingWriter)
|
||||
sc := shortcircuit.NewWriter(w)
|
||||
b := []byte("abcdefghi")
|
||||
expectedTotal := int64(len(b))
|
||||
|
||||
// Succeed
|
||||
n, err := sc.Write(b)
|
||||
must.EqOp(t, len(b), n)
|
||||
must.NoError(t, err)
|
||||
must.EqOp(t, 1, w.WriteCalls())
|
||||
must.False(t, sc.Failed())
|
||||
|
||||
// Fail
|
||||
const limit = 3
|
||||
w.FailAfter(limit)
|
||||
expectedTotal += limit
|
||||
|
||||
n, err = sc.Write(b)
|
||||
must.EqOp(t, limit, n)
|
||||
must.EqOp(t, ErrFakeFailure, err)
|
||||
must.True(t, sc.Failed())
|
||||
must.EqOp(t, 2, w.WriteCalls())
|
||||
total, err := sc.Status()
|
||||
must.EqOp(t, ErrFakeFailure, err)
|
||||
must.EqOp(t, expectedTotal, total)
|
||||
|
||||
// Fail again
|
||||
n, err = sc.Write(b)
|
||||
must.EqOp(t, 0, n)
|
||||
must.ErrorIs(t, err, shortcircuit.Error{})
|
||||
must.ErrorIs(t, err, ErrFakeFailure)
|
||||
must.True(t, sc.Failed())
|
||||
must.EqOp(t, 2, w.WriteCalls())
|
||||
total, err = sc.Status()
|
||||
must.EqOp(t, ErrFakeFailure, err)
|
||||
must.EqOp(t, expectedTotal, total)
|
||||
|
||||
// Clear and succeed
|
||||
w.Succeed()
|
||||
sc.ClearError()
|
||||
expectedTotal += int64(len(b))
|
||||
|
||||
n, err = sc.Write(b)
|
||||
must.EqOp(t, len(b), n)
|
||||
must.NoError(t, err)
|
||||
must.False(t, sc.Failed())
|
||||
must.EqOp(t, 3, w.WriteCalls())
|
||||
total, err = sc.Status()
|
||||
must.EqOp(t, expectedTotal, total)
|
||||
must.NoError(t, err)
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user