Skip to content
quarry Artifact [881d95b3cf]
Login

Artifact [881d95b3cf]

Artifact 881d95b3cf7c613de8465d95c7661ff1ccff413bdf046789c627b886c8210bae:


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)
	}
}