package runner
import (
"context"
"errors"
"io"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
)
// flakyCheckout fails its first n calls, the way a clone fails while a
// deploy restarts the forge, then checks out for real.
type flakyCheckout struct {
mu sync.Mutex
fails int
calls int
inner fakeCheckout
}
func (f *flakyCheckout) Checkout(ctx context.Context, dir, checkin string) error {
f.mu.Lock()
f.calls++
failing := f.calls <= f.fails
f.mu.Unlock()
if failing {
return errors.New("connection refused")
}
return f.inner.Checkout(ctx, dir, checkin)
}
// THE CHECKOUT IS RETRIED IN PLACE; THE COMMAND NEVER IS.
//
// The pipeline's steps run once (a red test is a verdict, not
// weather), so the one phase that meets infrastructure trouble, getting
// the tree, retries inside the task. A command that fails is reported
// on its first run.
func TestTheCheckoutRetriesAndTheCommandRunsOnce(t *testing.T) {
ran := filepath.Join(t.TempDir(), "ran")
manifest := func(exit string) string {
return `{"tasks":{"t":["sh","-c","echo x >> ` + ran + `; exit ` + exit + `"]}}`
}
runs := func() int {
b, _ := os.ReadFile(ran)
return strings.Count(string(b), "x")
}
newRunner := func(co Checkout) *Runner {
r, err := New(Config{
Bridge: "http://127.0.0.1:1", Token: "WT", Tasks: []string{"t"},
WorkDir: t.TempDir(), Checkout: co, Logf: t.Logf,
CheckoutRetry: []time.Duration{time.Millisecond, time.Millisecond, time.Millisecond},
})
if err != nil {
t.Fatal(err)
}
return r
}
ctx := context.Background()
co := &flakyCheckout{fails: 2, inner: fakeCheckout{map[string]string{"c1": manifest("0")}}}
if err := newRunner(co).execute(ctx, "t", "c1", io.Discard, io.Discard); err != nil {
t.Errorf("FAIL: a checkout that failed twice then worked failed the task: %v", err)
}
if co.calls != 3 || runs() != 1 {
t.Errorf("FAIL: checkout calls = %d, command runs = %d; want 3 and 1", co.calls, runs())
}
co = &flakyCheckout{fails: 99, inner: fakeCheckout{map[string]string{"c1": manifest("0")}}}
err := newRunner(co).execute(ctx, "t", "c1", io.Discard, io.Discard)
if err == nil || !strings.Contains(err.Error(), "checkout") || co.calls != 4 {
t.Errorf("FAIL: a checkout that never works: err %v after %d calls; want a checkout error after 4", err, co.calls)
}
co = &flakyCheckout{inner: fakeCheckout{map[string]string{"c2": manifest("3")}}}
if err := newRunner(co).execute(ctx, "t", "c2", io.Discard, io.Discard); err == nil {
t.Error("FAIL: a failing command passed")
}
if co.calls != 1 || runs() != 2 {
t.Errorf("FAIL: a failing command: checkout calls = %d, total command runs = %d; want 1 and 2", co.calls, runs())
}
t.Log("ok: the checkout retries in place; a failing command runs once")
}