A queued ask could only be waited out and a running build only ended by stopping the machine, which redelivered it elsewhere. The holder now serves current, kill, pause and resume on its own machine's subjects; a kill ends the build's process group and labelled containers and settles the ask as failed, killed by hand; pause is kept in the workspace across a restart and said on the bus. The controller writes cancelled ids to a cancelled set the holder reads on taking an ask, closing the race a delete alone leaves.
95 lines
3.2 KiB
Go
95 lines
3.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// A holder paused stays paused across its restart: the flag is kept in its workspace (novox/hq ADR
|
|
// 0219), and what it says about itself follows.
|
|
func TestAPausedHolderStaysPausedAcrossARestart(t *testing.T) {
|
|
workspace := t.TempDir()
|
|
var said []link.HolderState
|
|
h := newHolder("ace", link.TheBuildMachine, workspace, func(s link.HolderState) error {
|
|
said = append(said, s)
|
|
return nil
|
|
})
|
|
if h.Paused() {
|
|
t.Fatal("a new holder starts paused")
|
|
}
|
|
if _, err := h.handlers()["pause"](context.Background(), json.RawMessage(`{}`)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !h.Paused() || len(said) != 1 || !said[0].Paused || said[0].On != "ace" {
|
|
t.Fatalf("paused: %v, said %+v", h.Paused(), said)
|
|
}
|
|
again := newHolder("ace", link.TheBuildMachine, workspace, nil)
|
|
if !again.Paused() {
|
|
t.Fatal("restarted, the holder forgot it was paused")
|
|
}
|
|
if _, err := again.handlers()["resume"](context.Background(), json.RawMessage(`{}`)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if newHolder("ace", link.TheBuildMachine, workspace, nil).Paused() {
|
|
t.Fatal("resumed, the holder came back paused")
|
|
}
|
|
}
|
|
|
|
// kill ends the build with that id — its context, which ends its commands — removes what it left by
|
|
// label, and refuses an id it is not building.
|
|
func TestKillEndsTheBuildRunningHereAndNoOther(t *testing.T) {
|
|
h := newHolder("ace", link.TheBuildMachine, t.TempDir(), nil)
|
|
var removed []string
|
|
h.remove = func(_ context.Context, _ string, name string, args ...string) (string, error) {
|
|
removed = append(removed, name+" "+strings.Join(args, " "))
|
|
if args[0] == "ps" {
|
|
return "c1\n", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
kill := h.handlers()["kill"]
|
|
if _, err := kill(context.Background(), json.RawMessage(`{"id":"build-1"}`)); err == nil {
|
|
t.Fatal("killed a build while none ran")
|
|
}
|
|
|
|
building, cancel := context.WithCancel(context.Background())
|
|
r := h.begin(link.BuildRequest{ID: "build-1", Repository: "novox/a"}, cancel)
|
|
h.stepped(r, "image")
|
|
if c := h.current(); c.Running == nil || c.Running.ID != "build-1" || c.Running.Step != "image" {
|
|
t.Fatalf("current says %+v", c)
|
|
}
|
|
if _, err := kill(context.Background(), json.RawMessage(`{"id":"build-2"}`)); err == nil ||
|
|
!strings.Contains(err.Error(), "build-1") {
|
|
t.Fatalf("killed another id: %v", err)
|
|
}
|
|
// The build's own goroutine: it ends when its context does, as a build's commands do.
|
|
go func() {
|
|
<-building.Done()
|
|
h.end(r)
|
|
}()
|
|
answer, err := kill(context.Background(), json.RawMessage(`{"id":"build-1"}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if building.Err() == nil {
|
|
t.Fatal("the build's context was not cancelled")
|
|
}
|
|
if !h.wasKilled(r) {
|
|
t.Error("the build is not marked killed, so it would be announced as an ordinary failure")
|
|
}
|
|
said := answer.(map[string]any)["said"].(string)
|
|
if !strings.Contains(said, "1 container(s)") || !strings.Contains(said, link.KilledByHand) {
|
|
t.Errorf("kill said %q", said)
|
|
}
|
|
if len(removed) != 2 || !strings.Contains(removed[0], "label=mesh.build=build-1") {
|
|
t.Errorf("removed %v", removed)
|
|
}
|
|
if c := h.current(); c.Running != nil {
|
|
t.Errorf("after the kill current says %+v", c)
|
|
}
|
|
}
|