mirror of
https://github.com/luxfi/bft.git
synced 2026-10-30 17:01:54 +00:00
Reapplies the fork's whole delta on top of the merge, as one reviewable diff rather than 43 conflict resolutions. Nothing here changes consensus behaviour. - module github.com/luxfi/bft, Go 1.26.4; root package simplex -> bft, which would otherwise collide with the simplex/ subpackage under this module name. - go.uber.org/zap -> github.com/luxfi/log, so common.Logger takes ...log.Field. zap.Error maps to log.Err: log.Error is luxfi/log's package-level logging function, not a field constructor, and the compiler is the only thing that catches that. Needs luxfi/log v1.6.1 for the Stringers constructor. - avalanchego/ -> host/. The package holds NodeID, VMBlock, Bitmask and size limits — the host node's types, vendored to avoid importing a whole node — so it is named for what it holds. Renaming it luxd/ would have swapped one brand for another and said just as little. - testutil/logger.go reimplemented without zapcore. Upstream layers a keyword-filtering Core, a debug-swallowing Encoder and a Tee because that is zap's extension model; luxfi/log has no such seam, so every policy is applied in one emit. That also fixes two defects the layering caused: SilenceExceptKeywords rebuilt the logger and discarded an installed Intercept hook, and the two sinks read different env vars (LOG_LEVEL vs log_level) so they disagreed about suppressing debug. Hooks run outside the lock, so a hook that logs fails the test instead of deadlocking it. Fatal panics rather than exiting, because exiting takes the suite down with no attribution. - simplex/monitor_test.go keeps a local logger: testutil imports simplex, so an internal test cannot import testutil back without closing a cycle. - README keeps the name Simplex throughout — it is the protocol's own name and the subject of reference [1]. Only the deployment target changed. The HyperSDK sentence still says Avalanche because that sentence is true. - Drops the fork's Makefile: it duplicated unit-tests.sh and fuzz.sh, and ran the suite with -timeout 30s when simplex alone needs over 100s. Co-authored-by: Hanzo Dev <[email protected]>
226 lines
6.4 KiB
Go
226 lines
6.4 KiB
Go
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package common
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/luxfi/log"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type noopLogger struct{}
|
|
|
|
func (noopLogger) Fatal(string, ...log.Field) {}
|
|
func (noopLogger) Error(string, ...log.Field) {}
|
|
func (noopLogger) Warn(string, ...log.Field) {}
|
|
func (noopLogger) Info(string, ...log.Field) {}
|
|
func (noopLogger) Trace(string, ...log.Field) {}
|
|
func (noopLogger) Debug(string, ...log.Field) {}
|
|
func (noopLogger) Verbo(string, ...log.Field) {}
|
|
|
|
const testName = "test"
|
|
const testRunInterval = 200 * time.Millisecond
|
|
|
|
func waitNoReceive[T any](t *testing.T, ch <-chan T) {
|
|
select {
|
|
case <-ch:
|
|
t.Fatal("channel unexpectedly signaled")
|
|
case <-time.After(defaultWaitDuration):
|
|
// good
|
|
}
|
|
}
|
|
|
|
func waitReceive[T any](t *testing.T, ch <-chan T) T {
|
|
select {
|
|
case v := <-ch:
|
|
return v
|
|
case <-time.After(defaultWaitDuration):
|
|
t.Fatal("timed out waiting for signal")
|
|
return *new(T)
|
|
}
|
|
}
|
|
|
|
// Ensures tasks only run when at/after the runInterval boundary, and that
|
|
// "too-early" ticks are ignored by the runner loop.
|
|
func TestTimeoutHandlerRunsOnlyOnInterval(t *testing.T) {
|
|
start := time.Now()
|
|
|
|
ran := make(chan []uint64, 4)
|
|
runner := func(ids []uint64) {
|
|
// copy since ids is reused by caller
|
|
cp := append([]uint64(nil), ids...)
|
|
ran <- cp
|
|
}
|
|
|
|
h := NewTimeoutHandler(noopLogger{}, testName, start, testRunInterval, runner)
|
|
defer h.Close()
|
|
|
|
h.AddTask(1)
|
|
|
|
// Too early: < runInterval since last tick -> should not run
|
|
h.Tick(start.Add(testRunInterval / 2))
|
|
waitNoReceive(t, ran)
|
|
|
|
// Exactly at interval: should run once with id=1
|
|
h.Tick(start.Add(testRunInterval))
|
|
batch := waitReceive(t, ran)
|
|
require.Equal(t, []uint64{1}, sorted(batch))
|
|
|
|
h.AddTask(2)
|
|
// Another tick but less than interval since the lastTickTime: should not run
|
|
h.Tick(start.Add(testRunInterval + testRunInterval/2))
|
|
waitNoReceive(t, ran)
|
|
|
|
// At 2*interval: should run again
|
|
h.Tick(start.Add(2 * testRunInterval))
|
|
batch = waitReceive(t, ran)
|
|
require.Equal(t, []uint64{1, 2}, sorted(batch))
|
|
}
|
|
|
|
// Add & Remove single task, verifying it stops running after removal.
|
|
func TestTimeoutHandler_AddThenRemoveTask(t *testing.T) {
|
|
start := time.Now()
|
|
|
|
ran := make(chan []uint64, 2)
|
|
runner := func(ids []uint64) { ran <- append([]uint64(nil), ids...) }
|
|
|
|
h := NewTimeoutHandler(noopLogger{}, testName, start, testRunInterval, runner)
|
|
defer h.Close()
|
|
|
|
h.AddTask(7)
|
|
h.Tick(start.Add(testRunInterval))
|
|
require.Equal(t, []uint64{7}, sorted(waitReceive(t, ran)))
|
|
|
|
// Remove then tick again; nothing should run
|
|
h.RemoveTask(7)
|
|
h.Tick(start.Add(2 * testRunInterval))
|
|
waitNoReceive(t, ran)
|
|
}
|
|
|
|
// Multiple tasks get batched and delivered together; removing one leaves the rest.
|
|
func TestTimeoutHandler_MultipleTasksBatchAndPersist(t *testing.T) {
|
|
start := time.Now()
|
|
|
|
ran := make(chan []uint64, 2)
|
|
runner := func(ids []uint64) { ran <- append([]uint64(nil), ids...) }
|
|
|
|
h := NewTimeoutHandler(noopLogger{}, testName, start, testRunInterval, runner)
|
|
defer h.Close()
|
|
|
|
h.AddTask(1)
|
|
h.AddTask(2)
|
|
h.AddTask(3)
|
|
|
|
// First run: should see all three (order not guaranteed)
|
|
h.Tick(start.Add(testRunInterval))
|
|
got := sorted(waitReceive(t, ran))
|
|
require.Equal(t, []uint64{1, 2, 3}, got)
|
|
|
|
// Remove one; remaining should continue to run on next valid tick
|
|
h.RemoveTask(2)
|
|
h.Tick(start.Add(2 * testRunInterval))
|
|
got = sorted(waitReceive(t, ran))
|
|
require.Equal(t, []uint64{1, 3}, got)
|
|
}
|
|
|
|
// Adding the same task twice should not duplicate it in the batch (set semantics).
|
|
func TestTimeoutHandler_AddDuplicateTaskIsIdempotent(t *testing.T) {
|
|
start := time.Now()
|
|
|
|
ran := make(chan []uint64, 1)
|
|
runner := func(ids []uint64) { ran <- append([]uint64(nil), ids...) }
|
|
|
|
h := NewTimeoutHandler(noopLogger{}, testName, start, testRunInterval, runner)
|
|
defer h.Close()
|
|
|
|
h.AddTask(42)
|
|
h.AddTask(42) // duplicate
|
|
|
|
h.Tick(start.Add(testRunInterval))
|
|
got := sorted(waitReceive(t, ran))
|
|
require.Equal(t, []uint64{42}, got)
|
|
}
|
|
|
|
// RemoveOldTasks should drop tasks where shouldRemove(id, cutoff) == true.
|
|
// With lessUint(a,b), that means id < cutoff.
|
|
func TestTimeoutHandler_RemoveOldTasks(t *testing.T) {
|
|
start := time.Now()
|
|
|
|
ran := make(chan []uint64, 2)
|
|
runner := func(ids []uint64) { ran <- append([]uint64(nil), ids...) }
|
|
|
|
h := NewTimeoutHandler(noopLogger{}, testName, start, testRunInterval, runner)
|
|
defer h.Close()
|
|
|
|
for _, id := range []uint64{1, 2, 3, 4, 5} {
|
|
h.AddTask(id)
|
|
}
|
|
|
|
// Remove everything with id < 3 -> removes 1,2 ; keeps 3,4,5
|
|
h.RemoveOldTasks(func(id uint64, _ struct{}) bool {
|
|
return id < 3
|
|
})
|
|
h.Tick(start.Add(testRunInterval))
|
|
got := sorted(waitReceive(t, ran))
|
|
require.Equal(t, []uint64{3, 4, 5}, got)
|
|
|
|
// Now remove everything with id < 5 -> removes 3,4 ; keeps 5
|
|
h.RemoveOldTasks(func(id uint64, _ struct{}) bool {
|
|
return id < 5
|
|
})
|
|
h.Tick(start.Add(2 * testRunInterval))
|
|
got = sorted(waitReceive(t, ran))
|
|
require.Equal(t, []uint64{5}, got)
|
|
}
|
|
|
|
// After Close, the goroutine stops and further ticks should not cause runs.
|
|
func TestTimeoutHandler_CloseStopsRunner(t *testing.T) {
|
|
start := time.Now()
|
|
|
|
ran := make(chan []uint64, 1)
|
|
runner := func(ids []uint64) { ran <- append([]uint64(nil), ids...) }
|
|
|
|
h := NewTimeoutHandler(noopLogger{}, testName, start, testRunInterval, runner)
|
|
|
|
h.Close()
|
|
// Calls after Close should be safe and no-ops for scheduling.
|
|
h.AddTask(9)
|
|
h.Tick(start.Add(testRunInterval))
|
|
waitNoReceive(t, ran)
|
|
}
|
|
|
|
// If ticks come in faster than runInterval, the second "too-soon" tick should
|
|
// not trigger execution (interval gating). This overlaps with RunsOnlyOnInterval,
|
|
// but emphasizes back-to-back ticks.
|
|
func TestTimeoutHandler_BackToBackTicksUnderIntervalDontRun(t *testing.T) {
|
|
start := time.Now()
|
|
|
|
ran := make(chan []uint64, 2)
|
|
runner := func(ids []uint64) { ran <- append([]uint64(nil), ids...) }
|
|
|
|
h := NewTimeoutHandler(noopLogger{}, testName, start, testRunInterval, runner)
|
|
defer h.Close()
|
|
|
|
h.AddTask(100)
|
|
|
|
h.Tick(start.Add(testRunInterval)) // should run
|
|
require.Equal(t, []uint64{100}, sorted(waitReceive(t, ran)))
|
|
h.Tick(start.Add(testRunInterval + time.Millisecond)) // < interval since lastTickTime -> no run
|
|
waitNoReceive(t, ran)
|
|
|
|
// Next valid interval boundary -> runs again
|
|
h.Tick(start.Add(2 * testRunInterval))
|
|
require.Equal(t, []uint64{100}, sorted(waitReceive(t, ran)))
|
|
}
|
|
|
|
func sorted(v []uint64) []uint64 {
|
|
sorted := append([]uint64(nil), v...)
|
|
slices.Sort(sorted)
|
|
return sorted
|
|
}
|