diff --git a/internal/queue/queue.go b/internal/queue/queue.go index 9072016..6baaac1 100644 --- a/internal/queue/queue.go +++ b/internal/queue/queue.go @@ -317,6 +317,9 @@ func (q *Queue) loop() { start := time.Now() err := q.speak(ctx, item) dur := time.Since(start) + // Abbruch VOR dem eigenen cancel() ablesen — danach wäre ctx.Err() + // immer gesetzt und jede fertige Ansage hieße „abgebrochen". + cancelled := ctx.Err() != nil cancel() q.mu.Lock() @@ -325,7 +328,7 @@ func (q *Queue) loop() { q.mu.Unlock() switch { - case ctx.Err() != nil: + case cancelled: q.logf("queue: ansage %s abgebrochen", item.ID) case err != nil: q.logf("queue: ansage %s fehlgeschlagen nach %s: %v", item.ID, dur.Round(time.Millisecond), err) diff --git a/internal/queue/queue_test.go b/internal/queue/queue_test.go index 808a39c..0937c21 100644 --- a/internal/queue/queue_test.go +++ b/internal/queue/queue_test.go @@ -5,6 +5,8 @@ import ( "os" "path/filepath" "slices" + "strings" + "sync" "testing" "time" ) @@ -552,3 +554,57 @@ func TestInactiveQuietWindow(t *testing.T) { } } } + +// syncLog ist ein nebenläufigkeitssicherer Log-Puffer für Worker-Zeilen. +type syncLog struct { + mu sync.Mutex + b strings.Builder +} + +func (l *syncLog) Write(p []byte) (int, error) { + l.mu.Lock() + defer l.mu.Unlock() + return l.b.Write(p) +} + +func (l *syncLog) String() string { + l.mu.Lock() + defer l.mu.Unlock() + return l.b.String() +} + +// TestProtokollFertigNichtAbgebrochen: Eine normal beendete Ansage wird als +// „fertig" protokolliert, nicht als „abgebrochen" — der Worker darf seinen +// eigenen cancel() nicht als Abbruch deuten. Ein echter Cancel heißt +// dagegen „abgebrochen". +func TestProtokollFertigNichtAbgebrochen(t *testing.T) { + logb := &syncLog{} + g := neueSpielerin() + q := New(Laws{}, g.speak, logb) + defer q.Close() + + q.Enqueue(Item{Text: "normal zu ende"}) + g.warteBis(t, 1) + g.step <- struct{}{} + waitFor(t, func() bool { return strings.Contains(logb.String(), "fertig nach") }) + if strings.Contains(logb.String(), "abgebrochen") { + t.Errorf("fertige Ansage als abgebrochen protokolliert:\n%s", logb.String()) + } + + q.Enqueue(Item{Text: "wird abgebrochen"}) + g.warteBis(t, 2) + q.Cancel() + waitFor(t, func() bool { return strings.Contains(logb.String(), "abgebrochen") }) +} + +// waitFor pollt cond bis zu zwei Sekunden. +func waitFor(t *testing.T, cond func() bool) { + t.Helper() + deadline := time.Now().Add(2 * time.Second) + for !cond() { + if time.Now().After(deadline) { + t.Fatal("Bedingung nicht binnen 2 s erfüllt") + } + time.Sleep(5 * time.Millisecond) + } +}