package runner
import (
"context"
"encoding/json"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// EACH TASK GETS A TEMP DIRECTORY OF ITS OWN, REMOVED WHEN IT ENDS.
//
// A task killed before `go test` cleaned up left its go-build dirs in
// the host's tmpfs /tmp until the disk was full and NATS refused
// streams (#317). TMPDIR and GOTMPDIR now point into a per-task
// directory under the runner's own root, and that directory goes with
// the task however it ends.
func TestATasksTempDirIsRemovedWhenItEnds(t *testing.T) {
cases := []struct {
name string
run string
cancel bool
}{
{"success", "echo TEMPIS:$TMPDIR:GOTEMPIS:$GOTMPDIR:END; mkdir -p $GOTMPDIR/go-build1; echo x > $GOTMPDIR/go-build1/f", false},
{"failure", "echo TEMPIS:$TMPDIR:GOTEMPIS:$GOTMPDIR:END; echo x > $TMPDIR/f; exit 3", false},
{"cancel", "echo TEMPIS:$TMPDIR:GOTEMPIS:$GOTMPDIR:END; echo x > $TMPDIR/f; sleep 30", true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
job, _ := json.Marshal(Job{Steps: []JobStep{{Run: c.run}}})
tk := task("t1", "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()
root := filepath.Join(t.TempDir(), "tmp")
r, err := New(Config{
Bridge: srv.URL, Token: "WT", Tasks: []string{"t-job"}, MaxTasks: 1,
WorkDir: t.TempDir(), TempRoot: root, Checkout: appCheckout{}, Heartbeat: 100 * time.Millisecond,
Logf: func(string, ...any) {},
})
if err != nil {
t.Fatal(err)
}
ctx, cancelRun := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { r.Run(ctx); close(done) }()
defer func() { cancelRun(); <-done }()
var tmp string
deadline := time.Now().Add(15 * time.Second)
for ; time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
br.mu.Lock()
l := br.logs["t1"]
br.mu.Unlock()
if _, rest, ok := strings.Cut(l, "\nTEMPIS:"); ok && strings.Contains(rest, ":END") {
tmp, _, _ = strings.Cut(rest, ":")
if c.cancel {
br.mu.Lock()
br.cancelled["t1"] = true
br.mu.Unlock()
}
break
}
}
if tmp == "" {
t.Fatal("FAIL: the job never reported its TMPDIR")
}
if filepath.Dir(tmp) != root {
t.Errorf("FAIL: TMPDIR %q is not directly inside the runner's root %q", tmp, root)
}
br.mu.Lock()
l := br.logs["t1"]
br.mu.Unlock()
if !strings.Contains(l, "GOTEMPIS:"+tmp+":END") {
t.Errorf("FAIL: GOTMPDIR is not TMPDIR %q in %q", tmp, l)
}
gone := false
for ; time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if _, err := os.Stat(tmp); os.IsNotExist(err) {
gone = true
break
}
}
if !gone {
t.Errorf("FAIL: %s still exists after the task ended", tmp)
}
if es, _ := os.ReadDir(root); len(es) != 0 {
t.Errorf("FAIL: the root holds %d entries after the task ended", len(es))
}
})
}
}
// A RUNNER CLEARS WHAT A PREVIOUS RUN OF ITS OWN LEFT, AND NOTHING ELSE.
func TestARunnerStartSweepsItsOwnTempRootOnly(t *testing.T) {
base := t.TempDir()
root := filepath.Join(base, "tmp")
stale := filepath.Join(root, "task-old")
if err := os.MkdirAll(filepath.Join(stale, "go-build9"), 0o755); err != nil {
t.Fatal(err)
}
os.WriteFile(filepath.Join(stale, "go-build9", "f"), []byte("x"), 0o644)
neighbour := filepath.Join(base, "go-build-neighbour")
if err := os.MkdirAll(neighbour, 0o755); err != nil {
t.Fatal(err)
}
srv := httptest.NewServer((&fakeBridge{queue: map[string][]polled{}, resolved: map[string]map[string]any{}, logs: map[string]string{}}).handler())
defer srv.Close()
r, err := New(Config{
Bridge: srv.URL, Token: "WT", Tasks: []string{"t-job"}, MaxTasks: 1,
WorkDir: t.TempDir(), TempRoot: root, Checkout: appCheckout{}, Logf: func(string, ...any) {},
})
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { r.Run(ctx); close(done) }()
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) {
if _, err := os.Stat(stale); os.IsNotExist(err) {
break
}
}
cancel()
<-done
if _, err := os.Stat(stale); !os.IsNotExist(err) {
t.Errorf("FAIL: the stale task dir survived the start-up sweep")
}
if _, err := os.Stat(neighbour); err != nil {
t.Errorf("FAIL: the sweep touched %s outside the root: %v", neighbour, err)
}
}