Skip to content
quarry Artifact [bdd0c185f6]
Login

Artifact [bdd0c185f6]

Artifact bdd0c185f6f233b405a9bf5c419fad8540202590f5aeb411ad2a04444dd64c2d:


package runner

import (
	"context"
	"encoding/json"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strconv"
	"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(), tempRootName)
			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 ITS OWN TASK DIRS FROM A PREVIOUS LIFE, AND NOTHING ELSE.
//
// Not a neighbour of the root, not a file in it that is no task's, and
// not the live dir of another runner sharing the work dir.
func TestARunnerStartSweepsItsOwnTaskDirsOnly(t *testing.T) {
	base := t.TempDir()
	root := filepath.Join(base, tempRootName)
	mk := func(p string) string {
		if err := os.MkdirAll(filepath.Join(p, "go-build9"), 0o755); err != nil {
			t.Fatal(err)
		}
		os.WriteFile(filepath.Join(p, "go-build9", "f"), []byte("x"), 0o644)
		return p
	}
	deadPid := 1 << 22 // above any pid_max a test host sets
	stale := mk(filepath.Join(root, taskPrefix+strconv.Itoa(deadPid)+"-1"))
	ownOld := mk(filepath.Join(root, taskPrefix+strconv.Itoa(os.Getpid())+"-2"))
	other := mk(filepath.Join(root, "go-build-not-a-task"))
	otherFile := filepath.Join(root, "notes.txt")
	os.WriteFile(otherFile, []byte("x"), 0o644)
	neighbour := mk(filepath.Join(base, "go-build-neighbour"))
	// A live other runner: its parent shell's pid is alive and not ours.
	live := mk(filepath.Join(root, taskPrefix+strconv.Itoa(os.Getppid())+"-3"))

	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
	for _, p := range []string{stale, ownOld} {
		if _, err := os.Stat(p); !os.IsNotExist(err) {
			t.Errorf("FAIL: %s survived the start-up sweep", p)
		}
	}
	for _, p := range []string{other, otherFile, neighbour, live} {
		if _, err := os.Stat(p); err != nil {
			t.Errorf("FAIL: the sweep touched %s: %v", p, err)
		}
	}
}

// A TEMP ROOT THAT IS A SYMLINK IS REFUSED, UNTOUCHED.
func TestARunnerRefusesASymlinkedTempRoot(t *testing.T) {
	base := t.TempDir()
	target := filepath.Join(base, "elsewhere")
	if err := os.MkdirAll(target, 0o755); err != nil {
		t.Fatal(err)
	}
	victim := filepath.Join(target, taskPrefix+"1-1")
	os.MkdirAll(victim, 0o755)
	root := filepath.Join(base, tempRootName)
	if err := os.Symlink(target, root); err != nil {
		t.Skip(err)
	}
	r, err := New(Config{
		Bridge: "http://127.0.0.1:1", 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)
	}
	if err := r.Run(context.Background()); err == nil || err == context.Canceled {
		t.Errorf("FAIL: Run = %v, want a refusal of the symlinked root", err)
	}
	if _, err := os.Stat(victim); err != nil {
		t.Errorf("FAIL: a dir behind the symlink was touched: %v", err)
	}
	if info, _ := os.Stat(target); info.Mode().Perm() != 0o755 {
		t.Errorf("FAIL: the symlink's target was chmodded to %v", info.Mode().Perm())
	}
}