asked, waiting on the seat every five minutes, stored or refused, as the ask's phase on its step key, so a publish or a copy that stands still is readable on the bus. PublishOne and PublishFiles narrate for every build module; the image copy and the late publish use the same narration. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
108 lines
3.7 KiB
Go
108 lines
3.7 KiB
Go
package buildstep
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
reg "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// CAT-NR1: a build's publish on a seat says asked, waiting on the seat (on every tick of NarrateEvery while the holder
|
|
// has not answered), and stored or refused, each as the ask's phase written to its running step key.
|
|
func TestABuildNarratesItsPublishOnASeat(t *testing.T) {
|
|
digest := "sha256:" + strings.Repeat("c", 64)
|
|
for name, c := range map[string]struct {
|
|
answer error
|
|
ticks int
|
|
want []string
|
|
}{
|
|
"stored": {want: []string{
|
|
"asked go-package-registry: publish of mod 1.2.3 at " + digest,
|
|
"stored on go-package-registry: publish of mod 1.2.3 at " + digest + " (0 of its 1 parts sent, the rest held)"}},
|
|
"refused": {answer: errors.New("the holder is down"), want: []string{
|
|
"asked go-package-registry: publish of mod 1.2.3 at " + digest,
|
|
"refused by go-package-registry: publish of mod 1.2.3 at " + digest + ": go-package-registry's publish of mod: the holder is down"}},
|
|
"waiting twice, then stored": {ticks: 2, want: []string{
|
|
"asked go-package-registry: publish of mod 1.2.3 at " + digest,
|
|
"waiting on go-package-registry: publish of mod 1.2.3 at " + digest + ", 5m0s since asked",
|
|
"waiting on go-package-registry: publish of mod 1.2.3 at " + digest + ", 10m0s since asked",
|
|
"stored on go-package-registry: publish of mod 1.2.3 at " + digest + " (0 of its 1 parts sent, the rest held)"}},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
clock := time.Date(2026, 11, 4, 16, 0, 0, 0, time.UTC)
|
|
var mu sync.Mutex
|
|
oldNow, oldTicks := now, narrateTicks
|
|
now = func() time.Time { mu.Lock(); defer mu.Unlock(); return clock }
|
|
tick := make(chan time.Time)
|
|
stopped := make(chan struct{})
|
|
narrateTicks = func(every time.Duration) (<-chan time.Time, func()) {
|
|
if every != 5*time.Minute {
|
|
t.Errorf("the waiting lines come every %s, want 5m", every)
|
|
}
|
|
return tick, func() { close(stopped) }
|
|
}
|
|
t.Cleanup(func() { now, narrateTicks = oldNow, oldTicks })
|
|
|
|
var said []string
|
|
wrote := make(chan struct{}, 16)
|
|
step := delivery.Step{ID: "a1", Tool: "build", Node: "ace", State: delivery.StepRunning, Started: clock}
|
|
ph := &phases{key: "ace.a1", step: step, put: func(key string, value any) (uint64, error) {
|
|
var s delivery.Step
|
|
_ = json.Unmarshal(value.(json.RawMessage), &s)
|
|
phasesMu.Lock()
|
|
said = append(said, s.Phase)
|
|
phasesMu.Unlock()
|
|
wrote <- struct{}{}
|
|
return 1, nil
|
|
}}
|
|
release := make(chan struct{})
|
|
call := func(key string, body any) (json.RawMessage, error) {
|
|
<-release
|
|
if c.answer != nil {
|
|
return nil, c.answer
|
|
}
|
|
return json.Marshal(reg.PublishAnswer{Held: true, Transfer: "t"})
|
|
}
|
|
ctx := context.WithValue(context.Background(), phaseKey{}, ph)
|
|
done := make(chan error, 1)
|
|
go func() { done <- PublishOne(ctx, call, GoSeat, "mod", "package", "1.2.3", digest, "t", []byte("x")) }()
|
|
line := func() {
|
|
select {
|
|
case <-wrote:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("the publish said no line")
|
|
}
|
|
}
|
|
line() // asked, at the clock's start
|
|
for i := 0; i < c.ticks; i++ {
|
|
mu.Lock()
|
|
clock = clock.Add(5 * time.Minute)
|
|
mu.Unlock()
|
|
tick <- clock
|
|
line() // its waiting line
|
|
}
|
|
close(release)
|
|
err := <-done
|
|
if (err != nil) != (c.answer != nil) {
|
|
t.Fatalf("PublishOne = %v", err)
|
|
}
|
|
<-stopped
|
|
phasesMu.Lock()
|
|
defer phasesMu.Unlock()
|
|
// Each line is the step's phase, written to its key, in order.
|
|
if fmt.Sprint(said) != fmt.Sprint(c.want) {
|
|
t.Errorf("phases =\n%s\nwant\n%s", strings.Join(said, "\n"), strings.Join(c.want, "\n"))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
var phasesMu sync.Mutex
|