Skip to content

Commit

Permalink
test: add multi store compliance tests
Browse files Browse the repository at this point in the history
And pull out existing multi tests from tmsqlite into the new compliance
test set. In the next commit we are going to cross-store bug, and we can
use the compliance tests to ensure it does not happen on any future
store implementations either.
  • Loading branch information
mark-rushakoff committed Oct 10, 2024
1 parent 7d280b7 commit 27a6ee1
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 46 deletions.
42 changes: 42 additions & 0 deletions tm/tmstore/tmmemstore/memmultistore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package tmmemstore_test

import (
"testing"

"github.com/rollchains/gordian/tm/tmconsensus/tmconsensustest"
"github.com/rollchains/gordian/tm/tmstore/tmmemstore"
"github.com/rollchains/gordian/tm/tmstore/tmstoretest"
)

// MemMultiStore embeds all the tmmemstore types,
// in order to run [tmstoretest.TestMultiStoreCompliance].
// Since all of the store types are intended to act independently,
// we do not anticipate the multi store tests failing for the MemMultiStore.
type MemMultiStore struct {
*tmmemstore.ActionStore
*tmmemstore.CommittedHeaderStore
*tmmemstore.FinalizationStore
*tmmemstore.MirrorStore
*tmmemstore.RoundStore
*tmmemstore.ValidatorStore
}

func NewMemMultiStore() *MemMultiStore {
return &MemMultiStore{
ActionStore: tmmemstore.NewActionStore(),
CommittedHeaderStore: tmmemstore.NewCommittedHeaderStore(),
FinalizationStore: tmmemstore.NewFinalizationStore(),
MirrorStore: tmmemstore.NewMirrorStore(),
RoundStore: tmmemstore.NewRoundStore(),
ValidatorStore: tmmemstore.NewValidatorStore(tmconsensustest.SimpleHashScheme{}),
}
}

func TestMemMultiStoreCompliance(t *testing.T) {
tmstoretest.TestMultiStoreCompliance(
t,
func(cleanup func(func())) (*MemMultiStore, error) {
return NewMemMultiStore(), nil
},
)
}
125 changes: 125 additions & 0 deletions tm/tmstore/tmstoretest/multistorecompliance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package tmstoretest

import (
"context"
"fmt"
"testing"

"github.com/rollchains/gordian/tm/tmconsensus"
"github.com/rollchains/gordian/tm/tmconsensus/tmconsensustest"
"github.com/rollchains/gordian/tm/tmstore"
"github.com/stretchr/testify/require"
)

type MultiStoreFactory[S any] func(cleanup func(func())) (S, error)

// TestMultiStoreCompliance validates inter-store behavior,
// when a single type satisfies multiple store interfaces.
func TestMultiStoreCompliance[S any](
t *testing.T,
f MultiStoreFactory[S],
) {
confirmStoreInterfaces[S](t)

t.Run("ActionStore and CommittedHeaderStore", func(t *testing.T) {
type store interface {
tmstore.ActionStore
tmstore.CommittedHeaderStore
}
var s S
if _, ok := any(s).(store); !ok {
t.Skipf("implementation %T does not satisfy both ActionStore and CommittedHeaderStore", s)
}

t.Run("saving a proposed header action does not appear as a committed header", func(t *testing.T) {
t.Parallel()

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

plain, err := f(t.Cleanup)
require.NoError(t, err)
s := any(plain).(store)

fx := tmconsensustest.NewStandardFixture(2)

ph1 := fx.NextProposedHeader([]byte("app_data_1"), 0)
fx.SignProposal(ctx, &ph1, 0)

require.NoError(t, s.SaveProposedHeaderAction(ctx, ph1))

_, err = s.LoadCommittedHeader(ctx, 1)
require.Error(t, err)
require.ErrorIs(t, err, tmconsensus.HeightUnknownError{Want: 1})
})
})

t.Run("CommittedHeaderStore and RoundStore", func(t *testing.T) {
type store interface {
tmstore.CommittedHeaderStore
tmstore.RoundStore
}
var s S
if _, ok := any(s).(store); !ok {
t.Skipf("implementation %T does not satisfy both CommittedHeaderStore and RoundStore", s)
}

t.Run("saving a round proposed header does not appear as a committed header", func(t *testing.T) {
t.Parallel()

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

plain, err := f(t.Cleanup)
require.NoError(t, err)
s := any(plain).(store)

fx := tmconsensustest.NewStandardFixture(2)

ph1 := fx.NextProposedHeader([]byte("app_data_1"), 0)
fx.SignProposal(ctx, &ph1, 0)

require.NoError(t, s.SaveRoundProposedHeader(ctx, ph1))

_, err = s.LoadCommittedHeader(ctx, 1)
require.Error(t, err)
require.ErrorIs(t, err, tmconsensus.HeightUnknownError{Want: 1})
})
})
}

