Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tests: Consistent resource/account struct tests #4669

Merged
merged 5 commits into from
Oct 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 33 additions & 132 deletions data/basics/fields_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,133 +17,38 @@
package basics_test

import (
"fmt"
"reflect"
"strings"
"testing"

"github.com/algorand/go-algorand/data/basics"
"github.com/algorand/go-algorand/data/bookkeeping"
"github.com/algorand/go-algorand/test/partitiontest"
"github.com/stretchr/testify/require"
"github.com/algorand/go-algorand/test/reflectionhelpers"
)

type typePath []string

func (p typePath) addMapKey() typePath {
return append(p, "map_key")
}

func (p typePath) addValue() typePath {
return append(p, "value")
}

func (p typePath) addField(fieldName string) typePath {
return append(p, "field "+fieldName)
}

func (p typePath) validatePathFrom(t reflect.Type) error {
if len(p) == 0 {
// path is empty, so it's vacuously valid
return nil
}

value := p[0]
switch {
case value == "map_key":
return p[1:].validatePathFrom(t.Key())
case value == "value":
return p[1:].validatePathFrom(t.Elem())
case strings.HasPrefix(value, "field "):
fieldName := value[len("field "):]
fieldType, ok := t.FieldByName(fieldName)
if !ok {
return fmt.Errorf("Type '%s' does not have the field '%s'", t.Name(), fieldName)
}
return p[1:].validatePathFrom(fieldType.Type)
default:
return fmt.Errorf("Unexpected item in path: %s", value)
}
}

func (p typePath) Equals(other typePath) bool {
if len(p) != len(other) {
return false
}
for i := range p {
if p[i] != other[i] {
return false
}
}
return true
}

func (p typePath) String() string {
return strings.Join(p, "->")
}

func checkReferencedTypes(seen map[reflect.Type]bool, path typePath, typeStack []reflect.Type, check func(path typePath, stack []reflect.Type) bool) {
currentType := typeStack[len(typeStack)-1]

if _, seenType := seen[currentType]; seenType {
return
}

if !check(path, typeStack) {
// if currentType is not ok, don't visit its children
return
}

// add currentType to seen set, to avoid infinite recursion if currentType references itself
seen[currentType] = true

// after currentType's children are visited, "forget" the type, so we can examine it again if needed
// if this didn't happen, only 1 error per invalid type would get reported
defer delete(seen, currentType)

switch currentType.Kind() {
case reflect.Map:
newPath := path.addMapKey()
newStack := append(typeStack, currentType.Key())
checkReferencedTypes(seen, newPath, newStack, check)
fallthrough
case reflect.Array, reflect.Slice, reflect.Ptr:
newPath := path.addValue()
newStack := append(typeStack, currentType.Elem())
checkReferencedTypes(seen, newPath, newStack, check)
case reflect.Struct:
for i := 0; i < currentType.NumField(); i++ {
field := currentType.Field(i)
newPath := path.addField(field.Name)
newStack := append(typeStack, field.Type)
checkReferencedTypes(seen, newPath, newStack, check)
}
}
}

