Skip to content

Commit 251c92f

Browse files
committed
test(jsonrpc): avoid different test package setup to interfer with one another
- Added a PostgreSQL advisory lock held for the full test-process lifetime. - Applied it to all three schema-resetting packages: - internal/jsonrpc - internal/repository/postgres - test/validator - JSON-RPC services now clone the handler dispatch table. - Test handlers modify only their service instance, not the package-global map. - Added a regression test proving handler overrides do not leak across services.
1 parent b76b13c commit 251c92f

10 files changed

Lines changed: 126 additions & 18 deletions

File tree

internal/jsonrpc/batchbudget_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func TestBatchListItemLimitNormalizesLimitsLikeHandlers(t *testing.T) {
103103
func TestJSONRPCBatchRejectsListWorkOverLimitBeforeDispatch(t *testing.T) {
104104
s := newBatchTestService()
105105
var calls atomic.Int32
106-
withTestRPCHandler(t, "cartesi_listApplications", func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
106+
withTestRPCHandler(t, s, "cartesi_listApplications", func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
107107
calls.Add(1)
108108
return true, nil
109109
})
@@ -123,7 +123,7 @@ func TestJSONRPCBatchRejectsListWorkOverLimitBeforeDispatch(t *testing.T) {
123123
func TestJSONRPCBatchAllowsListWorkAtLimit(t *testing.T) {
124124
s := newBatchTestService()
125125
var calls atomic.Int32
126-
withTestRPCHandler(t, "cartesi_listApplications", func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
126+
withTestRPCHandler(t, s, "cartesi_listApplications", func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
127127
calls.Add(1)
128128
return true, nil
129129
})

internal/jsonrpc/batchcalls_test.go

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func newBatchTestService() *Service {
4141
Service: service.Service{
4242
Logger: slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil)),
4343
},
44+
handlers: cloneDispatchTable(jsonrpcHandlers),
4445
}
4546
}
4647

@@ -128,7 +129,7 @@ func TestJSONRPCBatchRejectsMoreThanMaximumBeforeDispatch(t *testing.T) {
128129
s := newBatchTestService()
129130
var calls atomic.Int32
130131
const method = "test_batch_cap"
131-
withTestRPCHandler(t, method, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
132+
withTestRPCHandler(t, s, method, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
132133
calls.Add(1)
133134
return true, nil
134135
})
@@ -312,7 +313,7 @@ func TestJSONRPCBatchReplacesResponsesAtCumulativeResponseBudget(t *testing.T) {
312313
var calls atomic.Int32
313314
const method = "test_large_batch_result"
314315
largeResult := strings.Repeat("x", testLargeResultSize)
315-
withTestRPCHandler(t, method, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
316+
withTestRPCHandler(t, s, method, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
316317
calls.Add(1)
317318
return largeResult, nil
318319
})
@@ -351,7 +352,7 @@ func TestJSONRPCBatchStopsBetweenEntriesWhenContextIsCanceled(t *testing.T) {
351352
s.Logger = slog.New(slog.NewJSONHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug}))
352353
ctx, cancel := context.WithCancel(context.Background())
353354
const method = "test_cancel_batch"
354-
withTestRPCHandler(t, method, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
355+
withTestRPCHandler(t, s, method, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
355356
calls.Add(1)
356357
cancel()
357358
return true, nil
@@ -387,7 +388,7 @@ func TestJSONRPCBatchStopsSilentlyWhenRepositoryCallIsCanceled(t *testing.T) {
387388
s.Logger = slog.New(slog.NewJSONHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug}))
388389
ctx, cancel := context.WithCancel(context.Background())
389390
const method = "test_repository_cancel_batch"
390-
withTestRPCHandler(t, method, func(s *Service, _ *http.Request, _ RPCRequest) (any, error) {
391+
withTestRPCHandler(t, s, method, func(s *Service, _ *http.Request, _ RPCRequest) (any, error) {
391392
calls.Add(1)
392393
cancel()
393394
return nil, s.repositoryError("Unable to retrieve test data from repository",
@@ -419,7 +420,7 @@ func TestJSONRPCBatchReturnsErrorsForIDDRequestsAfterDeadline(t *testing.T) {
419420
s := newBatchTestService()
420421
var calls atomic.Int32
421422
const method = "test_deadline_batch"
422-
withTestRPCHandler(t, method, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
423+
withTestRPCHandler(t, s, method, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
423424
calls.Add(1)
424425
return true, nil
425426
})
@@ -462,10 +463,10 @@ func TestJSONRPCBatchRecoversPanicPerEntry(t *testing.T) {
462463
panicMethod = "test_panic_batch"
463464
okMethod = "test_after_panic_batch"
464465
)
465-
withTestRPCHandler(t, panicMethod, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
466+
withTestRPCHandler(t, s, panicMethod, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
466467
panic("test panic")
467468
})
468-
withTestRPCHandler(t, okMethod, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
469+
withTestRPCHandler(t, s, okMethod, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
469470
return "ok", nil
470471
})
471472

@@ -496,7 +497,7 @@ func TestJSONRPCBatchUsesOneAdmissionPermit(t *testing.T) {
496497
}
497498
var nestedAcquisitions atomic.Int32
498499
const method = "test_batch_admission"
499-
withTestRPCHandler(t, method, func(s *Service, _ *http.Request, _ RPCRequest) (any, error) {
500+
withTestRPCHandler(t, s, method, func(s *Service, _ *http.Request, _ RPCRequest) (any, error) {
500501
if s.admission.TryAcquire() {
501502
nestedAcquisitions.Add(1)
502503
s.admission.Release()
@@ -578,15 +579,31 @@ func TestJSONRPCBatchMethodLoggingIsTruncated(t *testing.T) {
578579
require.True(t, found)
579580
}
580581

581-
func withTestRPCHandler(t *testing.T, method string, handler rpcHandler) {
582+
func withTestRPCHandler(t *testing.T, service *Service, method string, handler rpcHandler) {
582583
t.Helper()
583-
previous, existed := jsonrpcHandlers[method]
584-
jsonrpcHandlers[method] = handler
584+
previous, existed := service.handlers[method]
585+
service.handlers[method] = handler
585586
t.Cleanup(func() {
586587
if existed {
587-
jsonrpcHandlers[method] = previous
588+
service.handlers[method] = previous
588589
} else {
589-
delete(jsonrpcHandlers, method)
590+
delete(service.handlers, method)
590591
}
591592
})
592593
}
594+
595+
func TestRPCHandlerOverridesAreServiceLocal(t *testing.T) {
596+
first := newBatchTestService()
597+
second := newBatchTestService()
598+
const method = "test_service_local_handler"
599+
withTestRPCHandler(t, first, method, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
600+
return true, nil
601+
})
602+
603+
_, firstHasHandler := first.handlers[method]
604+
_, secondHasHandler := second.handlers[method]
605+
_, globalHasHandler := jsonrpcHandlers[method]
606+
require.True(t, firstHasHandler)
607+
require.False(t, secondHasHandler)
608+
require.False(t, globalHasHandler)
609+
}

internal/jsonrpc/jsonrpc.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ const (
7474
type rpcHandler = func(*Service, *http.Request, RPCRequest) (any, error)
7575
type dispatchTable = map[string]rpcHandler
7676

77+
func cloneDispatchTable(source dispatchTable) dispatchTable {
78+
clone := make(dispatchTable, len(source))
79+
for method, handler := range source {
80+
clone[method] = handler
81+
}
82+
return clone
83+
}
84+
7785
var jsonrpcHandlers = dispatchTable{
7886
"rpc.discover": handleDiscover,
7987
"cartesi_listApplications": handleListApplications,
@@ -212,7 +220,7 @@ func (s *Service) handleRequest(w io.Writer, r *http.Request, req RPCRequest) er
212220
if req.JSONRPC != "2.0" || req.Method == "" {
213221
return writeRPCError(w, req.ID, JSONRPC_INVALID_REQUEST, "invalid request")
214222
}
215-
fn, ok := jsonrpcHandlers[req.Method]
223+
fn, ok := s.handlers[req.Method]
216224
if !ok {
217225
s.Logger.Debug("RPC method not found", "method", truncatedMethod(req.Method))
218226
return writeRPCError(w, req.ID, JSONRPC_METHOD_NOT_FOUND, "Method not found")

internal/jsonrpc/jsonrpc_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func TestJSONRPCSingleRequestReplacesResponseAtResponseBudget(t *testing.T) {
8686
const method = "test_large_single_result"
8787
largeResult := strings.Repeat("x", MAX_RESPONSE_SIZE)
8888
var called bool
89-
withTestRPCHandler(t, method, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
89+
withTestRPCHandler(t, s, method, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) {
9090
called = true
9191
return largeResult, nil
9292
})

internal/jsonrpc/main_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// (c) Cartesi and individual authors (see AUTHORS)
2+
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)
3+
4+
package jsonrpc
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"os"
10+
"testing"
11+
12+
"github.com/cartesi/rollups-node/test/tooling/db"
13+
)
14+
15+
func TestMain(m *testing.M) {
16+
endpoint, err := db.GetTestDatabaseEndpoint()
17+
if err != nil {
18+
os.Exit(m.Run())
19+
}
20+
release, err := db.LockTestPostgres(context.Background(), endpoint)
21+
if err != nil {
22+
_, _ = fmt.Fprintln(os.Stderr, err)
23+
os.Exit(1)
24+
}
25+
code := m.Run()
26+
release()
27+
os.Exit(code)
28+
}

internal/jsonrpc/service.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type Service struct {
4343
listen func(network, address string) (net.Listener, error)
4444
// OpenAPI description for JSON-RPC API loaded from 'jsonrpc-discover.json' file
4545
discoverSpec any
46+
handlers dispatchTable
4647
// dispatchTimeout expires requests early enough to serialize a complete
4748
// timeout response before the HTTP server's write deadline.
4849
dispatchTimeout time.Duration
@@ -71,6 +72,7 @@ func Create(ctx context.Context, c *CreateInfo) (*Service, error) {
7172
}
7273

7374
s.repository = c.Repository
75+
s.handlers = cloneDispatchTable(jsonrpcHandlers)
7476
if s.repository == nil {
7577
return nil, fmt.Errorf("repository on validator service Create is nil")
7678
}

internal/jsonrpc/service_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func TestJSONRPC_ServerHandlerAppliesBatchDispatchTimeout(t *testing.T) {
5959
s.dispatchTimeout = 10 * time.Millisecond
6060

6161
const method = "test_server_dispatch_timeout"
62-
withTestRPCHandler(t, method, func(_ *Service, r *http.Request, _ RPCRequest) (any, error) {
62+
withTestRPCHandler(t, s, method, func(_ *Service, r *http.Request, _ RPCRequest) (any, error) {
6363
<-r.Context().Done()
6464
return true, nil
6565
})

internal/repository/postgres/postgres_repo_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ package postgres_test
55

66
import (
77
"context"
8+
"fmt"
9+
"os"
810
"testing"
911

1012
"github.com/cartesi/rollups-node/internal/repository"
@@ -14,6 +16,21 @@ import (
1416
"github.com/stretchr/testify/require"
1517
)
1618

19+
func TestMain(m *testing.M) {
20+
endpoint, err := db.GetTestDatabaseEndpoint()
21+
if err != nil {
22+
os.Exit(m.Run())
23+
}
24+
release, err := db.LockTestPostgres(context.Background(), endpoint)
25+
if err != nil {
26+
_, _ = fmt.Fprintln(os.Stderr, err)
27+
os.Exit(1)
28+
}
29+
code := m.Run()
30+
release()
31+
os.Exit(code)
32+
}
33+
1734
func TestPostgresRepository(t *testing.T) {
1835
endpoint, err := db.GetTestDatabaseEndpoint()
1936
if err != nil {

test/tooling/db/db.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44
package db
55

66
import (
7+
"context"
78
"fmt"
89
"os"
910

1011
"github.com/cartesi/rollups-node/internal/repository/postgres/schema"
12+
"github.com/jackc/pgx/v5"
1113
)
1214

15+
const testDatabaseLockID int64 = 0x4352545349544553 // "CRTSITES"
16+
1317
func GetTestDatabaseEndpoint() (string, error) {
1418
endpoint, ok := os.LookupEnv("CARTESI_TEST_DATABASE_CONNECTION")
1519
if !ok {
@@ -18,6 +22,21 @@ func GetTestDatabaseEndpoint() (string, error) {
1822
return endpoint, nil
1923
}
2024

25+
// LockTestPostgres serializes package test processes that reset the shared test
26+
// schema. The session-level advisory lock is held until the returned connection
27+
// closer is called.
28+
func LockTestPostgres(ctx context.Context, endpoint string) (func(), error) {
29+
conn, err := pgx.Connect(ctx, endpoint)
30+
if err != nil {
31+
return nil, fmt.Errorf("failed to connect for test database lock: %w", err)
32+
}
33+
if _, err := conn.Exec(ctx, "SELECT pg_advisory_lock($1)", testDatabaseLockID); err != nil {
34+
_ = conn.Close(context.Background())
35+
return nil, fmt.Errorf("failed to lock test database: %w", err)
36+
}
37+
return func() { _ = conn.Close(context.Background()) }, nil
38+
}
39+
2140
func SetupTestPostgres(endpoint string) error {
2241

2342
schema, err := schema.New(endpoint)

test/validator/validator_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ package validator
55

66
import (
77
"context"
8+
"fmt"
89
"log/slog"
910
"math/big"
11+
"os"
1012
"testing"
1113
"time"
1214

@@ -29,6 +31,21 @@ const MAX_OUTPUT_TREE_HEIGHT = merkle.TREE_DEPTH //nolint: revive
2931

3032
const testTimeout = 300 * time.Second
3133

34+
func TestMain(m *testing.M) {
35+
endpoint, err := db.GetTestDatabaseEndpoint()
36+
if err != nil {
37+
os.Exit(m.Run())
38+
}
39+
release, err := db.LockTestPostgres(context.Background(), endpoint)
40+
if err != nil {
41+
_, _ = fmt.Fprintln(os.Stderr, err)
42+
os.Exit(1)
43+
}
44+
code := m.Run()
45+
release()
46+
os.Exit(code)
47+
}
48+
3249
type ValidatorRepositoryIntegrationSuite struct {
3350
suite.Suite
3451
ctx context.Context

0 commit comments

Comments
 (0)