// confirmStoreInterfaces panics if S satifies less than two store interfaces.
func confirmStoreInterfaces[S any](t *testing.T) {
var s S
var n int

if _, ok := any(s).(tmstore.ActionStore); ok {
n++
}
if _, ok := any(s).(tmstore.CommittedHeaderStore); ok {
n++
}
if _, ok := any(s).(tmstore.FinalizationStore); ok {
n++
}
if _, ok := any(s).(tmstore.MirrorStore); ok {
n++
}
if _, ok := any(s).(tmstore.RoundStore); ok {
n++
}
if _, ok := any(s).(tmstore.ValidatorStore); ok {
n++
}

switch n {
case 0:
panic(fmt.Errorf(
"INVALID: type %T does not satisfy any store interfaces", s,
))
case 1:
panic(fmt.Errorf(
"INVALID: type %T only satisfies one store interface, but need two or more for TestMultiStoreCompliance", s,
))
}
}
56 changes: 10 additions & 46 deletions tmsqlite/tmstore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"testing"

"github.com/rollchains/gordian/gcrypto"
"github.com/rollchains/gordian/tm/tmconsensus"
"github.com/rollchains/gordian/tm/tmconsensus/tmconsensustest"
"github.com/rollchains/gordian/tm/tmstore"
"github.com/rollchains/gordian/tm/tmstore/tmstoretest"
Expand Down Expand Up @@ -141,50 +140,15 @@ func TestValidatorStoreCompliance(t *testing.T) {
})
}

// TODO: all of these tests should move into tmstoretest,
// but first we will need a pattern for testing one value that satisfies multiple interfaces.
func TestMulti(t *testing.T) {
t.Run("proposed header action does not conflict with committed header", func(t *testing.T) {
t.Parallel()

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

s, err := tmsqlite.NewInMemTMStore(ctx, tmconsensustest.SimpleHashScheme{}, &reg)
require.NoError(t, err)
defer s.Close()

fx := tmconsensustest.NewStandardFixture(2)

ph1 := fx.NextProposedHeader([]byte("app_data_1"), 0)
fx.SignProposal(ctx, &ph1, 0)

require.NoError(t, s.SaveProposedHeaderAction(ctx, ph1))

_, err = s.LoadCommittedHeader(ctx, 1)
require.Error(t, err)
require.ErrorIs(t, err, tmconsensus.HeightUnknownError{Want: 1})
})

t.Run("round proposed header does not conflict with committed header", func(t *testing.T) {
t.Parallel()

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

s, err := tmsqlite.NewInMemTMStore(ctx, tmconsensustest.SimpleHashScheme{}, &reg)
require.NoError(t, err)
defer s.Close()

fx := tmconsensustest.NewStandardFixture(2)

ph1 := fx.NextProposedHeader([]byte("app_data_1"), 0)
fx.SignProposal(ctx, &ph1, 0)

require.NoError(t, s.SaveRoundProposedHeader(ctx, ph1))

_, err = s.LoadCommittedHeader(ctx, 1)
require.Error(t, err)
require.ErrorIs(t, err, tmconsensus.HeightUnknownError{Want: 1})
func TestMultiCompliance(t *testing.T) {
tmstoretest.TestMultiStoreCompliance(t, func(cleanup func(func())) (*tmsqlite.TMStore, error) {
s, err := tmsqlite.NewInMemTMStore(context.Background(), tmconsensustest.SimpleHashScheme{}, &reg)
if err != nil {
return nil, err
}
cleanup(func() {
require.NoError(t, s.Close())
})
return s, nil
})
}

0 comments on commit 27a6ee1

Please sign in to comment.