textmachine/platform/internal/books/walk_test.go

154 lines
7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package books
import (
"context"
"testing"
"time"
)
// The tail of an upload cannot outlive UploadSettle, whatever it does on the way there.
//
// The PROPERTY and not a sum of the steps, and that is the whole of the form. The sum was derived by
// hand three times and was short all three, each time for a different reason (register row PD-464); a
// test that re-listed the steps would be the fourth hand-derivation and would hold for exactly as
// long as somebody remembered to update it. What is asserted here survives steps nobody has written
// yet, which is why the fourth case takes fifty of them.
func TestNoStepOfAnUploadsTailOutlivesTheWalk(t *testing.T) {
// THREE different numbers on purpose: in a fixture where the walk, a step's own budget and what a
// step asks for coincide, "capped by the walk" and "given what it asked for" are the same
// observation, and the whole class of differences between them goes invisible (D39.208 §5).
const walkBudget, writeShort, asksForever = 400 * time.Millisecond, 90 * time.Millisecond, time.Hour
s := &Service{writeBudget: writeShort, uploadSettle: walkBudget}
walk, cancel := s.walk(context.Background())
defer cancel()
end, ok := walk.Deadline()
if !ok {
t.Fatal("the walk carries no deadline, so nothing taken under it is bounded by anything")
}
// A step that asks for more than the walk has left gets the walk's end and not what it asked for.
long, cancelLong := s.step(walk, asksForever)
defer cancelLong()
if d, _ := long.Deadline(); !d.Equal(end) {
t.Errorf("a step that asked for %s ends at %s, want the walk's own end %s: the walk is not capping it",
asksForever, d.Format(time.StampMilli), end.Format(time.StampMilli))
}
// A step that asks for less keeps its own budget: the cap is a ceiling, not a replacement.
short, cancelShort := s.step(walk, writeShort)
defer cancelShort()
if d, _ := short.Deadline(); !d.Before(end) {
t.Errorf("a step that asked for %s ends at %s, at or past the walk's %s: it was given the ceiling instead of its budget",
writeShort, d.Format(time.StampMilli), end.Format(time.StampMilli))
}
// FIFTY steps, each asking for an hour, each taken under the one before it. This is the property
// the form exists for: the number of steps in the tail is not a term of where the tail ends.
under := walk
for i := range 50 {
next, cancelNext := s.step(under, asksForever)
defer cancelNext()
if d, _ := next.Deadline(); d.After(end) {
t.Fatalf("step %d of the tail ends at %s, past the walk's %s: adding a step moves the boundary",
i, d.Format(time.StampMilli), end.Format(time.StampMilli))
}
under = next
}
// A terminal write taken after the step before it is SPENT is still usable — that is the whole
// reason a step detaches at all: the engine call may legitimately consume everything, and the
// record of what happened must still be written. It is capped by the walk all the same.
spent, cancelSpent := s.step(walk, time.Nanosecond)
defer cancelSpent()
<-spent.Done()
after, cancelAfter := s.step(spent, writeShort)
defer cancelAfter()
if err := after.Err(); err != nil {
t.Errorf("a write taken after an exhausted step was born cancelled (%v): what happened would go unrecorded", err)
}
if d, _ := after.Deadline(); d.After(end) {
t.Errorf("a write taken after an exhausted step ends at %s, past the walk's %s",
d.Format(time.StampMilli), end.Format(time.StampMilli))
}
}
// Outside a walk — the queue's worker and the backstop sweep — a step is the detached, self-bounded
// write it has always been: their budgets are their own, and there is no upload waiting on them.
func TestAStepOutsideAWalkKeepsItsOwnBudgetAndSurvivesItsCaller(t *testing.T) {
const writeShort = 90 * time.Millisecond
s := &Service{writeBudget: writeShort}
parent, cancelParent := context.WithCancel(context.Background())
free, cancelFree := s.step(parent, writeShort)
defer cancelFree()
cancelParent()
if err := free.Err(); err != nil {
t.Errorf("a write outside a walk died with its caller (%v): a job whose deadline is spent would lose its record", err)
}
d, ok := free.Deadline()
if !ok {
t.Fatal("a write outside a walk got no deadline: detached is not the same as unlimited")
}
if left := time.Until(d); left > writeShort {
t.Errorf("a write outside a walk got %s, want no more than its own %s", left, writeShort)
}
}
// UploadSettle covers the tail END TO END, and part of it is spent after this package is done: the
// idempotency receipt is written once Accept has returned. So the walk takes the settle budget MINUS
// the receipt's share — otherwise the tail ends exactly one receipt past the number the boot compared
// against the windows an upload has to finish inside, which is the shape of every earlier miss.
func TestTheWalkLeavesTheReceiptItsShareOfTheSettleBudget(t *testing.T) {
s := &Service{}
before := time.Now()
walk, cancel := s.walk(context.Background())
defer cancel()
end, ok := walk.Deadline()
if !ok {
t.Fatal("the walk carries no deadline")
}
// `before` is read a hair earlier than the walk stamps its deadline, so the reading is the budget
// plus scheduling slack and never less than it.
got, want := end.Sub(before), UploadSettle-ReceiptBudget
if got < want || got > want+time.Second {
t.Errorf("the walk runs for about %s, want UploadSettle (%s) less the receipt's share (%s) = %s",
got, UploadSettle, ReceiptBudget, want)
}
}
// The cut an upload waits for runs on the WALK's deadline, not on a budget of its own.
//
// Asserted at the real call site and by the deadline the engine is HANDED, rather than by a stopwatch
// around the request: the question is which budget bounds the cut, and a clock answers it only on a
// machine that happened to be slow enough. A cut detached from the walk — the shape this replaced —
// hands the engine CutBudget here, three orders of magnitude past the walk it is inside.
func TestTheCutOfAnUploadIsBoundedByTheWalkAndNotByItsOwnBudget(t *testing.T) {
f := newFixture(t)
templated(t, f)
const tail, write = 700 * time.Millisecond, 20 * time.Millisecond
f.svc.uploadSettle = tail
// Shortened with the walk, because the reserve a cut leaves for the writes after it is three of
// these: against the real thirty seconds a 700 ms walk holds no cut at all, and the fixture would
// then be asserting about a cut that never ran (stepLeaving).
f.svc.writeBudget = write
var granted time.Duration
var bounded bool
f.engine.onManifest = func(ctx context.Context) {
d, ok := ctx.Deadline()
bounded = ok
granted = time.Until(d)
}
f.accept(t, "蛊真人.txt", "первая глава\fвторая глава")
if f.engine.called() != 1 {
t.Fatalf("the engine was asked %d times, so there is no granted deadline to judge", f.engine.called())
}
if !bounded {
t.Fatal("the cut ran with no deadline at all")
}
if granted <= 0 || granted > tail {
t.Errorf("the cut was granted %s inside a %s walk, want a slice of the walk", granted, tail)
}
if granted >= CutBudget {
t.Errorf("the cut was granted %s, its own CutBudget (%s): it is detached from the walk it runs inside",
granted, CutBudget)
}
}