Skip to content

Commit 6af4300

Browse files
committed
[4110] add unit tests for actpool/queueworker
1 parent 0694b52 commit 6af4300

File tree

1 file changed

+351
-0
lines changed

1 file changed

+351
-0
lines changed

actpool/queueworker_test.go

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
package actpool
2+
3+
import (
4+
"context"
5+
"errors"
6+
"math/big"
7+
"testing"
8+
"time"
9+
10+
"github.com/stretchr/testify/require"
11+
"go.uber.org/mock/gomock"
12+
13+
"github.com/iotexproject/go-pkgs/crypto"
14+
"github.com/iotexproject/iotex-core/v2/action"
15+
"github.com/iotexproject/iotex-core/v2/action/protocol"
16+
"github.com/iotexproject/iotex-core/v2/action/protocol/account/accountpb"
17+
accountutil "github.com/iotexproject/iotex-core/v2/action/protocol/account/util"
18+
"github.com/iotexproject/iotex-core/v2/blockchain/genesis"
19+
"github.com/iotexproject/iotex-core/v2/state"
20+
"github.com/iotexproject/iotex-core/v2/test/identityset"
21+
"github.com/iotexproject/iotex-core/v2/test/mock/mock_chainmanager"
22+
)
23+
24+
func TestQueueWorker(t *testing.T) {
25+
r := require.New(t)
26+
ctrl := gomock.NewController(t)
27+
28+
ap, sf, err := newTestActPool(ctrl)
29+
r.NoError(err)
30+
31+
senderKey := identityset.PrivateKey(28)
32+
senderAddrStr := identityset.Address(28).String()
33+
34+
// setup sf mock to return a valid account
35+
sf.EXPECT().State(gomock.Any(), gomock.Any()).DoAndReturn(
36+
func(account *state.Account, opts ...protocol.StateOption) (uint64, error) {
37+
pb := &accountpb.Account{
38+
Nonce: 1, // Confirmed nonce
39+
Balance: "10000000000",
40+
Type: accountpb.AccountType_ZERO_NONCE,
41+
}
42+
account.FromProto(pb)
43+
return 1, nil
44+
}).AnyTimes()
45+
sf.EXPECT().Height().Return(uint64(1), nil).AnyTimes()
46+
47+
jobQueue := make(chan workerJob, 1)
48+
worker := newQueueWorker(ap, jobQueue)
49+
r.NotNil(worker)
50+
// This is a bit of a hack. The actpool allocates workers internally based on sender address.
51+
// For this test, we will replace all workers with our test worker.
52+
for i := 0; i < len(ap.worker); i++ {
53+
ap.worker[i] = worker
54+
}
55+
56+
g := genesis.TestDefault()
57+
bctx := protocol.BlockCtx{}
58+
ctx := protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), g), bctx))
59+
60+
r.NoError(worker.Start())
61+
defer worker.Stop()
62+
63+
// Test Handle
64+
act, err := signedTransfer(senderKey, 1, big.NewInt(100), "io1mflp9m6hcgm2qcghchsdqj3z3adcdw2h72f73h", nil, 100000, big.NewInt(1))
65+
r.NoError(err)
66+
67+
job := workerJob{
68+
ctx: ctx,
69+
act: act,
70+
err: make(chan error, 1),
71+
}
72+
73+
jobQueue <- job
74+
err = <-job.err
75+
r.NoError(err)
76+
77+
// Verify action was added
78+
pNonce, err := ap.GetPendingNonce(senderAddrStr)
79+
r.NoError(err)
80+
r.Equal(uint64(2), pNonce)
81+
}
82+
83+
type testQueueWorker struct {
84+
ctrl *gomock.Controller
85+
ap *actPool
86+
sf *mock_chainmanager.MockStateReader
87+
worker *queueWorker
88+
ctx context.Context
89+
require *require.Assertions
90+
}
91+
92+
func newTestQueueWorker(t *testing.T, mockSetup func(*mock_chainmanager.MockStateReader, *actPool)) *testQueueWorker {
93+
r := require.New(t)
94+
ctrl := gomock.NewController(t)
95+
96+
ap, sf, err := newTestActPool(ctrl)
97+
r.NoError(err)
98+
99+
if mockSetup != nil {
100+
mockSetup(sf, ap)
101+
}
102+
103+
g := genesis.TestDefault()
104+
bctx := protocol.BlockCtx{}
105+
ctx := protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), g), bctx))
106+
107+
return &testQueueWorker{
108+
ctrl: ctrl,
109+
ap: ap,
110+
sf: sf,
111+
worker: newQueueWorker(ap, make(chan workerJob)),
112+
ctx: ctx,
113+
require: r,
114+
}
115+
}
116+
117+
func TestQueueWorker_HandleErrors(t *testing.T) {
118+
senderKey := identityset.PrivateKey(28)
119+
newJob := func(r *require.Assertions, ctx context.Context, nonce uint64, amount *big.Int) workerJob {
120+
act, err := signedTransfer(senderKey, nonce, amount, "io1mflp9m6hcgm2qcghchsdqj3z3adcdw2h72f73h", nil, 100000, big.NewInt(1))
121+
r.NoError(err)
122+
return workerJob{
123+
ctx: ctx,
124+
act: act,
125+
err: make(chan error, 1),
126+
}
127+
}
128+
129+
t.Run("nonce too low", func(t *testing.T) {
130+
test := newTestQueueWorker(t, func(sf *mock_chainmanager.MockStateReader, _ *actPool) {
131+
sf.EXPECT().State(gomock.Any(), gomock.Any()).DoAndReturn(
132+
func(account *state.Account, opts ...protocol.StateOption) (uint64, error) {
133+
pb := &accountpb.Account{
134+
Nonce: 5, // Confirmed nonce
135+
Balance: "10000000000",
136+
Type: accountpb.AccountType_ZERO_NONCE,
137+
}
138+
account.FromProto(pb)
139+
return 1, nil
140+
}).AnyTimes()
141+
})
142+
job := newJob(test.require, test.ctx, 4, big.NewInt(100)) // Action nonce is 4
143+
err := test.worker.Handle(job)
144+
test.require.ErrorIs(err, action.ErrNonceTooLow)
145+
})
146+
147+
t.Run("nonce too high", func(t *testing.T) {
148+
test := newTestQueueWorker(t, func(sf *mock_chainmanager.MockStateReader, ap *actPool) {
149+
sf.EXPECT().State(gomock.Any(), gomock.Any()).DoAndReturn(
150+
func(account *state.Account, opts ...protocol.StateOption) (uint64, error) {
151+
pb := &accountpb.Account{
152+
Nonce: 5, // Confirmed nonce
153+
Balance: "10000000000",
154+
Type: accountpb.AccountType_ZERO_NONCE,
155+
}
156+
account.FromProto(pb)
157+
return 1, nil
158+
}).AnyTimes()
159+
})
160+
job := newJob(test.require, test.ctx, 5+test.ap.cfg.MaxNumActsPerAcct, big.NewInt(100))
161+
err := test.worker.Handle(job)
162+
test.require.ErrorIs(err, action.ErrNonceTooHigh)
163+
})
164+
165+
t.Run("insufficient funds", func(t *testing.T) {
166+
test := newTestQueueWorker(t, func(sf *mock_chainmanager.MockStateReader, _ *actPool) {
167+
sf.EXPECT().State(gomock.Any(), gomock.Any()).DoAndReturn(
168+
func(account *state.Account, opts ...protocol.StateOption) (uint64, error) {
169+
pb := &accountpb.Account{
170+
Nonce: 0,
171+
Balance: "100", // Low balance
172+
Type: accountpb.AccountType_ZERO_NONCE,
173+
}
174+
account.FromProto(pb)
175+
return 1, nil
176+
}).AnyTimes()
177+
})
178+
// Action cost is 100000 (gas) * 1 (gasprice) + 1000 (amount) = 101000
179+
job := newJob(test.require, test.ctx, 1, big.NewInt(1000))
180+
err := test.worker.Handle(job)
181+
test.require.ErrorIs(err, action.ErrInsufficientFunds)
182+
})
183+
184+
t.Run("context canceled", func(t *testing.T) {
185+
test := newTestQueueWorker(t, nil)
186+
ctx, cancel := context.WithCancel(test.ctx)
187+
cancel() // Cancel context immediately
188+
job := newJob(test.require, ctx, 1, big.NewInt(100))
189+
err := test.worker.Handle(job)
190+
test.require.ErrorIs(err, context.Canceled)
191+
})
192+
}
193+
194+
func TestQueueWorker_StateError(t *testing.T) {
195+
stateErr := errors.New("state error")
196+
197+
t.Run("handle state error", func(t *testing.T) {
198+
test := newTestQueueWorker(t, func(sf *mock_chainmanager.MockStateReader, _ *actPool) {
199+
sf.EXPECT().State(gomock.Any(), gomock.Any()).Return(uint64(0), stateErr).Times(1)
200+
})
201+
act, err := signedTransfer(identityset.PrivateKey(28), 1, big.NewInt(100), "io1mflp9m6hcgm2qcghchsdqj3z3adcdw2h72f73h", nil, 100000, big.NewInt(1))
202+
test.require.NoError(err)
203+
job := workerJob{ctx: test.ctx, act: act}
204+
err = test.worker.Handle(job)
205+
test.require.ErrorIs(err, stateErr)
206+
})
207+
208+
t.Run("reset state error", func(t *testing.T) {
209+
senderKey := identityset.PrivateKey(28)
210+
senderAddrStr := identityset.Address(28).String()
211+
test := newTestQueueWorker(t, func(sf *mock_chainmanager.MockStateReader, _ *actPool) {
212+
// Mock a successful state read for the initial add
213+
sf.EXPECT().State(gomock.Any(), gomock.Any()).DoAndReturn(
214+
func(account *state.Account, opts ...protocol.StateOption) (uint64, error) {
215+
pb := &accountpb.Account{
216+
Nonce: 1,
217+
Balance: "10000000000",
218+
Type: accountpb.AccountType_ZERO_NONCE,
219+
}
220+
account.FromProto(pb)
221+
return 1, nil
222+
}).Times(1)
223+
sf.EXPECT().Height().Return(uint64(1), nil).AnyTimes()
224+
// Mock a failed state read for the reset
225+
sf.EXPECT().State(gomock.Any(), gomock.Any()).Return(uint64(0), stateErr).Times(1)
226+
})
227+
228+
act, err := signedTransfer(senderKey, 1, big.NewInt(100), "io1mflp9m6hcgm2qcghchsdqj3z3adcdw2h72f73h", nil, 100000, big.NewInt(1))
229+
test.require.NoError(err)
230+
err = test.worker.Handle(workerJob{ctx: test.ctx, act: act})
231+
test.require.NoError(err)
232+
test.require.False(test.worker.accountActs.Account(senderAddrStr).Empty())
233+
234+
test.worker.Reset(test.ctx)
235+
test.require.True(test.worker.accountActs.Account(senderAddrStr).Empty())
236+
})
237+
}
238+
239+
func TestQueueWorker_EdgeCases(t *testing.T) {
240+
t.Run("replace with overflow", func(t *testing.T) {
241+
test := newTestQueueWorker(t, func(sf *mock_chainmanager.MockStateReader, ap *actPool) {
242+
ap.cfg.MaxNumActsPerPool = 1 // Set pool size to 1
243+
sf.EXPECT().State(gomock.Any(), gomock.Any()).DoAndReturn(
244+
func(account *state.Account, opts ...protocol.StateOption) (uint64, error) {
245+
pb := &accountpb.Account{
246+
Nonce: 1,
247+
Balance: "10000000000",
248+
Type: accountpb.AccountType_ZERO_NONCE,
249+
}
250+
account.FromProto(pb)
251+
return 1, nil
252+
}).AnyTimes()
253+
sf.EXPECT().Height().Return(uint64(1), nil).AnyTimes()
254+
})
255+
256+
senderKey := identityset.PrivateKey(28)
257+
act1, err := signedTransfer(senderKey, 1, big.NewInt(100), "io1mflp9m6hcgm2qcghchsdqj3z3adcdw2h72f73h", nil, 100000, big.NewInt(1))
258+
test.require.NoError(err)
259+
act2, err := signedTransfer(senderKey, 1, big.NewInt(100), "io1mflp9m6hcgm2qcghchsdqj3z3adcdw2h72f73h", nil, 100000, big.NewInt(2))
260+
test.require.NoError(err)
261+
262+
// Add first action
263+
err = test.worker.Handle(workerJob{ctx: test.ctx, act: act1})
264+
test.require.NoError(err)
265+
266+
// Replace with second action, causing overflow
267+
job := workerJob{ctx: test.ctx, act: act2, rep: true}
268+
err = test.worker.Handle(job)
269+
test.require.ErrorIs(err, action.ErrTxPoolOverflow)
270+
})
271+
272+
t.Run("pending actions with timeout", func(t *testing.T) {
273+
test := newTestQueueWorker(t, func(sf *mock_chainmanager.MockStateReader, ap *actPool) {
274+
ap.cfg.ActionExpiry = 1 * time.Millisecond // Set a short expiry
275+
sf.EXPECT().State(gomock.Any(), gomock.Any()).DoAndReturn(
276+
func(account *state.Account, opts ...protocol.StateOption) (uint64, error) {
277+
pb := &accountpb.Account{
278+
Nonce: 1,
279+
Balance: "10000000000",
280+
Type: accountpb.AccountType_ZERO_NONCE,
281+
}
282+
account.FromProto(pb)
283+
return 1, nil
284+
}).AnyTimes()
285+
sf.EXPECT().Height().Return(uint64(1), nil).AnyTimes()
286+
})
287+
288+
// Action with nonce 2, which is greater than the pending nonce 1
289+
act, err := signedTransfer(identityset.PrivateKey(28), 2, big.NewInt(100), "io1mflp9m6hcgm2qcghchsdqj3z3adcdw2h72f73h", nil, 100000, big.NewInt(1))
290+
test.require.NoError(err)
291+
292+
err = test.worker.Handle(workerJob{ctx: test.ctx, act: act})
293+
test.require.NoError(err)
294+
295+
time.Sleep(2 * time.Millisecond) // Wait for action to expire
296+
297+
pending := test.worker.PendingActions(test.ctx)
298+
test.require.Empty(pending)
299+
})
300+
301+
t.Run("all actions for non-existent account", func(t *testing.T) {
302+
test := newTestQueueWorker(t, nil)
303+
addr := identityset.Address(29)
304+
acts, ok := test.worker.AllActions(addr)
305+
test.require.False(ok)
306+
test.require.Nil(acts)
307+
})
308+
309+
t.Run("pending nonce for non-existent account", func(t *testing.T) {
310+
test := newTestQueueWorker(t, nil)
311+
addr := identityset.Address(29)
312+
nonce, ok := test.worker.PendingNonce(addr)
313+
test.require.False(ok)
314+
test.require.Zero(nonce)
315+
})
316+
}
317+
318+
func signedTransfer(
319+
senderKey crypto.PrivateKey,
320+
nonce uint64,
321+
amount *big.Int,
322+
recipient string,
323+
payload []byte,
324+
gasLimit uint64,
325+
gasPrice *big.Int,
326+
) (*action.SealedEnvelope, error) {
327+
transfer := action.NewTransfer(amount, recipient, payload)
328+
bd := &action.EnvelopeBuilder{}
329+
elp := bd.SetNonce(nonce).
330+
SetGasLimit(gasLimit).
331+
SetGasPrice(gasPrice).
332+
SetAction(transfer).
333+
Build()
334+
return action.Sign(elp, senderKey)
335+
}
336+
337+
func newTestActPool(ctrl *gomock.Controller) (*actPool, *mock_chainmanager.MockStateReader, error) {
338+
cfg := DefaultConfig
339+
cfg.ActionExpiry = 10 * time.Second
340+
sf := mock_chainmanager.NewMockStateReader(ctrl)
341+
ap, err := NewActPool(genesis.TestDefault(), sf, cfg)
342+
if err != nil {
343+
return nil, nil, err
344+
}
345+
ap.AddActionEnvelopeValidators(protocol.NewGenericValidator(sf, accountutil.AccountState))
346+
actPool, ok := ap.(*actPool)
347+
if !ok {
348+
panic("wrong type")
349+
}
350+
return actPool, sf, nil
351+
}

0 commit comments

Comments
 (0)