Files
prysm/beacon-chain/sync/backfill/log_test.go
kasey 61de11e2c4 Backfill data columns (#15580)
**What type of PR is this?**

Feature

**What does this PR do? Why is it needed?**

Adds data column support to backfill.

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description to this PR with sufficient context for
reviewers to understand this PR.

---------

Co-authored-by: Kasey <kasey@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Preston Van Loon <preston@pvl.dev>
2025-12-02 15:19:32 +00:00

380 lines
11 KiB
Go

package backfill
import (
"bytes"
"sync"
"testing"
"time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
// trackingHook is a logrus hook that counts Log callCount for testing.
type trackingHook struct {
mu sync.RWMutex
entries []*logrus.Entry
}
func (h *trackingHook) Levels() []logrus.Level {
return logrus.AllLevels
}
func (h *trackingHook) Fire(entry *logrus.Entry) error {
h.mu.Lock()
defer h.mu.Unlock()
h.entries = append(h.entries, entry)
return nil
}
func (h *trackingHook) callCount() int {
h.mu.RLock()
defer h.mu.RUnlock()
return len(h.entries)
}
func (h *trackingHook) emitted(t *testing.T) []string {
h.mu.RLock()
defer h.mu.RUnlock()
e := make([]string, len(h.entries))
for i, entry := range h.entries {
entry.Buffer = new(bytes.Buffer)
serialized, err := entry.Logger.Formatter.Format(entry)
require.NoError(t, err)
e[i] = string(serialized)
}
return e
}
func entryWithHook() (*logrus.Entry, *trackingHook) {
logger := logrus.New()
logger.SetLevel(logrus.TraceLevel)
hook := &trackingHook{}
logger.AddHook(hook)
entry := logrus.NewEntry(logger)
return entry, hook
}
func intervalSecondsAndDuration(i int) (int64, time.Duration) {
return int64(i), time.Duration(i) * time.Second
}
// mockClock provides a controllable time source for testing.
// It allows tests to set the current time and advance it as needed.
type mockClock struct {
t time.Time
}
// now returns the current time.
func (c *mockClock) now() time.Time {
return c.t
}
func setupMockClock(il *intervalLogger) *mockClock {
// initialize now so that the time aligns with the start of the
// interval bucket. This ensures that adding less than an interval
// of time to the timestamp can never move into the next bucket.
interval := intervalNumber(time.Now(), il.seconds)
now := time.Unix(interval*il.seconds, 0)
clock := &mockClock{t: now}
il.now = clock.now
return clock
}
// TestNewIntervalLogger verifies logger is properly initialized
func TestNewIntervalLogger(t *testing.T) {
base := logrus.NewEntry(logrus.New())
intSec := int64(10)
il := newIntervalLogger(base, intSec)
require.NotNil(t, il)
require.Equal(t, intSec, il.seconds)
require.Equal(t, int64(0), il.last.Load())
require.Equal(t, base, il.Entry)
}
// TestLogOncePerInterval verifies that Log is called only once within an interval window
func TestLogOncePerInterval(t *testing.T) {
entry, hook := entryWithHook()
il := newIntervalLogger(entry, 10)
_ = setupMockClock(il) // use a fixed time to make sure no race is possible
// First log should call the underlying Log method
il.Log(logrus.InfoLevel, "test message 1")
require.Equal(t, 1, hook.callCount())
// Second log in same interval should not call Log
il.Log(logrus.InfoLevel, "test message 2")
require.Equal(t, 1, hook.callCount())
// Third log still in same interval should not call Log
il.Log(logrus.InfoLevel, "test message 3")
require.Equal(t, 1, hook.callCount())
// Verify last is set to current interval
require.Equal(t, il.intervalNumber(), il.last.Load())
}
// TestLogAcrossIntervalBoundary verifies logging at interval boundaries resets correctly
func TestLogAcrossIntervalBoundary(t *testing.T) {
iSec, iDur := intervalSecondsAndDuration(10)
entry, hook := entryWithHook()
il := newIntervalLogger(entry, iSec)
clock := setupMockClock(il)
il.Log(logrus.InfoLevel, "first interval")
require.Equal(t, 1, hook.callCount())
// Log in new interval should succeed
clock.t = clock.t.Add(2 * iDur)
il.Log(logrus.InfoLevel, "second interval")
require.Equal(t, 2, hook.callCount())
}
// TestWithFieldChaining verifies WithField returns logger and can be chained
func TestWithFieldChaining(t *testing.T) {
entry, hook := entryWithHook()
iSec, iDur := intervalSecondsAndDuration(10)
il := newIntervalLogger(entry, iSec)
clock := setupMockClock(il)
result := il.WithField("key1", "value1")
require.NotNil(t, result)
result.Info("test")
require.Equal(t, 1, hook.callCount())
// make sure there was no mutation of the base as a side effect
clock.t = clock.t.Add(iDur)
il.Info("another")
// Verify field is present in logged entry
emitted := hook.emitted(t)
require.Contains(t, emitted[0], "test")
require.Contains(t, emitted[0], "key1=value1")
require.Contains(t, emitted[1], "another")
require.NotContains(t, emitted[1], "key1=value1")
}
// TestWithFieldsChaining verifies WithFields properly adds multiple fields
func TestWithFieldsChaining(t *testing.T) {
entry, hook := entryWithHook()
iSec, iDur := intervalSecondsAndDuration(10)
il := newIntervalLogger(entry, iSec)
clock := setupMockClock(il)
fields := logrus.Fields{
"key1": "value1",
"key2": "value2",
}
result := il.WithFields(fields)
require.NotNil(t, result)
result.Info("test")
require.Equal(t, 1, hook.callCount())
// make sure there was no mutation of the base as a side effect
clock.t = clock.t.Add(iDur)
il.Info("another")
// Verify field is present in logged entry
emitted := hook.emitted(t)
require.Contains(t, emitted[0], "test")
require.Contains(t, emitted[0], "key1=value1")
require.Contains(t, emitted[0], "key2=value2")
require.Contains(t, emitted[1], "another")
require.NotContains(t, emitted[1], "key1=value1")
require.NotContains(t, emitted[1], "key2=value2")
}
// TestWithErrorChaining verifies WithError properly adds error field
func TestWithErrorChaining(t *testing.T) {
entry, hook := entryWithHook()
iSec, iDur := intervalSecondsAndDuration(10)
il := newIntervalLogger(entry, iSec)
clock := setupMockClock(il)
expected := errors.New("lowercase words")
result := il.WithError(expected)
require.NotNil(t, result)
result.Error("test")
require.Equal(t, 1, hook.callCount())
require.NotNil(t, result)
// make sure there was no mutation of the base as a side effect
clock.t = clock.t.Add(iDur)
il.Info("different")
// Verify field is present in logged entry
emitted := hook.emitted(t)
require.Contains(t, emitted[0], expected.Error())
require.Contains(t, emitted[0], "test")
require.Contains(t, emitted[1], "different")
require.NotContains(t, emitted[1], "test")
require.NotContains(t, emitted[1], "lowercase words")
}
// TestLogLevelMethods verifies all log level methods work and respect rate limiting
func TestLogLevelMethods(t *testing.T) {
entry, hook := entryWithHook()
il := newIntervalLogger(entry, 10)
_ = setupMockClock(il) // use a fixed time to make sure no race is possible
// First call from each level-specific method should succeed
il.Trace("trace message")
require.Equal(t, 1, hook.callCount())
// Subsequent callCount in same interval should be suppressed
il.Debug("debug message")
require.Equal(t, 1, hook.callCount())
il.Info("info message")
require.Equal(t, 1, hook.callCount())
il.Print("print message")
require.Equal(t, 1, hook.callCount())
il.Warn("warn message")
require.Equal(t, 1, hook.callCount())
il.Warning("warning message")
require.Equal(t, 1, hook.callCount())
il.Error("error message")
require.Equal(t, 1, hook.callCount())
}
// TestConcurrentLogging verifies multiple goroutines can safely call Log concurrently
func TestConcurrentLogging(t *testing.T) {
entry, hook := entryWithHook()
il := newIntervalLogger(entry, 10)
_ = setupMockClock(il) // use a fixed time to make sure no race is possible
var wg sync.WaitGroup
wait := make(chan struct{})
for range 10 {
wg.Add(1)
go func() {
<-wait
defer wg.Done()
il.Log(logrus.InfoLevel, "concurrent message")
}()
}
close(wait) // maximize raciness by unblocking goroutines together
wg.Wait()
// Only one Log call should succeed across all goroutines in the same interval
require.Equal(t, 1, hook.callCount())
}
// TestZeroInterval verifies behavior with small interval (logs every second)
func TestZeroInterval(t *testing.T) {
entry, hook := entryWithHook()
il := newIntervalLogger(entry, 1)
clock := setupMockClock(il)
il.Log(logrus.InfoLevel, "first")
require.Equal(t, 1, hook.callCount())
// Move to next second
clock.t = clock.t.Add(time.Second)
il.Log(logrus.InfoLevel, "second")
require.Equal(t, 2, hook.callCount())
}
// TestCompleteLoggingFlow tests realistic scenario with repeated logging
func TestCompleteLoggingFlow(t *testing.T) {
entry, hook := entryWithHook()
iSec, iDur := intervalSecondsAndDuration(10)
il := newIntervalLogger(entry, iSec)
clock := setupMockClock(il)
// Add field
il = il.WithField("request_id", "12345")
// Log multiple times in same interval - only first succeeds
il.Info("message 1")
require.Equal(t, 1, hook.callCount())
il.Warn("message 2")
require.Equal(t, 1, hook.callCount())
// Move to next interval
clock.t = clock.t.Add(iDur)
// Should be able to log again in new interval
il.Error("message 3")
require.Equal(t, 2, hook.callCount())
require.NotNil(t, il)
}
// TestAtomicSwapCorrectness verifies atomic swap works correctly
func TestAtomicSwapCorrectness(t *testing.T) {
il := newIntervalLogger(logrus.NewEntry(logrus.New()), 10)
_ = setupMockClock(il) // use a fixed time to make sure no race is possible
// Swap operation should return different value on first call
current := il.intervalNumber()
old := il.last.Swap(current)
require.Equal(t, int64(0), old) // initial value is 0
require.Equal(t, current, il.last.Load())
// Swap with same value should return the same value
old = il.last.Swap(current)
require.Equal(t, current, old)
}
// TestLogMethodsWithClockAdvancement verifies that log methods respect rate limiting
// within an interval but emit again after the interval passes.
func TestLogMethodsWithClockAdvancement(t *testing.T) {
entry, hook := entryWithHook()
iSec, iDur := intervalSecondsAndDuration(10)
il := newIntervalLogger(entry, iSec)
clock := setupMockClock(il)
// First Error call should log
il.Error("error 1")
require.Equal(t, 1, hook.callCount())
// Warn call in same interval should be suppressed
il.Warn("warn 1")
require.Equal(t, 1, hook.callCount())
// Info call in same interval should be suppressed
il.Info("info 1")
require.Equal(t, 1, hook.callCount())
// Debug call in same interval should be suppressed
il.Debug("debug 1")
require.Equal(t, 1, hook.callCount())
// Move forward 5 seconds - still in same 10-second interval
require.Equal(t, 5*time.Second, iDur/2)
clock.t = clock.t.Add(iDur / 2)
il.Error("error 2")
require.Equal(t, 1, hook.callCount(), "should still be suppressed within same interval")
firstInterval := il.intervalNumber()
// Move forward to next interval (10 second interval boundary)
clock.t = clock.t.Add(iDur / 2)
nextInterval := il.intervalNumber()
require.NotEqual(t, firstInterval, nextInterval, "should be in new interval now")
il.Error("error 3")
require.Equal(t, 2, hook.callCount(), "should emit in new interval")
// Another call in the new interval should be suppressed
il.Warn("warn 2")
require.Equal(t, 2, hook.callCount())
// Move forward to yet another interval
clock.t = clock.t.Add(iDur)
il.Info("info 2")
require.Equal(t, 3, hook.callCount(), "should emit in third interval")
}