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

Fix dependency error propagation by replaying the panic #373

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 30 additions & 1 deletion mage/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1749,7 +1749,36 @@ func TestWrongDependency(t *testing.T) {
}
}

// / This code liberally borrowed from https://github.com/rsc/goversion/blob/master/version/exe.go
// See https://github.com/magefile/mage/issues/371
func TestRacyDependency(t *testing.T) {
var stdout, stderr bytes.Buffer
inv := Invocation{
Dir: "./testdata/racy_deps",
Args: []string{"test4"},
Stderr: &stderr,
Stdout: &stdout,
}
code := Invoke(inv)
if code != 1 {
t.Fatalf("expected 1, but got %v", code)
}
expectedStderr := `Error: error test1
error test1
error test1
`
actualStderr := stderr.String()
if actualStderr != expectedStderr {
t.Fatalf("expected %q, but got %q", expectedStderr, actualStderr)
}
expectedStdout := `execute test1
`
actualStdout := stdout.String()
if actualStdout != expectedStdout {
t.Fatalf("expected %q, but got %q", expectedStdout, actualStdout)
}
}

// This code liberally borrowed from https://github.com/rsc/goversion/blob/master/version/exe.go

type (
exeType int
Expand Down
35 changes: 35 additions & 0 deletions mage/testdata/racy_deps/magefile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//go:build mage
// +build mage

package main

import (
"fmt"
"time"

"github.com/magefile/mage/mg"
)

func Test1() error {
fmt.Println("execute test1")
return fmt.Errorf("error test1")
}

func Test2() error {
mg.Deps(Test1)
fmt.Println("execute test2")
return nil
}

func Test3() error {
time.Sleep(time.Second)
mg.Deps(Test2)
fmt.Println("execute test3")
return nil
}

func Test4() error {
mg.Deps(Test1, Test2, Test3)
fmt.Println("execute test4")
return nil
}
56 changes: 37 additions & 19 deletions mg/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ func (o *onceMap) LoadOrStore(f Fn) *onceFun {
return existing
}
one := &onceFun{
once: &sync.Once{},
fn: f,
displayName: displayName(f.Name()),
}
Expand Down Expand Up @@ -91,37 +90,44 @@ func CtxDeps(ctx context.Context, fns ...interface{}) {

// runDeps assumes you've already called checkFns.
func runDeps(ctx context.Context, fns []Fn) {
mu := &sync.Mutex{}
var errs []string
var exit int
wg := &sync.WaitGroup{}
resultCh := make(chan error, len(fns))
var wg sync.WaitGroup
wg.Add(len(fns))
for _, f := range fns {
fn := onces.LoadOrStore(f)
wg.Add(1)
go func() {
defer func() {
if v := recover(); v != nil {
mu.Lock()
if err, ok := v.(error); ok {
exit = changeExit(exit, ExitStatus(err))
} else {
exit = changeExit(exit, 1)
var err error
var ok bool
if err, ok = v.(error); !ok {
err = Fatal(1)
}
select {
case <-ctx.Done():
case resultCh <- err:
}
errs = append(errs, fmt.Sprint(v))
mu.Unlock()
}
wg.Done()
}()
if err := fn.run(ctx); err != nil {
mu.Lock()
errs = append(errs, fmt.Sprint(err))
exit = changeExit(exit, ExitStatus(err))
mu.Unlock()
err := fn.run(ctx)
select {
case <-ctx.Done():
case resultCh <- err:
}
}()
}

wg.Wait()
close(resultCh)
var errs []string
var exit int
for err := range resultCh {
if err != nil {
errs = append(errs, err.Error())
exit = changeExit(exit, ExitStatus(err))
}
}
if len(errs) > 0 {
panic(Fatal(exit, strings.Join(errs, "\n")))
}
Expand Down Expand Up @@ -191,9 +197,12 @@ func displayName(name string) string {
}

type onceFun struct {
once *sync.Once
once sync.Once
fn Fn
err error
// depPanicked references the panic a dependency had previously
// failed with if set
depPanicked interface{}

displayName string
}
Expand All @@ -202,10 +211,19 @@ type onceFun struct {
// the same error output.
func (o *onceFun) run(ctx context.Context) error {
o.once.Do(func() {
defer func() {
if v := recover(); v != nil {
o.depPanicked = v
panic(v)
}
}()
if Verbose() {
logger.Println("Running dependency:", displayName(o.fn.Name()))
}
o.err = o.fn.Run(ctx)
})
if o.depPanicked != nil {
panic(o.depPanicked)
}
return o.err
}