Skip to content

Commit bc7d968

Browse files
committed
test: fold new tests into the existing per-source test files
1 parent b090dd4 commit bc7d968

6 files changed

Lines changed: 320 additions & 355 deletions

File tree

universalClient/chains/common/event_cleaner_race_test.go

Lines changed: 0 additions & 147 deletions
This file was deleted.

universalClient/chains/common/event_cleaner_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package common
33
import (
44
"context"
55
"fmt"
6+
"sync"
67
"testing"
78
"time"
89

@@ -337,3 +338,138 @@ func TestEventCleanerStartStopLifecycle(t *testing.T) {
337338
time.Sleep(50 * time.Millisecond)
338339
})
339340
}
341+
342+
// Start and Stop race against the cleanup goroutine. Run under -race.
343+
func TestEventCleaner_StartStopUnderRace(t *testing.T) {
344+
for i := 0; i < 20; i++ {
345+
database := newTestCleanerDB(t, nil)
346+
cleaner := NewEventCleaner(database, intPtr(3600), intPtr(0), "test-chain", zerolog.Nop())
347+
// Fast enough that the goroutine is inside performCleanup while Stop runs.
348+
cleaner.cleanupInterval = time.Millisecond
349+
350+
require.NoError(t, cleaner.Start(context.Background()))
351+
cleaner.Stop()
352+
}
353+
}
354+
355+
// Concurrent Stop calls must not double close the channel or return before the
356+
// goroutine has exited.
357+
func TestEventCleaner_ConcurrentStop(t *testing.T) {
358+
database := newTestCleanerDB(t, nil)
359+
cleaner := NewEventCleaner(database, intPtr(3600), intPtr(0), "test-chain", zerolog.Nop())
360+
cleaner.cleanupInterval = time.Millisecond
361+
362+
require.NoError(t, cleaner.Start(context.Background()))
363+
364+
var wg sync.WaitGroup
365+
for i := 0; i < 8; i++ {
366+
wg.Add(1)
367+
go func() {
368+
defer wg.Done()
369+
cleaner.Stop()
370+
}()
371+
}
372+
wg.Wait()
373+
374+
assert.False(t, cleaner.running)
375+
}
376+
377+
// Concurrent Start calls must leave exactly one goroutine running.
378+
func TestEventCleaner_ConcurrentStart(t *testing.T) {
379+
database := newTestCleanerDB(t, nil)
380+
cleaner := NewEventCleaner(database, intPtr(3600), intPtr(0), "test-chain", zerolog.Nop())
381+
cleaner.cleanupInterval = time.Millisecond
382+
383+
var mu sync.Mutex
384+
started := 0
385+
386+
var wg sync.WaitGroup
387+
for i := 0; i < 8; i++ {
388+
wg.Add(1)
389+
go func() {
390+
defer wg.Done()
391+
if err := cleaner.Start(context.Background()); err == nil {
392+
mu.Lock()
393+
started++
394+
mu.Unlock()
395+
}
396+
}()
397+
}
398+
wg.Wait()
399+
400+
assert.Equal(t, 1, started, "more than one cleanup goroutine was started")
401+
cleaner.Stop()
402+
}
403+
404+
// Stop must not return while a cleanup is still in flight, otherwise the caller
405+
// can close the chain database underneath an in-flight query.
406+
//
407+
// Held open with a write transaction so the goroutine is genuinely blocked
408+
// inside performCleanup while Stop is called. Without that, the goroutine exits
409+
// so fast that a Stop which does not wait looks identical to one that does.
410+
func TestEventCleaner_StopWaitsForInFlightCleanup(t *testing.T) {
411+
database := newTestCleanerDB(t, nil)
412+
cleaner := NewEventCleaner(database, intPtr(3600), intPtr(0), "test-chain", zerolog.Nop())
413+
cleaner.cleanupInterval = time.Millisecond
414+
415+
// Start first: the initial cleanup is synchronous and would block on the lock.
416+
require.NoError(t, cleaner.Start(context.Background()))
417+
418+
// Take the write lock so the next ticked cleanup blocks on DELETE.
419+
tx := database.Client().Begin()
420+
require.NoError(t, tx.Error)
421+
require.NoError(t, tx.Exec(
422+
"CREATE TABLE IF NOT EXISTS lock_probe (id INTEGER PRIMARY KEY)").Error)
423+
require.NoError(t, tx.Exec("INSERT INTO lock_probe (id) VALUES (1)").Error)
424+
425+
time.Sleep(50 * time.Millisecond) // let a tick land and block
426+
427+
stopped := make(chan struct{})
428+
go func() {
429+
cleaner.Stop()
430+
close(stopped)
431+
}()
432+
433+
select {
434+
case <-stopped:
435+
tx.Rollback()
436+
t.Fatal("Stop returned while a cleanup was still in flight")
437+
case <-time.After(200 * time.Millisecond):
438+
}
439+
440+
tx.Rollback() // release the lock; the cleanup can now finish
441+
442+
select {
443+
case <-stopped:
444+
case <-time.After(5 * time.Second):
445+
t.Fatal("Stop did not return after the cleanup finished")
446+
}
447+
}
448+
449+
// Cancelling the context stops the goroutine, and a later Stop is still safe.
450+
func TestEventCleaner_ContextCancelThenStop(t *testing.T) {
451+
database := newTestCleanerDB(t, nil)
452+
cleaner := NewEventCleaner(database, intPtr(3600), intPtr(0), "test-chain", zerolog.Nop())
453+
cleaner.cleanupInterval = time.Millisecond
454+
455+
ctx, cancel := context.WithCancel(context.Background())
456+
require.NoError(t, cleaner.Start(ctx))
457+
cancel()
458+
time.Sleep(20 * time.Millisecond)
459+
460+
cleaner.Stop() // must not hang or panic
461+
assert.False(t, cleaner.running)
462+
}
463+
464+
// Restart after Stop gets a fresh channel rather than reusing the closed one.
465+
func TestEventCleaner_RestartAfterStop(t *testing.T) {
466+
database := newTestCleanerDB(t, nil)
467+
cleaner := NewEventCleaner(database, intPtr(3600), intPtr(0), "test-chain", zerolog.Nop())
468+
cleaner.cleanupInterval = time.Millisecond
469+
470+
require.NoError(t, cleaner.Start(context.Background()))
471+
cleaner.Stop()
472+
473+
require.NoError(t, cleaner.Start(context.Background()), "restart was refused")
474+
cleaner.Stop()
475+
}

