Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

better error handling #188

Merged
merged 5 commits into from
Jun 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))