package runner
import (
"context"
"encoding/json"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
)
// A TASK WHOSE RUN IS CANCELLED STOPS.
//
// jva measured a closed landing's merge job running its full four
// minutes after the run was cancelled, holding the runner. dagnats#737
// answers 409 to a heartbeat of such a task; the runner then kills the
// job, its children included, and reports nothing.
func TestATaskWhoseRunIsCancelledStops(t *testing.T) {
job, _ := json.Marshal(Job{Steps: []JobStep{{Run: "sleep 30; echo ran-to-$((40+2))"}}})
tk := task("doomed", "c1")
tk.Metadata = map[string]string{JobKey: string(job)}
br := &fakeBridge{queue: map[string][]polled{"t-job": {tk}}, resolved: map[string]map[string]any{}, logs: map[string]string{},
cancelled: map[string]bool{}}
srv := httptest.NewServer(br.handler())
defer srv.Close()
var logged []string
var logMu sync.Mutex
r, err := New(Config{
Bridge: srv.URL, Token: "WT", Tasks: []string{"t-job"}, MaxTasks: 1,
WorkDir: t.TempDir(), Checkout: appCheckout{}, Heartbeat: 100 * time.Millisecond,
Logf: func(f string, a ...any) { logMu.Lock(); logged = append(logged, f); logMu.Unlock() },
})
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { r.Run(ctx); close(done) }()
// Let it start, then cancel the run.
time.Sleep(500 * time.Millisecond)
br.mu.Lock()
br.cancelled["doomed"] = true
br.mu.Unlock()
cancelledAt := time.Now()
stopped := false
for deadline := time.Now().Add(15 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
br.mu.Lock()
l := br.logs["doomed"]
br.mu.Unlock()
if strings.Contains(l, "stopped: this task's run was cancelled") {
stopped = true
break
}
}
took := time.Since(cancelledAt)
cancel()
<-done
if !stopped {
t.Fatalf("FAIL: the task did not stop within 15s of its run being cancelled")
}
if took > 8*time.Second {
t.Errorf("FAIL: stopping took %s; want it within a few heartbeats", took)
}
br.mu.Lock()
defer br.mu.Unlock()
if strings.Contains(br.logs["doomed"], "ran-to-42") {
t.Error("FAIL: the job ran to its end")
}
for _, a := range br.actions {
if a == "doomed complete" || a == "doomed fail" {
t.Errorf("FAIL: the runner reported %q for a withdrawn task", a)
}
}
t.Logf("ok: stopped %s after its run was cancelled, reported nothing", took.Round(10*time.Millisecond))
}