func makeTypeCheckFunction(t *testing.T, exceptions []typePath, startType reflect.Type) func(path typePath, stack []reflect.Type) bool {
func makeTypeCheckFunction(t *testing.T, exceptions []reflectionhelpers.TypePath, startType reflect.Type) reflectionhelpers.ReferencedTypesIterationAction {
for _, exception := range exceptions {
err := exception.validatePathFrom(startType)
require.NoError(t, err)
// ensure all exceptions can resolve without panicking
exception.ResolveType(startType)
}

return func(path typePath, stack []reflect.Type) bool {
return func(path reflectionhelpers.TypePath, stack []reflect.Type) bool {
currentType := stack[len(stack)-1]

for _, exception := range exceptions {
if path.Equals(exception) {
t.Logf("Skipping exception for path: %s", path.String())
t.Logf("Skipping exception for path: %s", path)
return true
}
}

switch currentType.Kind() {
case reflect.String:
t.Errorf("Invalid string type referenced from %s. Use []byte instead. Full path: %s", startType.Name(), path.String())
t.Errorf("Invalid string type referenced from %v. Use []byte instead. Full path: %s", startType, path)
return false
case reflect.Chan, reflect.Func, reflect.Interface, reflect.UnsafePointer:
// raise an error if one of these strange types is referenced too
t.Errorf("Invalid type %s referenced from %s. Full path: %s", currentType.Name(), startType.Name(), path.String())
t.Errorf("Invalid type %v referenced from %v. Full path: %s", currentType, startType, path)
return false
default:
return true
Expand All @@ -157,26 +62,24 @@ func TestBlockFields(t *testing.T) {
typeToCheck := reflect.TypeOf(bookkeeping.Block{})

// These exceptions are for pre-existing usages of string. Only add to this list if you really need to use string.
exceptions := []typePath{
typePath{}.addField("BlockHeader").addField("GenesisID"),
typePath{}.addField("BlockHeader").addField("UpgradeState").addField("CurrentProtocol"),
typePath{}.addField("BlockHeader").addField("UpgradeState").addField("NextProtocol"),
typePath{}.addField("BlockHeader").addField("UpgradeVote").addField("UpgradePropose"),
typePath{}.addField("Payset").addValue().addField("SignedTxnWithAD").addField("SignedTxn").addField("Txn").addField("Type"),
typePath{}.addField("Payset").addValue().addField("SignedTxnWithAD").addField("SignedTxn").addField("Txn").addField("Header").addField("GenesisID"),
typePath{}.addField("Payset").addValue().addField("SignedTxnWithAD").addField("SignedTxn").addField("Txn").addField("AssetConfigTxnFields").addField("AssetParams").addField("UnitName"),
typePath{}.addField("Payset").addValue().addField("SignedTxnWithAD").addField("SignedTxn").addField("Txn").addField("AssetConfigTxnFields").addField("AssetParams").addField("AssetName"),
typePath{}.addField("Payset").addValue().addField("SignedTxnWithAD").addField("SignedTxn").addField("Txn").addField("AssetConfigTxnFields").addField("AssetParams").addField("URL"),
typePath{}.addField("Payset").addValue().addField("SignedTxnWithAD").addField("ApplyData").addField("EvalDelta").addField("GlobalDelta").addMapKey(),
typePath{}.addField("Payset").addValue().addField("SignedTxnWithAD").addField("ApplyData").addField("EvalDelta").addField("GlobalDelta").addValue().addField("Bytes"),
typePath{}.addField("Payset").addValue().addField("SignedTxnWithAD").addField("ApplyData").addField("EvalDelta").addField("LocalDeltas").addValue().addMapKey(),
typePath{}.addField("Payset").addValue().addField("SignedTxnWithAD").addField("ApplyData").addField("EvalDelta").addField("LocalDeltas").addValue().addValue().addField("Bytes"),
typePath{}.addField("Payset").addValue().addField("SignedTxnWithAD").addField("ApplyData").addField("EvalDelta").addField("Logs").addValue(),
exceptions := []reflectionhelpers.TypePath{
reflectionhelpers.TypePath{}.AddField("BlockHeader").AddField("GenesisID"),
reflectionhelpers.TypePath{}.AddField("BlockHeader").AddField("UpgradeState").AddField("CurrentProtocol"),
reflectionhelpers.TypePath{}.AddField("BlockHeader").AddField("UpgradeState").AddField("NextProtocol"),
reflectionhelpers.TypePath{}.AddField("BlockHeader").AddField("UpgradeVote").AddField("UpgradePropose"),
reflectionhelpers.TypePath{}.AddField("Payset").AddValue().AddField("SignedTxnWithAD").AddField("SignedTxn").AddField("Txn").AddField("Type"),
reflectionhelpers.TypePath{}.AddField("Payset").AddValue().AddField("SignedTxnWithAD").AddField("SignedTxn").AddField("Txn").AddField("Header").AddField("GenesisID"),
reflectionhelpers.TypePath{}.AddField("Payset").AddValue().AddField("SignedTxnWithAD").AddField("SignedTxn").AddField("Txn").AddField("AssetConfigTxnFields").AddField("AssetParams").AddField("UnitName"),
reflectionhelpers.TypePath{}.AddField("Payset").AddValue().AddField("SignedTxnWithAD").AddField("SignedTxn").AddField("Txn").AddField("AssetConfigTxnFields").AddField("AssetParams").AddField("AssetName"),
reflectionhelpers.TypePath{}.AddField("Payset").AddValue().AddField("SignedTxnWithAD").AddField("SignedTxn").AddField("Txn").AddField("AssetConfigTxnFields").AddField("AssetParams").AddField("URL"),
reflectionhelpers.TypePath{}.AddField("Payset").AddValue().AddField("SignedTxnWithAD").AddField("ApplyData").AddField("EvalDelta").AddField("GlobalDelta").AddMapKey(),
reflectionhelpers.TypePath{}.AddField("Payset").AddValue().AddField("SignedTxnWithAD").AddField("ApplyData").AddField("EvalDelta").AddField("GlobalDelta").AddValue().AddField("Bytes"),
reflectionhelpers.TypePath{}.AddField("Payset").AddValue().AddField("SignedTxnWithAD").AddField("ApplyData").AddField("EvalDelta").AddField("LocalDeltas").AddValue().AddMapKey(),
reflectionhelpers.TypePath{}.AddField("Payset").AddValue().AddField("SignedTxnWithAD").AddField("ApplyData").AddField("EvalDelta").AddField("LocalDeltas").AddValue().AddValue().AddField("Bytes"),
reflectionhelpers.TypePath{}.AddField("Payset").AddValue().AddField("SignedTxnWithAD").AddField("ApplyData").AddField("EvalDelta").AddField("Logs").AddValue(),
}

seen := make(map[reflect.Type]bool)

checkReferencedTypes(seen, nil, []reflect.Type{typeToCheck}, makeTypeCheckFunction(t, exceptions, typeToCheck))
reflectionhelpers.IterateReferencedTypes(typeToCheck, makeTypeCheckFunction(t, exceptions, typeToCheck))
}

func TestAccountDataFields(t *testing.T) {
Expand All @@ -185,17 +88,15 @@ func TestAccountDataFields(t *testing.T) {
typeToCheck := reflect.TypeOf(basics.AccountData{})

// These exceptions are for pre-existing usages of string. Only add to this list if you really need to use string.
exceptions := []typePath{
typePath{}.addField("AssetParams").addValue().addField("UnitName"),
typePath{}.addField("AssetParams").addValue().addField("AssetName"),
typePath{}.addField("AssetParams").addValue().addField("URL"),
typePath{}.addField("AppLocalStates").addValue().addField("KeyValue").addMapKey(),
typePath{}.addField("AppLocalStates").addValue().addField("KeyValue").addValue().addField("Bytes"),
typePath{}.addField("AppParams").addValue().addField("GlobalState").addMapKey(),
typePath{}.addField("AppParams").addValue().addField("GlobalState").addValue().addField("Bytes"),
exceptions := []reflectionhelpers.TypePath{
reflectionhelpers.TypePath{}.AddField("AssetParams").AddValue().AddField("UnitName"),
reflectionhelpers.TypePath{}.AddField("AssetParams").AddValue().AddField("AssetName"),
reflectionhelpers.TypePath{}.AddField("AssetParams").AddValue().AddField("URL"),
reflectionhelpers.TypePath{}.AddField("AppLocalStates").AddValue().AddField("KeyValue").AddMapKey(),
reflectionhelpers.TypePath{}.AddField("AppLocalStates").AddValue().AddField("KeyValue").AddValue().AddField("Bytes"),
reflectionhelpers.TypePath{}.AddField("AppParams").AddValue().AddField("GlobalState").AddMapKey(),
reflectionhelpers.TypePath{}.AddField("AppParams").AddValue().AddField("GlobalState").AddValue().AddField("Bytes"),
}

seen := make(map[reflect.Type]bool)

checkReferencedTypes(seen, nil, []reflect.Type{typeToCheck}, makeTypeCheckFunction(t, exceptions, typeToCheck))
reflectionhelpers.IterateReferencedTypes(typeToCheck, makeTypeCheckFunction(t, exceptions, typeToCheck))
}
177 changes: 177 additions & 0 deletions ledger/accountdb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2131,6 +2131,183 @@ func TestResourcesDataSetData(t *testing.T) {
}
}

// TestResourceDataRoundtripConversion ensures that basics.AppLocalState, basics.AppParams,
// basics.AssetHolding, and basics.AssetParams can be converted to resourcesData and back without
// losing any data. It uses reflection to be sure that no new fields are omitted.
//
// In other words, this test makes sure any new fields in basics.AppLocalState, basics.AppParams,
// basics.AssetHolding, or basics.AssetParam also get added to resourcesData.
func TestResourceDataRoundtripConversion(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()

t.Run("basics.AppLocalState", func(t *testing.T) {
t.Parallel()
for i := 0; i < 1000; i++ {
randObj, _ := protocol.RandomizeObject(&basics.AppLocalState{})
basicsAppLocalState := *randObj.(*basics.AppLocalState)

var data resourcesData
data.SetAppLocalState(basicsAppLocalState)
roundTripAppLocalState := data.GetAppLocalState()

require.Equal(t, basicsAppLocalState, roundTripAppLocalState)
}
})

t.Run("basics.AppParams", func(t *testing.T) {
t.Parallel()
for i := 0; i < 1000; i++ {
randObj, _ := protocol.RandomizeObject(&basics.AppParams{})
basicsAppParams := *randObj.(*basics.AppParams)

for _, haveHoldings := range []bool{true, false} {
var data resourcesData
data.SetAppParams(basicsAppParams, haveHoldings)
roundTripAppParams := data.GetAppParams()

require.Equal(t, basicsAppParams, roundTripAppParams)
}
}
})

t.Run("basics.AssetHolding", func(t *testing.T) {
t.Parallel()
for i := 0; i < 1000; i++ {
randObj, _ := protocol.RandomizeObject(&basics.AssetHolding{})
basicsAssetHolding := *randObj.(*basics.AssetHolding)

var data resourcesData
data.SetAssetHolding(basicsAssetHolding)
roundTripAssetHolding := data.GetAssetHolding()

require.Equal(t, basicsAssetHolding, roundTripAssetHolding)
}
})

t.Run("basics.AssetParams", func(t *testing.T) {
t.Parallel()
for i := 0; i < 1000; i++ {
randObj, _ := protocol.RandomizeObject(&basics.AssetParams{})
basicsAssetParams := *randObj.(*basics.AssetParams)

for _, haveHoldings := range []bool{true, false} {
var data resourcesData
data.SetAssetParams(basicsAssetParams, haveHoldings)
roundTripAssetParams := data.GetAssetParams()

require.Equal(t, basicsAssetParams, roundTripAssetParams)
}
}
})
}

// TestBaseAccountDataRoundtripConversion ensures that baseAccountData can be converted to
// ledgercore.AccountData and basics.AccountData and back without losing any data. It uses
// reflection to be sure that no new fields are omitted.
//
// In other words, this test makes sure any new fields in baseAccountData also get added to
// ledgercore.AccountData and basics.AccountData. You should add a manual override in this test if
// the field really only belongs in baseAccountData.
func TestBaseAccountDataRoundtripConversion(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()

t.Run("ledgercore.AccountData", func(t *testing.T) {
t.Parallel()
for i := 0; i < 1000; i++ {
randObj, _ := protocol.RandomizeObject(&baseAccountData{})
baseAccount := *randObj.(*baseAccountData)

ledgercoreAccount := baseAccount.GetLedgerCoreAccountData()
var roundTripAccount baseAccountData
roundTripAccount.SetCoreAccountData(&ledgercoreAccount)

// Manually set UpdateRound, since it is lost in GetLedgerCoreAccountData
roundTripAccount.UpdateRound = baseAccount.UpdateRound

require.Equal(t, baseAccount, roundTripAccount)
}
})

t.Run("basics.AccountData", func(t *testing.T) {
t.Parallel()
for i := 0; i < 1000; i++ {
randObj, _ := protocol.RandomizeObject(&baseAccountData{})
baseAccount := *randObj.(*baseAccountData)

basicsAccount := baseAccount.GetAccountData()
var roundTripAccount baseAccountData
roundTripAccount.SetAccountData(&basicsAccount)

// Manually set UpdateRound, since it is lost in GetAccountData
roundTripAccount.UpdateRound = baseAccount.UpdateRound

// Manually set resources, since resource information is lost in GetAccountData
roundTripAccount.TotalAssetParams = baseAccount.TotalAssetParams
roundTripAccount.TotalAssets = baseAccount.TotalAssets
roundTripAccount.TotalAppLocalStates = baseAccount.TotalAppLocalStates
roundTripAccount.TotalAppParams = baseAccount.TotalAppParams

require.Equal(t, baseAccount, roundTripAccount)
}
})
}

// TestBasicsAccountDataRoundtripConversion ensures that basics.AccountData can be converted to
// baseAccountData and back without losing any data. It uses reflection to be sure that this test is
// always up-to-date with new fields.
//
// In other words, this test makes sure any new fields in basics.AccountData also get added to
// baseAccountData.
func TestBasicsAccountDataRoundtripConversion(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()

for i := 0; i < 1000; i++ {
randObj, _ := protocol.RandomizeObject(&basics.AccountData{})
basicsAccount := *randObj.(*basics.AccountData)

var baseAccount baseAccountData
baseAccount.SetAccountData(&basicsAccount)
roundTripAccount := baseAccount.GetAccountData()

// Manually set resources, since GetAccountData doesn't attempt to restore them
roundTripAccount.AssetParams = basicsAccount.AssetParams
roundTripAccount.Assets = basicsAccount.Assets
roundTripAccount.AppLocalStates = basicsAccount.AppLocalStates
roundTripAccount.AppParams = basicsAccount.AppParams

require.Equal(t, basicsAccount, roundTripAccount)
require.Equal(t, uint64(len(roundTripAccount.AssetParams)), baseAccount.TotalAssetParams)
require.Equal(t, uint64(len(roundTripAccount.Assets)), baseAccount.TotalAssets)
require.Equal(t, uint64(len(roundTripAccount.AppLocalStates)), baseAccount.TotalAppLocalStates)
require.Equal(t, uint64(len(roundTripAccount.AppParams)), baseAccount.TotalAppParams)
}
}

// TestLedgercoreAccountDataRoundtripConversion ensures that ledgercore.AccountData can be converted
// to baseAccountData and back without losing any data. It uses reflection to be sure that no new
// fields are omitted.
//
// In other words, this test makes sure any new fields in ledgercore.AccountData also get added to
// baseAccountData.
func TestLedgercoreAccountDataRoundtripConversion(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()

for i := 0; i < 1000; i++ {
randObj, _ := protocol.RandomizeObject(&ledgercore.AccountData{})
ledgercoreAccount := *randObj.(*ledgercore.AccountData)

var baseAccount baseAccountData
baseAccount.SetCoreAccountData(&ledgercoreAccount)
roundTripAccount := baseAccount.GetLedgerCoreAccountData()

require.Equal(t, ledgercoreAccount, roundTripAccount)
}
}

func TestBaseAccountDataIsEmpty(t *testing.T) {
partitiontest.PartitionTest(t)
positiveTesting := func(t *testing.T) {
Expand Down
Loading