Add tests for the critical mining path
Tests cover the four subsystems that sit between a share submission and
a confirmed block reward:
bitcoind/rpc_test.go — isRetryable classification, 3-attempt backoff
on 503, no retry on semantic RPC errors (-5), retry on warm-up (-28),
CoinbaseReward calculation.
logmon/tailer_test.go — handleLine regex parsing, share-diff carry from
attempt line to block event, diff reset after use, unrelated-line
silence, file-read via goroutine, cursor save/resume across restart,
log rotation re-open.
store/blocks_test.go — InsertBlock roundtrip (all fields including
chain), idempotent dedup, MarkOrphaned, UpdateEnrichment,
BlocksNeedingEnrichment, KV get/set/overwrite, and migration of an
existing DB that lacks the chain column.
state/blocks_test.go — reconcileOnce chain-filter (testnet block not
orphaned on mainnet), genuine reorg detection (different canonical
hash → orphaned), transient RPC error (no orphan on -5), legacy
empty-chain block still checked for reorgs, IngestBlockEvents new
block persisted + confirmed counter incremented, dedup skips counter
and OnRefresh, chain stamped from current snapshot at ingest time.
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
package logmon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func discardLog() *slog.Logger {
|
||||
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
}
|
||||
|
||||
// ---- handleLine unit tests (no filesystem, no goroutines) ----
|
||||
|
||||
func TestHandleLine_Solved(t *testing.T) {
|
||||
tl := New("/unused", discardLog())
|
||||
tl.handleLine("Solved and confirmed block 840123\n")
|
||||
select {
|
||||
case ev := <-tl.Events:
|
||||
if ev.Height != 840123 {
|
||||
t.Errorf("Height = %d, want 840123", ev.Height)
|
||||
}
|
||||
default:
|
||||
t.Fatal("no BlockEvent emitted for solved line")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleLine_AttemptCarriedToBlock(t *testing.T) {
|
||||
tl := New("/unused", discardLog())
|
||||
tl.handleLine("Possible block solve diff 32768 !\n")
|
||||
tl.handleLine("Solved and confirmed block 900000\n")
|
||||
|
||||
select {
|
||||
case <-tl.Attempts:
|
||||
default:
|
||||
t.Fatal("no AttemptEvent emitted for attempt line")
|
||||
}
|
||||
select {
|
||||
case ev := <-tl.Events:
|
||||
if ev.ShareDiff != 32768 {
|
||||
t.Errorf("ShareDiff = %v, want 32768", ev.ShareDiff)
|
||||
}
|
||||
if ev.Height != 900000 {
|
||||
t.Errorf("Height = %d, want 900000", ev.Height)
|
||||
}
|
||||
default:
|
||||
t.Fatal("no BlockEvent emitted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleLine_SubmittingVariant(t *testing.T) {
|
||||
tl := New("/unused", discardLog())
|
||||
tl.handleLine("Submitting possible block solve share diff 65536 !\n")
|
||||
select {
|
||||
case ev := <-tl.Attempts:
|
||||
if ev.ShareDiff != 65536 {
|
||||
t.Errorf("ShareDiff = %v, want 65536", ev.ShareDiff)
|
||||
}
|
||||
default:
|
||||
t.Fatal("no AttemptEvent for submitting variant")
|
||||
}
|
||||
// Must NOT also emit a BlockEvent.
|
||||
select {
|
||||
case ev := <-tl.Events:
|
||||
t.Errorf("unexpected BlockEvent from attempt line: %+v", ev)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleLine_DiffResetAfterBlock(t *testing.T) {
|
||||
tl := New("/unused", discardLog())
|
||||
tl.handleLine("Possible block solve diff 16384 !\n")
|
||||
tl.handleLine("Solved and confirmed block 100\n")
|
||||
// Second block — no preceding attempt line; diff must NOT carry over.
|
||||
tl.handleLine("Solved and confirmed block 101\n")
|
||||
|
||||
drain := func() BlockEvent {
|
||||
select {
|
||||
case ev := <-tl.Events:
|
||||
return ev
|
||||
default:
|
||||
t.Fatal("no BlockEvent")
|
||||
return BlockEvent{}
|
||||
}
|
||||
}
|
||||
ev100 := drain()
|
||||
ev101 := drain()
|
||||
if ev100.ShareDiff != 16384 {
|
||||
t.Errorf("block 100 ShareDiff = %v, want 16384", ev100.ShareDiff)
|
||||
}
|
||||
if ev101.ShareDiff != 0 {
|
||||
t.Errorf("block 101 ShareDiff = %v, want 0 (no preceding attempt)", ev101.ShareDiff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleLine_Unrelated(t *testing.T) {
|
||||
tl := New("/unused", discardLog())
|
||||
for _, line := range []string{
|
||||
"Stratifier started\n",
|
||||
"New block hash 000000abc\n",
|
||||
"Worker authenticated\n",
|
||||
} {
|
||||
tl.handleLine(line)
|
||||
}
|
||||
select {
|
||||
case ev := <-tl.Events:
|
||||
t.Errorf("unexpected BlockEvent from unrelated line: %+v", ev)
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case ev := <-tl.Attempts:
|
||||
t.Errorf("unexpected AttemptEvent from unrelated line: %+v", ev)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Tailer.Run integration tests (filesystem + goroutines) ----
|
||||
|
||||
func tempLog(t *testing.T) string {
|
||||
t.Helper()
|
||||
f, err := os.CreateTemp("", "ckpool*.log")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
t.Cleanup(func() { _ = os.Remove(f.Name()) })
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
func appendLines(t *testing.T, path string, lines ...string) {
|
||||
t.Helper()
|
||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
for _, l := range lines {
|
||||
if _, err := f.WriteString(l); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestTailerRun_BasicRead starts the tailer on an empty file, appends log
|
||||
// lines, and verifies the expected events are emitted.
|
||||
func TestTailerRun_BasicRead(t *testing.T) {
|
||||
path := tempLog(t)
|
||||
tl := New(path, discardLog())
|
||||
tl.PollWait = 20 * time.Millisecond
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
go tl.Run(ctx)
|
||||
|
||||
// Give the tailer time to open the empty file and park at EOF.
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
|
||||
appendLines(t, path,
|
||||
"Possible block solve diff 16384 !\n",
|
||||
"Solved and confirmed block 100\n",
|
||||
)
|
||||
|
||||
select {
|
||||
case <-tl.Attempts:
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timed out waiting for AttemptEvent")
|
||||
}
|
||||
select {
|
||||
case ev := <-tl.Events:
|
||||
if ev.Height != 100 {
|
||||
t.Errorf("Height = %d, want 100", ev.Height)
|
||||
}
|
||||
if ev.ShareDiff != 16384 {
|
||||
t.Errorf("ShareDiff = %v, want 16384", ev.ShareDiff)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timed out waiting for BlockEvent")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTailerRun_CursorResume verifies that a new tailer honours a saved
|
||||
// cursor and does not re-emit lines that were already processed.
|
||||
func TestTailerRun_CursorResume(t *testing.T) {
|
||||
path := tempLog(t)
|
||||
|
||||
var mu sync.Mutex
|
||||
var savedIno uint64
|
||||
var savedOff int64
|
||||
|
||||
tl := New(path, discardLog())
|
||||
tl.PollWait = 20 * time.Millisecond
|
||||
tl.SaveCursor = func(ino uint64, off int64) {
|
||||
mu.Lock()
|
||||
savedIno, savedOff = ino, off
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
ctx1, cancel1 := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
go tl.Run(ctx1)
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
|
||||
// Phase 1: write 2 blocks and drain both events.
|
||||
appendLines(t, path,
|
||||
"Solved and confirmed block 200\n",
|
||||
"Solved and confirmed block 201\n",
|
||||
)
|
||||
for i := 0; i < 2; i++ {
|
||||
select {
|
||||
case <-tl.Events:
|
||||
case <-ctx1.Done():
|
||||
t.Fatalf("phase 1: only got %d events, want 2", i)
|
||||
}
|
||||
}
|
||||
// Allow the tailer to hit EOF and save the cursor.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
cancel1()
|
||||
// Give the goroutine time to exit and force-save.
|
||||
time.Sleep(60 * time.Millisecond)
|
||||
|
||||
mu.Lock()
|
||||
ino, off := savedIno, savedOff
|
||||
mu.Unlock()
|
||||
if ino == 0 {
|
||||
t.Fatal("cursor was never saved after phase 1")
|
||||
}
|
||||
|
||||
// Phase 2: append a third block, start a new tailer from the cursor.
|
||||
// It must see ONLY block 202, not replays of 200 or 201.
|
||||
appendLines(t, path, "Solved and confirmed block 202\n")
|
||||
|
||||
tl2 := New(path, discardLog())
|
||||
tl2.PollWait = 20 * time.Millisecond
|
||||
tl2.LoadCursor = func() (uint64, int64, bool) { return ino, off, true }
|
||||
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel2()
|
||||
go tl2.Run(ctx2)
|
||||
|
||||
select {
|
||||
case ev := <-tl2.Events:
|
||||
if ev.Height != 202 {
|
||||
t.Errorf("resumed tailer emitted height %d, want 202 (cursor resume may be broken)", ev.Height)
|
||||
}
|
||||
case <-ctx2.Done():
|
||||
t.Fatal("timed out waiting for block 202 after cursor resume")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTailerRun_Rotation verifies that when the log file is replaced
|
||||
// (inode changes), the tailer re-opens it from the start and reads the
|
||||
// new content.
|
||||
func TestTailerRun_Rotation(t *testing.T) {
|
||||
path := tempLog(t)
|
||||
tl := New(path, discardLog())
|
||||
tl.PollWait = 30 * time.Millisecond
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
go tl.Run(ctx)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Write a block to the original file and collect it.
|
||||
appendLines(t, path, "Solved and confirmed block 300\n")
|
||||
select {
|
||||
case ev := <-tl.Events:
|
||||
if ev.Height != 300 {
|
||||
t.Errorf("before rotation: height = %d, want 300", ev.Height)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timed out waiting for pre-rotation block")
|
||||
}
|
||||
|
||||
// Simulate log rotation: replace the file at the same path.
|
||||
newF, err := os.CreateTemp("", "ckpool_new*.log")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
newF.WriteString("Solved and confirmed block 301\n")
|
||||
newF.Close()
|
||||
if err := os.Rename(newF.Name(), path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
select {
|
||||
case ev := <-tl.Events:
|
||||
if ev.Height != 301 {
|
||||
t.Errorf("after rotation: height = %d, want 301", ev.Height)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timed out waiting for post-rotation block")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user