package ratelimit import ( "bytes" "context" "errors" "io" "strings" "sync" "testing" "time" ) // fakeClock ersetzt die Zeit in Tests. // // Ein Test, der echte Sekunden verstreichen lässt, wird entweder langsam oder // unzuverlässig. Mit einer gesteuerten Uhr lässt sich das Zeitverhalten genau // prüfen, ohne zu warten. type fakeClock struct { // mutex schützt den Zustand. mutex sync.Mutex // currentTime ist die aktuelle Zeit. currentTime time.Time // totalSlept ist die insgesamt angeforderte Wartezeit. totalSlept time.Duration } // now liefert die aktuelle Zeit der gesteuerten Uhr. func (clock *fakeClock) now() time.Time { clock.mutex.Lock() defer clock.mutex.Unlock() return clock.currentTime } // sleep zählt die Wartezeit und lässt die Uhr vorrücken. func (clock *fakeClock) sleep(waitContext context.Context, waitDuration time.Duration) error { if contextError := waitContext.Err(); contextError != nil { return contextError } clock.mutex.Lock() defer clock.mutex.Unlock() clock.totalSlept += waitDuration clock.currentTime = clock.currentTime.Add(waitDuration) return nil } // newTestLimiter baut einen Begrenzer mit gesteuerter Uhr. func newTestLimiter(testInstance *testing.T, bytesPerSecond int64) (*Limiter, *fakeClock) { testInstance.Helper() builtLimiter, limiterError := NewLimiter(bytesPerSecond) if limiterError != nil { testInstance.Fatalf("der Begrenzer ließ sich nicht bauen: %v", limiterError) } testClock := &fakeClock{currentTime: time.Date(2026, time.June, 1, 0, 0, 0, 0, time.UTC)} builtLimiter.nowFunction = testClock.now builtLimiter.sleepFunction = testClock.sleep builtLimiter.lastRefillTime = testClock.currentTime return builtLimiter, testClock } // TestLimiterEnforcesRateOverTime prüft die Einhaltung der Rate. func TestLimiterEnforcesRateOverTime(testInstance *testing.T) { const bytesPerSecond int64 = 1 << 20 // 1 MiB/s testLimiter, testClock := newTestLimiter(testInstance, bytesPerSecond) // Der Eimer ist anfangs voll: 1 MiB geht ohne Warten durch. if waitError := testLimiter.Wait(context.Background(), bytesPerSecond); waitError != nil { testInstance.Fatalf("die erste Übertragung schlug fehl: %v", waitError) } if testClock.totalSlept != 0 { testInstance.Errorf("die erste Übertragung wartete %s, obwohl der Eimer voll war", testClock.totalSlept) } // Weitere 4 MiB müssen ungefähr vier Sekunden kosten. if waitError := testLimiter.Wait(context.Background(), 4*bytesPerSecond); waitError != nil { testInstance.Fatalf("die zweite Übertragung schlug fehl: %v", waitError) } expectedWait := 4 * time.Second tolerance := 200 * time.Millisecond if testClock.totalSlept < expectedWait-tolerance || testClock.totalSlept > expectedWait+tolerance { testInstance.Errorf("für 4 MiB bei 1 MiB/s wurde %s gewartet, erwartet waren etwa %s", testClock.totalSlept, expectedWait) } } // TestLimiterHandlesRequestLargerThanBurst prüft den Sonderfall. // // Eine Anforderung größer als der Eimer liesse sich nie erfüllen und blockierte // für immer. Sie muss in Teilstücke zerlegt werden. func TestLimiterHandlesRequestLargerThanBurst(testInstance *testing.T) { const bytesPerSecond int64 = 1 << 20 testLimiter, testClock := newTestLimiter(testInstance, bytesPerSecond) // Zehnmal die Eimergröße auf einmal. requestedBytes := 10 * testLimiter.burstBytes waitDone := make(chan error, 1) go func() { waitDone <- testLimiter.Wait(context.Background(), requestedBytes) }() select { case waitError := <-waitDone: if waitError != nil { testInstance.Fatalf("die große Übertragung schlug fehl: %v", waitError) } case <-time.After(5 * time.Second): testInstance.Fatal("eine Anforderung größer als der Eimer blockierte") } // Neun Eimerfüllungen müssen bezahlt werden, eine war vorrätig. expectedWait := 9 * time.Second tolerance := time.Second if testClock.totalSlept < expectedWait-tolerance { testInstance.Errorf("für %d Byte bei 1 MiB/s wurde nur %s gewartet", requestedBytes, testClock.totalSlept) } } // TestLimiterRespectsContextCancellation prüft den Abbruch. // // Ohne ihn hinge ein abgebrochener Sicherungslauf noch minutenlang in einer // Wartezeit, die niemand mehr braucht. func TestLimiterRespectsContextCancellation(testInstance *testing.T) { testLimiter, limiterError := NewLimiter(minimumBurstBytes) if limiterError != nil { testInstance.Fatalf("der Begrenzer ließ sich nicht bauen: %v", limiterError) } // Der Eimer wird geleert, damit der nächste Aufruf tatsächlich wartet. if waitError := testLimiter.Wait(context.Background(), testLimiter.burstBytes); waitError != nil { testInstance.Fatalf("die Vorbereitung schlug fehl: %v", waitError) } cancelledContext, cancelWait := context.WithCancel(context.Background()) cancelWait() startTime := time.Now() waitError := testLimiter.Wait(cancelledContext, testLimiter.burstBytes*4) elapsedDuration := time.Since(startTime) if !errors.Is(waitError, context.Canceled) { testInstance.Fatalf("der Abbruch wurde nicht gemeldet: %v", waitError) } if elapsedDuration > time.Second { testInstance.Errorf("der abgebrochene Aufruf dauerte %s", elapsedDuration) } } // TestUnlimitedLimiterDoesNotWrap prüft den abgeschalteten Begrenzer. // // Eine Hülle, die nichts tut, kostet bei jedem Block einen Aufruf mehr. func TestUnlimitedLimiterDoesNotWrap(testInstance *testing.T) { unlimitedLimiter, limiterError := NewLimiter(Unlimited) if limiterError != nil { testInstance.Fatalf("der Begrenzer ließ sich nicht bauen: %v", limiterError) } if !unlimitedLimiter.IsUnlimited() { testInstance.Error("ein Begrenzer mit Rate 0 meldete sich als begrenzend") } sourceReader := strings.NewReader("Inhalt") wrappedReader := NewLimitedReader(context.Background(), sourceReader, unlimitedLimiter) if wrappedReader != io.Reader(sourceReader) { testInstance.Error("ohne Begrenzung wurde der Datenstrom trotzdem umhüllt") } // Auch ein nicht vorhandener Begrenzer darf nicht stören. var missingLimiter *Limiter if !missingLimiter.IsUnlimited() { testInstance.Error("ein fehlender Begrenzer meldete sich als begrenzend") } } // TestLimitedReaderDeliversAllBytes prüft die Unversehrtheit der Daten. // // Eine Bandbreitengrenze darf den Inhalt nicht antasten — sonst wäre sie die // heimtückischste Art, ein Backup zu beschädigen. func TestLimitedReaderDeliversAllBytes(testInstance *testing.T) { testLimiter, _ := newTestLimiter(testInstance, 1<<20) sourceData := bytes.Repeat([]byte("Syncova"), 50_000) limitedReader := NewLimitedReader(context.Background(), bytes.NewReader(sourceData), testLimiter) readData, readError := io.ReadAll(limitedReader) if readError != nil { testInstance.Fatalf("das Lesen schlug fehl: %v", readError) } if !bytes.Equal(readData, sourceData) { testInstance.Fatal("die begrenzte Übertragung veränderte die Daten") } } // TestLimitedWriterDeliversAllBytes prüft das Schreibziel. func TestLimitedWriterDeliversAllBytes(testInstance *testing.T) { testLimiter, _ := newTestLimiter(testInstance, 1<<20) sourceData := bytes.Repeat([]byte("Backup"), 50_000) var targetBuffer bytes.Buffer limitedWriter := NewLimitedWriter(context.Background(), &targetBuffer, testLimiter) writtenCount, writeError := limitedWriter.Write(sourceData) if writeError != nil { testInstance.Fatalf("das Schreiben schlug fehl: %v", writeError) } if writtenCount != len(sourceData) { testInstance.Errorf("es wurden %d von %d Byte geschrieben", writtenCount, len(sourceData)) } if !bytes.Equal(targetBuffer.Bytes(), sourceData) { testInstance.Fatal("die begrenzte Übertragung veränderte die Daten") } } // TestSharedLimiterSplitsRateBetweenWorkers prüft die gemeinsame Nutzung. // // Ein Begrenzer je Arbeiter ergäbe ein Vielfaches der vereinbarten Rate — genau // der Fehler, der eine Bandbreitengrenze wirkungslos macht. func TestSharedLimiterSplitsRateBetweenWorkers(testInstance *testing.T) { const bytesPerSecond int64 = 1 << 20 testLimiter, testClock := newTestLimiter(testInstance, bytesPerSecond) const workerCount = 4 const bytesPerWorker = 1 << 20 var waitGroup sync.WaitGroup waitGroup.Add(workerCount) for workerIndex := 0; workerIndex < workerCount; workerIndex++ { go func() { defer waitGroup.Done() _ = testLimiter.Wait(context.Background(), bytesPerWorker) }() } waitGroup.Wait() // Vier Arbeiter übertragen zusammen 4 MiB; einer war vorrätig, für drei // muss gewartet werden. expectedMinimumWait := 2 * time.Second if testClock.totalSlept < expectedMinimumWait { testInstance.Fatalf("vier Arbeiter warteten zusammen nur %s; die Rate wurde vervielfacht", testClock.totalSlept) } } // TestParseBandwidthLimitDistinguishesBitsAndBytes ist der wichtigste // Auswertungstest. // // Netzwerkleute rechnen in Bit je Sekunde, Speicherleute in Byte. Wer // „100Mbit" meint und „100MB" einträgt, vergibt das Achtfache. func TestParseBandwidthLimitDistinguishesBitsAndBytes(testInstance *testing.T) { expectedValues := map[string]int64{ "": Unlimited, "0": Unlimited, "unbegrenzt": Unlimited, "50MB": 50 << 20, "50 MB": 50 << 20, "50MB/s": 50 << 20, "1GB": 1 << 30, "512KB": 512 << 10, "100Mbit": 100 * 1000 * 1000 / 8, "1Gbit": 1000 * 1000 * 1000 / 8, "1.5MB": 1_572_864, "1,5MB": 1_572_864, "1048576": 1 << 20, } for limitText, expectedValue := range expectedValues { parsedValue, parseError := ParseBandwidthLimit(limitText) if parseError != nil { testInstance.Errorf("%q wurde abgelehnt: %v", limitText, parseError) continue } if parsedValue != expectedValue { testInstance.Errorf("%q ergab %d, erwartet waren %d", limitText, parsedValue, expectedValue) } } // Der Kern: 100Mbit ist deutlich weniger als 100MB. bitValue, _ := ParseBandwidthLimit("100Mbit") byteValue, _ := ParseBandwidthLimit("100MB") if bitValue >= byteValue { testInstance.Fatalf("100Mbit (%d) wurde nicht kleiner als 100MB (%d) ausgewertet", bitValue, byteValue) } } // TestParseBandwidthLimitRejectsNonsense prüft die Eingabeprüfung. func TestParseBandwidthLimitRejectsNonsense(testInstance *testing.T) { invalidInputs := []string{"schnell", "-5MB", "MB", "1,2,3MB"} for _, invalidInput := range invalidInputs { if _, parseError := ParseBandwidthLimit(invalidInput); parseError == nil { testInstance.Errorf("die Eingabe %q wurde angenommen", invalidInput) } } } // TestNewLimiterRejectsUnusablyLowRates prüft die Untergrenze. // // Kleiner als ein typischer Netzwerkblock zu begrenzen führt zu ständigem // Warten in winzigen Schritten; der Durchsatz bräche stärker ein als vorgesehen. func TestNewLimiterRejectsUnusablyLowRates(testInstance *testing.T) { if _, limiterError := NewLimiter(100); !errors.Is(limiterError, ErrInvalidRate) { testInstance.Errorf("eine zu kleine Rate wurde angenommen: %v", limiterError) } if _, limiterError := NewLimiter(-1); !errors.Is(limiterError, ErrInvalidRate) { testInstance.Errorf("eine negative Rate wurde angenommen: %v", limiterError) } } // TestFormatBandwidthLimit prüft die Ausgabe. func TestFormatBandwidthLimit(testInstance *testing.T) { expectedTexts := map[int64]string{ Unlimited: "unbegrenzt", 50 << 20: "50.0 MB/s", 1 << 30: "1.0 GB/s", 512 << 10: "512.0 KB/s", 500: "500 B/s", } for bytesPerSecond, expectedText := range expectedTexts { if formattedText := FormatBandwidthLimit(bytesPerSecond); formattedText != expectedText { testInstance.Errorf("%d ergab %q, erwartet war %q", bytesPerSecond, formattedText, expectedText) } } }