117 lines
4.6 KiB
Go
117 lines
4.6 KiB
Go
package updater
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log/slog"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/shcizo/package-updater/internal/protocol"
|
|
)
|
|
|
|
func TestResumeSelfUpdates(t *testing.T) {
|
|
const pinned = "gitea.shcizo.se/shcizo/package-updater:1.4@sha256:aa"
|
|
started := time.Date(2026, 10, 13, 9, 0, 0, 0, time.UTC)
|
|
self := fakeSelf{id: "selfid", labels: map[string]string{labelComposeProject: "updater", labelComposeService: "agent"}}
|
|
|
|
helper := func(id, project, service string, extra map[string]string) container.Summary {
|
|
labels := map[string]string{
|
|
labelSelfUpdateJob: "j-" + id,
|
|
labelSelfUpdateRef: pinned,
|
|
labelSelfUpdateStarted: started.Format(time.RFC3339Nano),
|
|
labelSelfUpdateProject: project,
|
|
labelSelfUpdateService: service,
|
|
labelSelfUpdateOutput: "resolved " + pinned + "\n$ docker compose pull agent\n",
|
|
}
|
|
for k, v := range extra {
|
|
if v == "" {
|
|
delete(labels, k)
|
|
} else {
|
|
labels[k] = v
|
|
}
|
|
}
|
|
return container.Summary{ID: id, Labels: labels}
|
|
}
|
|
separator := "--- agent restarted; output of the update helper ---\n"
|
|
|
|
cases := []struct {
|
|
name string
|
|
helper container.Summary
|
|
exit int64
|
|
state container.ContainerState
|
|
noStarted bool // the old agent's start time is lost with the label
|
|
wantStatus string
|
|
wantErr string
|
|
wantOutput string
|
|
}{
|
|
{name: "helper succeeded", helper: helper("h1", "updater", "agent", nil), wantStatus: protocol.ResultSucceeded,
|
|
wantOutput: "resolved " + pinned + "\n$ docker compose pull agent\n" + separator + "Recreated\n"},
|
|
{name: "helper failed", helper: helper("h1", "updater", "agent", nil), exit: 1, wantStatus: protocol.ResultFailed,
|
|
wantErr: "docker compose up: exit status 1 (output: Recreated)"},
|
|
{name: "no ref label", helper: helper("h1", "updater", "agent", map[string]string{labelSelfUpdateRef: ""}),
|
|
wantStatus: protocol.ResultFailed, wantErr: "update helper has no ref label"},
|
|
{name: "no started label", helper: helper("h1", "updater", "agent", map[string]string{labelSelfUpdateStarted: ""}),
|
|
noStarted: true,
|
|
wantStatus: protocol.ResultFailed, wantErr: `update helper has no usable started label ""`},
|
|
{name: "helper never started", helper: helper("h1", "updater", "agent", nil), state: container.StateCreated,
|
|
wantStatus: protocol.ResultFailed, wantErr: "update helper was never started"},
|
|
{name: "no output label", helper: helper("h1", "updater", "agent", map[string]string{labelSelfUpdateOutput: ""}),
|
|
wantStatus: protocol.ResultSucceeded, wantOutput: separator + "Recreated\n"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
h := &fakeHelper{exit: c.exit, state: c.state, logs: "Recreated\n"}
|
|
unrelated := []container.Summary{
|
|
helper("other-project", "other", "agent", nil),
|
|
helper("other-service", "updater", "web", nil),
|
|
}
|
|
cli := fakeHelperCompose{fakeCompose{self, append(unrelated, c.helper)}, h}
|
|
r := NewRunner(execFunc(func(context.Context, Job, io.Writer) (string, error) {
|
|
t.Error("a resumed job must not reach the executor")
|
|
return "", nil
|
|
}), time.Minute)
|
|
|
|
require.NoError(t, ResumeSelfUpdates(context.Background(), cli, "me", r, slog.New(slog.NewTextHandler(io.Discard, nil))))
|
|
assert.Equal(t, []string{"j-h1"}, r.Known(), "known before the runner starts, so the first hello lists it")
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() { defer wg.Done(); r.Run(ctx) }()
|
|
t.Cleanup(func() { cancel(); wg.Wait() })
|
|
|
|
got := waitPending(t, r, 1)
|
|
require.Len(t, got, 1)
|
|
assert.Equal(t, "j-h1", got[0].ID)
|
|
res := got[0].Result
|
|
assert.Equal(t, c.wantStatus, res.Status)
|
|
if c.state != container.StateCreated {
|
|
assert.Equal(t, container.WaitConditionNotRunning, h.waitCond)
|
|
}
|
|
assert.Equal(t, []string{"h1"}, h.removed, "only the matching helper is touched")
|
|
if c.wantStatus == protocol.ResultSucceeded {
|
|
assert.Equal(t, pinned, res.Ref)
|
|
assert.Equal(t, c.wantOutput, res.Output)
|
|
} else {
|
|
assert.Equal(t, c.wantErr, res.Error)
|
|
}
|
|
if !c.noStarted {
|
|
assert.True(t, started.Equal(res.StartedAt), "the old agent's start time is kept")
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("an agent outside Compose resumes nothing", func(t *testing.T) {
|
|
cli := fakeHelperCompose{fakeCompose{fakeSelf{id: "selfid"}, []container.Summary{helper("h1", "updater", "agent", nil)}}, &fakeHelper{}}
|
|
r := NewRunner(nil, time.Minute)
|
|
require.NoError(t, ResumeSelfUpdates(context.Background(), cli, "me", r, slog.New(slog.NewTextHandler(io.Discard, nil))))
|
|
assert.Empty(t, r.Known())
|
|
})
|
|
}
|