diff --git a/internal/progress/tracker_stall_test.go b/internal/progress/tracker_stall_test.go index f31f425..6aec38b 100644 --- a/internal/progress/tracker_stall_test.go +++ b/internal/progress/tracker_stall_test.go @@ -96,3 +96,59 @@ func TestTracker_TheStillRunningLineIsNotHeldUpBehindAHungSave(t *testing.T) { require.Eventually(t, func() bool { return strings.Contains(buf.String(), "still running") }, 3*time.Second, 5*time.Millisecond) } + +// deadlineSink records how long each write was given, and answers at once. +type deadlineSink struct { + mu sync.Mutex + saves []time.Duration + beats []time.Duration + noDLOK bool +} + +func (d *deadlineSink) record(ctx context.Context, into *[]time.Duration) { + d.mu.Lock() + defer d.mu.Unlock() + dl, ok := ctx.Deadline() + if !ok { + d.noDLOK = true + return + } + *into = append(*into, time.Until(dl)) +} + +func (d *deadlineSink) SaveProgress(ctx context.Context, _ []domain.ProgressStep, _ time.Time) error { + d.record(ctx, &d.saves) + return nil +} + +func (d *deadlineSink) Beat(ctx context.Context, _ time.Time) error { + d.record(ctx, &d.beats) + return nil +} + +// The production value is the one the worker gets: it builds a Tracker without options. +// A zero or tiny default would drop every write, a huge one would bound nothing. +func TestTracker_TheDefaultSaveAndBeatDeadlineIsAFewSecondsAway(t *testing.T) { + sink := &deadlineSink{} + tr := progress.NewTracker(context.Background(), sink, slog.New(slog.NewTextHandler(&lockedBuf{}, nil)), + []progress.Planned{{Key: "a", Label: "A"}}) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go tr.Heartbeat(ctx, 5*time.Millisecond, time.Hour) + + tr.Begin("a", 0) + require.Eventually(t, func() bool { + sink.mu.Lock() + defer sink.mu.Unlock() + return len(sink.beats) > 0 + }, 3*time.Second, 5*time.Millisecond) + + sink.mu.Lock() + defer sink.mu.Unlock() + require.False(t, sink.noDLOK, "a write was given no deadline at all") + require.NotEmpty(t, sink.saves) + for _, d := range append(append([]time.Duration{}, sink.saves...), sink.beats...) { + require.Greater(t, d, time.Second, "the deadline is already (nearly) spent when the write starts") + require.LessOrEqual(t, d, 5*time.Second, "the default bound is 5 s") + } +}