universalClient/chains/common/event_processor_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,3 +1054,77 @@ func TestProcessConfirmedEventsEnabledFlags(t *testing.T) {
10541054
assert.Equal(t, store.StatusConfirmed, inboundEvt.Status)
10551055
})
10561056
}
1057+
1058+
// The wire values the gateways emit are 0-indexed (Gas, GasAndPayload, Funds,
1059+
// FundsAndPayload) while the chain enum reserves 0 for UNSPECIFIED, so the
1060+
// mapping is shifted by one. A decoder that leaves TxType unset therefore does
1061+
// not produce "unknown", it produces GAS.
1062+
func TestConstructInbound_TxTypeMapping(t *testing.T) {
1063+
processor := &EventProcessor{}
1064+
1065+
for _, tc := range []struct {
1066+
wire uint
1067+
want uexecutortypes.TxType
1068+
}{
1069+
{0, uexecutortypes.TxType_GAS},
1070+
{1, uexecutortypes.TxType_GAS_AND_PAYLOAD},
1071+
{2, uexecutortypes.TxType_FUNDS},
1072+
{3, uexecutortypes.TxType_FUNDS_AND_PAYLOAD},
1073+
{4, uexecutortypes.TxType_UNSPECIFIED_TX},
1074+
{99, uexecutortypes.TxType_UNSPECIFIED_TX},
1075+
} {
1076+
data, err := json.Marshal(UniversalTx{
1077+
SourceChain: "solana:devnet",
1078+
Sender: "0xabc",
1079+
Recipient: "0xdef",
1080+
Amount: "5000000",
1081+
TxType: tc.wire,
1082+
})
1083+
require.NoError(t, err)
1084+
1085+
inbound, err := processor.constructInbound(&store.Event{
1086+
EventID: "sig:0",
1087+
EventData: data,
1088+
})
1089+
require.NoError(t, err)
1090+
assert.Equal(t, tc.want, inbound.TxType, "wire value %d", tc.wire)
1091+
}
1092+
}
1093+
1094+
// A FUNDS transfer must never reach the keeper as GAS. The two dispatch to
1095+
// different handlers: GAS mints and autoswaps into the sender UEA, FUNDS
1096+
// deposits PRC20 to the recipient, so the same amount lands with a different
1097+
// party. This is the end to end assertion the finding asks for.
1098+
func TestConstructInbound_FundsNeverBecomesGas(t *testing.T) {
1099+
processor := &EventProcessor{}
1100+
1101+
data, err := json.Marshal(UniversalTx{
1102+
SourceChain: "solana:devnet",
1103+
Sender: "0xabc",
1104+
Recipient: "0xdef",
1105+
Amount: "5000000",
1106+
TxType: 2, // Funds, as the real devnet events carry
1107+
})
1108+
require.NoError(t, err)
1109+
1110+
inbound, err := processor.constructInbound(&store.Event{
1111+
EventID: "sig:0",
1112+
EventData: data,
1113+
})
1114+
require.NoError(t, err)
1115+
1116+
assert.Equal(t, uexecutortypes.TxType_FUNDS, inbound.TxType)
1117+
assert.NotEqual(t, uexecutortypes.TxType_GAS, inbound.TxType,
1118+
"a FUNDS transfer routed to GAS credits the sender instead of the recipient")
1119+
}
1120+
1121+
// An event whose data never made it past the decoder must be refused outright
1122+
// rather than defaulted. The parsers now discard such events, so this is the
1123+
// backstop if one ever reaches the store.
1124+
func TestConstructInbound_RejectsEventWithoutData(t *testing.T) {
1125+
processor := &EventProcessor{}
1126+
1127+
_, err := processor.constructInbound(&store.Event{EventID: "sig:0"})
1128+
require.Error(t, err)
1129+
assert.Contains(t, err.Error(), "event data is missing")
1130+
}

0 commit comments

Comments
 (0)