Skip to content

Commit

Permalink
Merge pull request #188 from vito/runs-errgroup
Browse files Browse the repository at this point in the history
better error handling
  • Loading branch information
vito authored Jun 13, 2022
2 parents 304bb8c + fd028cd commit 5007d7e
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 33 deletions.
4 changes: 4 additions & 0 deletions bass/util.bass
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
; just for eyeballing files
(defn ls paths
(from (linux/alpine)
($ ls -al & $paths)))
20 changes: 11 additions & 9 deletions pkg/bass/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io"
"sort"
"strings"

"github.com/agext/levenshtein"
"github.com/morikuni/aec"
Expand All @@ -19,7 +20,7 @@ import (
type NiceError interface {
error

NiceError(io.Writer) error
NiceError(io.Writer, error) error
}

type FlagError struct {
Expand All @@ -31,8 +32,8 @@ func (err FlagError) Error() string {
return err.Err.Error()
}

func (err FlagError) NiceError(w io.Writer) error {
fmt.Fprintf(w, "\x1b[31m%s\x1b[0m\n", err)
func (err FlagError) NiceError(w io.Writer, outer error) error {
fmt.Fprintln(w, aec.RedF.Apply(outer.Error()))
fmt.Fprintln(w)
fmt.Fprintln(w, "flags:")

Expand Down Expand Up @@ -80,8 +81,8 @@ func (err UnboundError) Error() string {
return fmt.Sprintf("unbound symbol: %s", err.Symbol)
}

func (unbound UnboundError) NiceError(w io.Writer) error {
_, err := fmt.Fprintf(w, aec.RedF.Apply("unbound symbol: %s")+"\n", unbound.Symbol)
func (unbound UnboundError) NiceError(w io.Writer, outer error) error {
fmt.Fprintln(w, aec.RedF.Apply(outer.Error()))

similar := []Symbol{}
unbound.Scope.Each(func(k Symbol, _ Value) error {
Expand Down Expand Up @@ -119,7 +120,7 @@ func (unbound UnboundError) NiceError(w io.Writer) error {
fmt.Fprintf(w, "did you mean %s, perchance?\n", aec.Bold.Apply(string(similar[0])))
}

return err
return nil
}

type ArityError struct {
Expand Down Expand Up @@ -210,11 +211,12 @@ func (err *StructuredError) Error() string {
return err.Message
}

return fmt.Sprintf("%s; %s", err.Message, err.Fields)
return fmt.Sprintf("%s; fields: %s", err.Message, err.Fields)
}

func (structured *StructuredError) NiceError(w io.Writer) error {
fmt.Fprintln(w, aec.RedF.Apply(structured.Message))
func (structured *StructuredError) NiceError(w io.Writer, outer error) error {
prefix, _, _ := strings.Cut(outer.Error(), "; fields: ")
fmt.Fprintln(w, aec.RedF.Apply(prefix))

if len(structured.Fields.Bindings) > 0 {
fmt.Fprintln(w)
Expand Down
11 changes: 8 additions & 3 deletions pkg/bass/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package bass_test
import (
"bufio"
"bytes"
"errors"
"fmt"
"testing"

Expand All @@ -25,7 +26,7 @@ func TestUnboundErrorNice(t *testing.T) {
"foo",
bass.Bindings{},
[]string{
aec.RedF.Apply(`unbound symbol: foo`),
aec.RedF.Apply(`wrapped: unbound symbol: foo`),
},
},
{
Expand All @@ -38,7 +39,7 @@ func TestUnboundErrorNice(t *testing.T) {
"f12": bass.Null{},
},
output{
aec.RedF.Apply(`unbound symbol: f123`),
aec.RedF.Apply(`wrapped: unbound symbol: f123`),
``,
`similar bindings:`,
``,
Expand All @@ -61,8 +62,12 @@ func TestUnboundErrorNice(t *testing.T) {
Scope: scope,
}

var nice bass.NiceError
err := fmt.Errorf("wrapped: %w", unboundErr)
is.True(errors.As(err, &nice))

out := new(bytes.Buffer)
is.NoErr(unboundErr.NiceError(out))
is.NoErr(nice.NiceError(out, err))

scanner := bufio.NewScanner(out)
for _, line := range example.Message {
Expand Down
30 changes: 29 additions & 1 deletion pkg/bass/runs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,38 @@ package bass
import (
"context"
"sync"

"github.com/hashicorp/go-multierror"
)

type Runs struct {
sync.WaitGroup
wg sync.WaitGroup

errs error
errsL sync.Mutex
}

func (runs *Runs) Go(f func() error) {
runs.wg.Add(1)
go func() {
defer runs.wg.Done()
runs.record(f())
}()
}

func (runs *Runs) Wait() error {
runs.wg.Wait()
return runs.errs
}

func (runs *Runs) record(err error) {
runs.errsL.Lock()
if runs.errs != nil {
runs.errs = multierror.Append(runs.errs, err)
} else {
runs.errs = err
}
runs.errsL.Unlock()
}

type runsKey struct{}
Expand Down
54 changes: 54 additions & 0 deletions pkg/bass/runs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package bass_test

import (
"context"
"fmt"
"strings"
"testing"

"github.com/vito/bass/pkg/bass"
"github.com/vito/bass/pkg/basstest"
"github.com/vito/is"
)

func TestRuns(t *testing.T) {
is := is.New(t)

ctx := context.Background()
ctx, runs := bass.TrackRuns(ctx)

errorScpt := bass.NewInMemoryFile("error.bass", `
(defn main [msg]
(error msg))
`)

thunk1 := bass.MustThunk(errorScpt).AppendArgs(bass.String("oh no"))
thunk2 := bass.MustThunk(errorScpt).AppendArgs(bass.String("let's go"))

errCb := bass.Func("err-if-not-ok", "[ok?]", func(ok bool) error {
if !ok {
return fmt.Errorf("it failed!")
}

return nil
})

comb1, err := thunk1.Start(ctx, errCb)
is.NoErr(err)

comb2, err := thunk2.Start(ctx, errCb)
is.NoErr(err)

_, err = basstest.Call(comb1, bass.NewEmptyScope(), bass.NewList())
is.True(err != nil)
is.Equal(err.Error(), "it failed!: oh no")

_, err = basstest.Call(comb2, bass.NewEmptyScope(), bass.NewList())
is.True(err != nil)
is.Equal(err.Error(), "it failed!: let's go")

err = runs.Wait()
is.True(err != nil)
is.True(strings.Contains(err.Error(), "it failed!: oh no"))
is.True(strings.Contains(err.Error(), "it failed!: let's go"))
}
15 changes: 11 additions & 4 deletions pkg/bass/thunk.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,10 @@ func (thunk Thunk) Start(ctx context.Context, handler Combiner) (Combiner, error
var waitErr error

runs := RunsFromContext(ctx)
runs.Add(1)

wg := new(sync.WaitGroup)
wg.Add(1)
go func() {
defer runs.Done()
runs.Go(func() error {
defer wg.Done()

runErr := thunk.Run(ctx, io.Discard)
Expand All @@ -217,7 +216,9 @@ func (thunk Thunk) Start(ctx context.Context, handler Combiner) (Combiner, error
} else {
waitRes = res
}
}()

return waitErr
})

return Func(thunk.String(), "[]", func() (Value, error) {
wg.Wait()
Expand Down Expand Up @@ -289,6 +290,12 @@ func (thunk Thunk) WithArgs(args []Value) Thunk {
return thunk
}

// AppendArgs appends to the thunk's arg values.
func (thunk Thunk) AppendArgs(args ...Value) Thunk {
thunk.Args = append(thunk.Args, args...)
return thunk
}

// WithEnv sets the thunk's env.
func (thunk Thunk) WithEnv(env *Scope) Thunk {
thunk.Env = env
Expand Down
5 changes: 3 additions & 2 deletions pkg/cli/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ func WriteError(ctx context.Context, err error) {
}
}

if nice, ok := err.(bass.NiceError); ok {
metaErr := nice.NiceError(out)
var nice bass.NiceError
if errors.As(err, &nice) {
metaErr := nice.NiceError(out, err)
if metaErr != nil {
fmt.Fprintf(out, aec.RedF.Apply("errored while erroring: %s")+"\n", metaErr)
fmt.Fprintf(out, aec.RedF.Apply("original error: %T: %s")+"\n", err, err)
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/progress_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ func (err ProgressError) Error() string {
return fmt.Sprintf("%s: %s", err.msg, stripUselessPart(rootErr.Error()))
}

func (progErr ProgressError) NiceError(w io.Writer) error {
fmt.Fprintf(w, aec.RedF.Apply("%s")+"\n", progErr.Error())
func (progErr ProgressError) NiceError(w io.Writer, outer error) error {
fmt.Fprintln(w, aec.RedF.Apply(outer.Error()))
fmt.Fprintln(w)

progErr.prog.Summarize(textio.NewPrefixWriter(w, " "))
Expand Down
19 changes: 7 additions & 12 deletions project.bass
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
; location to track dependency resolution
(def *memos*
*dir*/bass/bass.lock)
; file for memoized dependency resolution
(def *memos* *dir*/bass/bass.lock)

; load dependencies
(use (.git (linux/alpine/git))
(*dir*/bass/bass.bass)
(*dir*/bass/util.bass)
(git:github/vito/bass-loop/ref/main/bass/github.bass))

; standard suite of validations for the repo
(defn checks [src]
{:build-linux (ls (bass:build src "dev" "linux" "amd64"))
:build-darwin (ls (bass:build src "dev" "darwin" "amd64")
{:build-linux (util:ls (bass:build src "dev" "linux" "amd64"))
:build-darwin (util:ls (bass:build src "dev" "darwin" "amd64")
(bass:build src "dev" "darwin" "arm64"))
:build-windows (ls (bass:build src "dev" "windows" "amd64"))
:docs (ls (bass:docs src))
:build-windows (util:ls (bass:build src "dev" "windows" "amd64"))
:docs (util:ls (bass:docs src))
:test (bass:tests src ["./..."])
:nix (bass:nix-checks src)})

; just for eyeballing the files
(defn ls paths
(from (linux/alpine)
($ ls -al & $paths)))

; called by bass-loop
(defn github-hook [event client]
(github:check-hook event client git checks))

0 comments on commit 5007d7e

Please sign in to comment.