From d42618c4f1ce36bc94fda3357952191025d8f3da Mon Sep 17 00:00:00 2001
From: cce <51567+cce@users.noreply.github.com>
Date: Thu, 6 Nov 2025 09:57:21 -0500
Subject: [PATCH 1/7] update roundtrip.Check with more features and use in a
bunch of places
---
crypto/onetimesig_test.go | 14 ++
daemon/algod/api/server/v2/account_test.go | 132 ++++++++++++++++--
daemon/algod/api/server/v2/dryrun_test.go | 25 ++++
data/basics/teal_test.go | 69 ++++++++++
data/basics/testing/copiers.go | 30 ----
data/basics/testing/roundtrip/roundtrip.go | 153 +++++++++++++++++++++
ledger/store/trackerdb/data_test.go | 9 +-
network/msgOfInterest_test.go | 20 +++
8 files changed, 409 insertions(+), 43 deletions(-)
delete mode 100644 data/basics/testing/copiers.go
create mode 100644 data/basics/testing/roundtrip/roundtrip.go
diff --git a/crypto/onetimesig_test.go b/crypto/onetimesig_test.go
index d9eec123d3..323363322a 100644
--- a/crypto/onetimesig_test.go
+++ b/crypto/onetimesig_test.go
@@ -20,6 +20,9 @@ import (
"fmt"
"testing"
+ "github.com/stretchr/testify/require"
+
+ "github.com/algorand/go-algorand/data/basics/testing/roundtrip"
"github.com/algorand/go-algorand/test/partitiontest"
)
@@ -140,6 +143,17 @@ func testOneTimeSignVerifyNewStyle(t *testing.T, c *OneTimeSignatureSecrets, c2
}
}
+func TestHeartbeatProofRoundTrip(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ toOTS := func(h HeartbeatProof) OneTimeSignature { return h.ToOneTimeSignature() }
+ toProof := func(ots OneTimeSignature) HeartbeatProof { return ots.ToHeartbeatProof() }
+
+ // Test with an empty proof as the example, RandomizeObject will generate 100 random variants
+ var emptyProof HeartbeatProof
+ require.True(t, roundtrip.Check(t, emptyProof, toOTS, toProof))
+}
+
func BenchmarkOneTimeSigBatchVerification(b *testing.B) {
for _, enabled := range []bool{false, true} {
b.Run(fmt.Sprintf("batch=%v", enabled), func(b *testing.B) {
diff --git a/daemon/algod/api/server/v2/account_test.go b/daemon/algod/api/server/v2/account_test.go
index e58990ada0..8714f79469 100644
--- a/daemon/algod/api/server/v2/account_test.go
+++ b/daemon/algod/api/server/v2/account_test.go
@@ -25,11 +25,31 @@ import (
"github.com/algorand/go-algorand/config"
"github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/model"
"github.com/algorand/go-algorand/data/basics"
+ "github.com/algorand/go-algorand/data/basics/testing/roundtrip"
ledgertesting "github.com/algorand/go-algorand/ledger/testing"
"github.com/algorand/go-algorand/protocol"
"github.com/algorand/go-algorand/test/partitiontest"
)
+// makeAccountConverters creates conversion functions for round-trip testing between
+// basics.AccountData and model.Account.
+func makeAccountConverters(t *testing.T, addrStr string, round basics.Round, proto *config.ConsensusParams, withoutRewards basics.MicroAlgos) (
+ toModel func(basics.AccountData) model.Account,
+ toBasics func(model.Account) basics.AccountData,
+) {
+ toModel = func(ad basics.AccountData) model.Account {
+ converted, err := AccountDataToAccount(addrStr, &ad, round, proto, withoutRewards)
+ require.NoError(t, err)
+ return converted
+ }
+ toBasics = func(acc model.Account) basics.AccountData {
+ converted, err := AccountToAccountData(&acc)
+ require.NoError(t, err)
+ return converted
+ }
+ return toModel, toBasics
+}
+
func TestAccount(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()
@@ -106,8 +126,9 @@ func TestAccount(t *testing.T) {
b := a.WithUpdatedRewards(proto.RewardUnit, 100)
addr := basics.Address{}.String()
- conv, err := AccountDataToAccount(addr, &b, round, &proto, a.MicroAlgos)
- require.NoError(t, err)
+ toModel, toBasics := makeAccountConverters(t, addr, round, &proto, a.MicroAlgos)
+
+ conv := toModel(b)
require.Equal(t, addr, conv.Address)
require.Equal(t, b.MicroAlgos.Raw, conv.Amount)
require.Equal(t, a.MicroAlgos.Raw, conv.AmountWithoutPendingRewards)
@@ -145,6 +166,21 @@ func TestAccount(t *testing.T) {
verifyCreatedApp(0, appIdx1, appParams1)
verifyCreatedApp(1, appIdx2, appParams2)
+ appRoundTrip := func(idx basics.AppIndex, params basics.AppParams) {
+ require.True(t, roundtrip.Check(t, params,
+ func(ap basics.AppParams) model.Application {
+ return AppParamsToApplication(addr, idx, &ap)
+ },
+ func(app model.Application) basics.AppParams {
+ converted, err := ApplicationParamsToAppParams(&app.Params)
+ require.NoError(t, err)
+ return converted
+ }))
+ }
+
+ appRoundTrip(appIdx1, appParams1)
+ appRoundTrip(appIdx2, appParams2)
+
makeTKV := func(k string, v interface{}) model.TealKeyValue {
value := model.TealValue{}
switch v.(type) {
@@ -198,9 +234,7 @@ func TestAccount(t *testing.T) {
verifyCreatedAsset(0, assetIdx1, assetParams1)
verifyCreatedAsset(1, assetIdx2, assetParams2)
- c, err := AccountToAccountData(&conv)
- require.NoError(t, err)
- require.Equal(t, b, c)
+ require.True(t, roundtrip.Check(t, b, toModel, toBasics))
t.Run("IsDeterministic", func(t *testing.T) {
// convert the same account a few more times to make sure we always
@@ -223,11 +257,91 @@ func TestAccountRandomRoundTrip(t *testing.T) {
for addr, acct := range accts {
round := basics.Round(2)
proto := config.Consensus[protocol.ConsensusFuture]
- conv, err := AccountDataToAccount(addr.String(), &acct, round, &proto, acct.MicroAlgos)
- require.NoError(t, err)
- c, err := AccountToAccountData(&conv)
+ toModel, toBasics := makeAccountConverters(t, addr.String(), round, &proto, acct.MicroAlgos)
+ // AccountData has constraints (Status field must be valid), and this test
+ // already uses RandomAccounts to generate valid random accounts
+ require.True(t, roundtrip.Check(t, acct, toModel, toBasics, roundtrip.NoRandomCases()))
+ }
+ }
+}
+
+func TestConvertTealKeyValueRoundTrip(t *testing.T) {
+ partitiontest.PartitionTest(t)
+ t.Parallel()
+
+ t.Run("nil input", func(t *testing.T) {
+ require.Nil(t, convertTKVToGenerated(nil))
+ result, err := convertGeneratedTKV(nil)
+ require.NoError(t, err)
+ require.Nil(t, result)
+ })
+
+ t.Run("empty map treated as nil", func(t *testing.T) {
+ empty := basics.TealKeyValue{}
+ require.Nil(t, convertTKVToGenerated(&empty))
+ result, err := convertGeneratedTKV(convertTKVToGenerated(&empty))
+ require.NoError(t, err)
+ require.Nil(t, result)
+ })
+
+ t.Run("round-trip non-empty map", func(t *testing.T) {
+ kv := basics.TealKeyValue{
+ "alpha": {Type: basics.TealUintType, Uint: 17},
+ "beta": {Type: basics.TealBytesType, Bytes: "\x00\x01binary"},
+ }
+
+ toGenerated := func(val basics.TealKeyValue) *model.TealKeyValueStore {
+ return convertTKVToGenerated(&val)
+ }
+ toBasics := func(store *model.TealKeyValueStore) basics.TealKeyValue {
+ converted, err := convertGeneratedTKV(store)
require.NoError(t, err)
- require.Equal(t, acct, c)
+ return converted
}
+
+ require.True(t, roundtrip.Check(t, kv, toGenerated, toBasics))
+ })
+}
+
+func TestAppLocalStateRoundTrip(t *testing.T) {
+ partitiontest.PartitionTest(t)
+ t.Parallel()
+
+ appIdx := basics.AppIndex(42)
+ cases := map[string]basics.AppLocalState{
+ "empty kv": {
+ Schema: basics.StateSchema{NumUint: 1, NumByteSlice: 0},
+ KeyValue: nil,
+ },
+ "mixed kv": {
+ Schema: basics.StateSchema{NumUint: 2, NumByteSlice: 3},
+ KeyValue: basics.TealKeyValue{
+ "counter": {Type: basics.TealUintType, Uint: 99},
+ "note": {Type: basics.TealBytesType, Bytes: "hello world"},
+ },
+ },
+ }
+
+ for name, state := range cases {
+ state := state
+ t.Run(name, func(t *testing.T) {
+ modelState := AppLocalState(state, appIdx)
+ modelStates := []model.ApplicationLocalState{modelState}
+
+ acc := model.Account{
+ Status: basics.Offline.String(),
+ Amount: 0,
+ AppsLocalState: &modelStates,
+ }
+
+ ad, err := AccountToAccountData(&acc)
+ require.NoError(t, err)
+
+ require.NotNil(t, ad.AppLocalStates)
+ got, ok := ad.AppLocalStates[appIdx]
+ require.True(t, ok)
+ require.Equal(t, state.Schema, got.Schema)
+ require.Equal(t, state.KeyValue, got.KeyValue)
+ })
}
}
diff --git a/daemon/algod/api/server/v2/dryrun_test.go b/daemon/algod/api/server/v2/dryrun_test.go
index 8bd7addc92..5d0c9cc0bc 100644
--- a/daemon/algod/api/server/v2/dryrun_test.go
+++ b/daemon/algod/api/server/v2/dryrun_test.go
@@ -32,6 +32,7 @@ import (
"github.com/algorand/go-algorand/crypto"
"github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/model"
"github.com/algorand/go-algorand/data/basics"
+ "github.com/algorand/go-algorand/data/basics/testing/roundtrip"
"github.com/algorand/go-algorand/data/transactions"
"github.com/algorand/go-algorand/data/transactions/logic"
"github.com/algorand/go-algorand/data/txntest"
@@ -1087,6 +1088,30 @@ func TestStateDeltaToStateDelta(t *testing.T) {
gsd := globalDeltaToStateDelta(sd)
require.Equal(t, 3, len(gsd))
+ decode := func(msd model.StateDelta) basics.StateDelta {
+ if len(msd) == 0 {
+ return nil
+ }
+ result := make(basics.StateDelta, len(msd))
+ for _, kv := range msd {
+ keyBytes, err := base64.StdEncoding.DecodeString(kv.Key)
+ require.NoError(t, err)
+ vd := basics.ValueDelta{Action: basics.DeltaAction(kv.Value.Action)}
+ if kv.Value.Bytes != nil {
+ decoded, err := base64.StdEncoding.DecodeString(*kv.Value.Bytes)
+ require.NoError(t, err)
+ vd.Bytes = string(decoded)
+ }
+ if kv.Value.Uint != nil {
+ vd.Uint = *kv.Value.Uint
+ }
+ result[string(keyBytes)] = vd
+ }
+ return result
+ }
+
+ require.True(t, roundtrip.Check(t, sd, globalDeltaToStateDelta, decode))
+
var keys []string
// test with a loop because sd is a map and iteration order is random
for _, item := range gsd {
diff --git a/data/basics/teal_test.go b/data/basics/teal_test.go
index 50b0501e49..460cd18e59 100644
--- a/data/basics/teal_test.go
+++ b/data/basics/teal_test.go
@@ -20,10 +20,37 @@ import (
"testing"
"github.com/stretchr/testify/require"
+ "pgregory.net/rapid"
+ "github.com/algorand/go-algorand/data/basics/testing/roundtrip"
"github.com/algorand/go-algorand/test/partitiontest"
)
+// genTealValue generates a valid TealValue with proper Type/field correspondence.
+func genTealValue() *rapid.Generator[TealValue] {
+ return rapid.Custom(func(t *rapid.T) TealValue {
+ tealType := rapid.OneOf(rapid.Just(TealUintType), rapid.Just(TealBytesType)).Draw(t, "type")
+
+ if tealType == TealUintType {
+ return TealValue{Type: TealUintType, Uint: rapid.Uint64().Draw(t, "uint")}
+ }
+ return TealValue{Type: TealBytesType, Bytes: rapid.String().Draw(t, "bytes")}
+ })
+}
+
+// genValueDelta generates a valid ValueDelta with proper Action/field correspondence.
+// Note: DeleteAction is excluded as it doesn't round-trip to TealValue.
+func genValueDelta() *rapid.Generator[ValueDelta] {
+ return rapid.Custom(func(t *rapid.T) ValueDelta {
+ action := rapid.OneOf(rapid.Just(SetUintAction), rapid.Just(SetBytesAction)).Draw(t, "action")
+
+ if action == SetUintAction {
+ return ValueDelta{Action: SetUintAction, Uint: rapid.Uint64().Draw(t, "uint")}
+ }
+ return ValueDelta{Action: SetBytesAction, Bytes: rapid.String().Draw(t, "bytes")}
+ })
+}
+
func TestStateDeltaEqual(t *testing.T) {
partitiontest.PartitionTest(t)
@@ -60,3 +87,45 @@ func TestStateDeltaEqual(t *testing.T) {
d2 = StateDelta{"test": {Action: SetBytesAction, Bytes: "val1"}}
a.False(d1.Equal(d2))
}
+
+func TestTealValueRoundTrip(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ // Test with a simple example value
+ example := TealValue{Type: TealUintType, Uint: 17}
+
+ // Use roundtrip.Check with WithRapid for property-based testing
+ require.True(t, roundtrip.Check(t, example,
+ func(tv TealValue) ValueDelta { return tv.ToValueDelta() },
+ func(vd ValueDelta) TealValue {
+ tv, ok := vd.ToTealValue()
+ require.True(t, ok)
+ return tv
+ },
+ roundtrip.WithRapid(genTealValue())))
+}
+
+func TestValueDeltaRoundTrip(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ // Test with a simple example value
+ example := ValueDelta{Action: SetUintAction, Uint: 42}
+
+ // Use roundtrip.Check with WithRapid for property-based testing
+ require.True(t, roundtrip.Check(t, example,
+ func(vd ValueDelta) TealValue {
+ tv, ok := vd.ToTealValue()
+ require.True(t, ok)
+ return tv
+ },
+ func(tv TealValue) ValueDelta { return tv.ToValueDelta() },
+ roundtrip.WithRapid(genValueDelta())))
+}
+
+func TestValueDeltaDeleteDoesNotRoundTrip(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ vd := ValueDelta{Action: DeleteAction}
+ _, ok := vd.ToTealValue()
+ require.False(t, ok)
+}
diff --git a/data/basics/testing/copiers.go b/data/basics/testing/copiers.go
deleted file mode 100644
index e2df453e6e..0000000000
--- a/data/basics/testing/copiers.go
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) 2019-2025 Algorand, Inc.
-// This file is part of go-algorand
-//
-// go-algorand is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as
-// published by the Free Software Foundation, either version 3 of the
-// License, or (at your option) any later version.
-//
-// go-algorand is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with go-algorand. If not, see .
-
-package testing
-
-import (
- "reflect"
- "testing"
-)
-
-// RoundTrip checks that converting an A -> B -> A gives the original value.
-// Returns true if equal, false otherwise.
-func RoundTrip[A any, B any](t *testing.T, a A, toB func(A) B, toA func(B) A) bool {
- b := toB(a)
- a2 := toA(b)
- return reflect.DeepEqual(a, a2)
-}
diff --git a/data/basics/testing/roundtrip/roundtrip.go b/data/basics/testing/roundtrip/roundtrip.go
new file mode 100644
index 0000000000..e124e48c54
--- /dev/null
+++ b/data/basics/testing/roundtrip/roundtrip.go
@@ -0,0 +1,153 @@
+package roundtrip
+
+import (
+ "reflect"
+ "testing"
+
+ "pgregory.net/rapid"
+
+ "github.com/algorand/go-algorand/protocol"
+)
+
+const defaultRandomCount = 100
+
+// CheckOption configures the behavior of Check.
+type CheckOption interface {
+ apply(*checkConfig)
+}
+
+type checkConfig struct {
+ randomCount *int
+ randomOpts []protocol.RandomizeObjectOption
+ rapidGen interface{} // *rapid.Generator[A], stored as interface{} to avoid type parameters
+ useRapid bool
+}
+
+type randomCountOption int
+
+func (n randomCountOption) apply(cfg *checkConfig) {
+ count := int(n)
+ cfg.randomCount = &count
+}
+
+type randomOptsOption []protocol.RandomizeObjectOption
+
+func (opts randomOptsOption) apply(cfg *checkConfig) {
+ cfg.randomOpts = append(cfg.randomOpts, opts...)
+}
+
+type rapidGenOption struct {
+ gen interface{}
+}
+
+func (r rapidGenOption) apply(cfg *checkConfig) {
+ cfg.rapidGen = r.gen
+ cfg.useRapid = true
+}
+
+// Opts configures round-trip checking behavior.
+// The first argument specifies the number of random test cases to generate.
+// Additional protocol.RandomizeObjectOption arguments can be passed to customize randomization.
+func Opts(count int, opts ...protocol.RandomizeObjectOption) CheckOption {
+ return multiOption{randomCountOption(count), randomOptsOption(opts)}
+}
+
+// NoRandomCases disables random testing, only testing the provided example value.
+// Use this for types with complex constraints or when using custom random generators elsewhere.
+func NoRandomCases() CheckOption {
+ return randomCountOption(0)
+}
+
+// WithRapid specifies a rapid.Generator to use for property-based testing.
+// If provided, rapid.Check will be used instead of protocol.RandomizeObject (runs 100 tests).
+func WithRapid[A any](gen *rapid.Generator[A]) CheckOption {
+ return rapidGenOption{gen: gen}
+}
+
+type multiOption []CheckOption
+
+func (m multiOption) apply(cfg *checkConfig) {
+ for _, opt := range m {
+ opt.apply(cfg)
+ }
+}
+
+// Check verifies that converting from A -> B -> A yields the original value.
+// By default, tests the provided example plus 100 randomly generated values using protocol.RandomizeObject.
+// Use WithRapid to provide a custom rapid.Generator for property-based testing.
+// Use Opts to customize the number of random tests or pass RandomizeObjectOptions.
+func Check[A any, B any](t *testing.T, a A, toB func(A) B, toA func(B) A, opts ...CheckOption) bool {
+ cfg := checkConfig{}
+ for _, opt := range opts {
+ opt.apply(&cfg)
+ }
+
+ // Test the provided example first
+ if !checkOne(t, a, toB, toA) {
+ t.Errorf("Round-trip failed for provided example: %+v", a)
+ return false
+ }
+
+ // Use rapid property testing if generator provided
+ if cfg.useRapid {
+ gen, ok := cfg.rapidGen.(*rapid.Generator[A])
+ if !ok {
+ t.Errorf("Invalid rapid generator type")
+ return false
+ }
+
+ // Run rapid property tests (runs 100 tests by default)
+ // Note: rapid.Check controls the count, not us
+ passed := true
+ rapid.Check(t, func(t1 *rapid.T) {
+ randA := gen.Draw(t1, "value")
+ if !checkOne(t, randA, toB, toA) {
+ t.Errorf("Round-trip failed for rapid-generated value: %+v", randA)
+ passed = false
+ }
+ })
+ return passed
+ }
+
+ // Otherwise use protocol.RandomizeObject
+ randomCount := defaultRandomCount
+ if cfg.randomCount != nil {
+ randomCount = *cfg.randomCount
+ }
+
+ if randomCount > 0 {
+ var template A
+ for i := 0; i < randomCount; i++ {
+ randObj, err := protocol.RandomizeObject(&template, cfg.randomOpts...)
+ if err != nil {
+ t.Logf("Failed to randomize object (variant %d): %v", i, err)
+ continue
+ }
+
+ // Type assert the result back to *A, then dereference
+ randPtr, ok := randObj.(*A)
+ if !ok {
+ t.Errorf("Type assertion failed for random variant %d", i)
+ return false
+ }
+ randA := *randPtr
+
+ if !checkOne(t, randA, toB, toA) {
+ t.Errorf("Round-trip failed for random variant %d: %+v", i, randA)
+ return false
+ }
+ }
+ }
+
+ return true
+}
+
+func checkOne[A any, B any](t *testing.T, a A, toB func(A) B, toA func(B) A) bool {
+ b := toB(a)
+ a2 := toA(b)
+ if !reflect.DeepEqual(a, a2) {
+ t.Logf("Round-trip mismatch:\n Original: %+v\n After: %+v", a, a2)
+ return false
+ }
+ return true
+}
diff --git a/ledger/store/trackerdb/data_test.go b/ledger/store/trackerdb/data_test.go
index 4524169b89..c805ce1955 100644
--- a/ledger/store/trackerdb/data_test.go
+++ b/ledger/store/trackerdb/data_test.go
@@ -27,6 +27,7 @@ import (
"github.com/algorand/go-algorand/crypto"
"github.com/algorand/go-algorand/data/basics"
basics_testing "github.com/algorand/go-algorand/data/basics/testing"
+ "github.com/algorand/go-algorand/data/basics/testing/roundtrip"
"github.com/algorand/go-algorand/ledger/ledgercore"
ledgertesting "github.com/algorand/go-algorand/ledger/testing"
"github.com/algorand/go-algorand/protocol"
@@ -1292,7 +1293,7 @@ func TestCopyFunctions(t *testing.T) {
return rd.GetAssetParams()
}
for _, nz := range basics_testing.NearZeros(t, basics.AssetParams{}) {
- assert.True(t, basics_testing.RoundTrip(t, nz, assetToRD, rdToAsset), nz)
+ assert.True(t, roundtrip.Check(t, nz, assetToRD, rdToAsset), nz)
}
// Asset holdings are copied into and out of ResourceData losslessly
@@ -1305,7 +1306,7 @@ func TestCopyFunctions(t *testing.T) {
return rd.GetAssetHolding()
}
for _, nz := range basics_testing.NearZeros(t, basics.AssetHolding{}) {
- assert.True(t, basics_testing.RoundTrip(t, nz, holdingToRD, rdToHolding), nz)
+ assert.True(t, roundtrip.Check(t, nz, holdingToRD, rdToHolding), nz)
}
// AppParams are copied into and out of ResourceData losslessly
@@ -1318,7 +1319,7 @@ func TestCopyFunctions(t *testing.T) {
return rd.GetAppParams()
}
for _, nz := range basics_testing.NearZeros(t, basics.AppParams{}) {
- assert.True(t, basics_testing.RoundTrip(t, nz, apToRD, rdToAP), nz)
+ assert.True(t, roundtrip.Check(t, nz, apToRD, rdToAP), nz)
}
// AppLocalStates are copied into and out of ResourceData losslessly
@@ -1331,7 +1332,7 @@ func TestCopyFunctions(t *testing.T) {
return rd.GetAppLocalState()
}
for _, nz := range basics_testing.NearZeros(t, basics.AppLocalState{}) {
- assert.True(t, basics_testing.RoundTrip(t, nz, localsToRD, rdToLocals), nz)
+ assert.True(t, roundtrip.Check(t, nz, localsToRD, rdToLocals), nz)
}
}
diff --git a/network/msgOfInterest_test.go b/network/msgOfInterest_test.go
index c596781d64..137a9cf502 100644
--- a/network/msgOfInterest_test.go
+++ b/network/msgOfInterest_test.go
@@ -17,10 +17,12 @@
package network
import (
+ "maps"
"testing"
"github.com/stretchr/testify/require"
+ "github.com/algorand/go-algorand/data/basics/testing/roundtrip"
"github.com/algorand/go-algorand/protocol"
"github.com/algorand/go-algorand/test/partitiontest"
)
@@ -70,3 +72,21 @@ func TestMarshallMessageOfInterest(t *testing.T) {
require.Equal(t, tags[protocol.AgreementVoteTag], true)
require.Equal(t, 1, len(tags))
}
+
+func TestDefaultSendMessageTagsMarshalRoundTrip(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ cloned := maps.Clone(defaultSendMessageTags)
+
+ toBytes := func(tags map[protocol.Tag]bool) []byte {
+ return marshallMessageOfInterestMap(tags)
+ }
+ toTags := func(data []byte) map[protocol.Tag]bool {
+ tags, err := unmarshallMessageOfInterest(data)
+ require.NoError(t, err)
+ return tags
+ }
+
+ // map[protocol.Tag]bool has constraints (Tag must be valid), so disable random testing
+ require.True(t, roundtrip.Check(t, cloned, toBytes, toTags, roundtrip.NoRandomCases()), "default messages of interest should round-trip")
+}
From 0609903720d5c6c073eabf04cb916eb73b54e5ef Mon Sep 17 00:00:00 2001
From: cce <51567+cce@users.noreply.github.com>
Date: Thu, 6 Nov 2025 10:13:56 -0500
Subject: [PATCH 2/7] fix lint
---
daemon/algod/api/server/v2/account_test.go | 1 -
data/basics/testing/roundtrip/roundtrip.go | 16 ++++++++++++++++
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/daemon/algod/api/server/v2/account_test.go b/daemon/algod/api/server/v2/account_test.go
index 8714f79469..4858821c29 100644
--- a/daemon/algod/api/server/v2/account_test.go
+++ b/daemon/algod/api/server/v2/account_test.go
@@ -323,7 +323,6 @@ func TestAppLocalStateRoundTrip(t *testing.T) {
}
for name, state := range cases {
- state := state
t.Run(name, func(t *testing.T) {
modelState := AppLocalState(state, appIdx)
modelStates := []model.ApplicationLocalState{modelState}
diff --git a/data/basics/testing/roundtrip/roundtrip.go b/data/basics/testing/roundtrip/roundtrip.go
index e124e48c54..3c1f521229 100644
--- a/data/basics/testing/roundtrip/roundtrip.go
+++ b/data/basics/testing/roundtrip/roundtrip.go
@@ -1,3 +1,19 @@
+// Copyright (C) 2019-2025 Algorand, Inc.
+// This file is part of go-algorand
+//
+// go-algorand is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// go-algorand is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with go-algorand. If not, see .
+
package roundtrip
import (
From d3afedb615b2bf65c9a5ca489043807c7fff89c5 Mon Sep 17 00:00:00 2001
From: cce <51567+cce@users.noreply.github.com>
Date: Thu, 6 Nov 2025 14:29:55 -0500
Subject: [PATCH 3/7] fix tests
---
daemon/algod/api/server/v2/account_test.go | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/daemon/algod/api/server/v2/account_test.go b/daemon/algod/api/server/v2/account_test.go
index 4858821c29..056746b976 100644
--- a/daemon/algod/api/server/v2/account_test.go
+++ b/daemon/algod/api/server/v2/account_test.go
@@ -234,7 +234,7 @@ func TestAccount(t *testing.T) {
verifyCreatedAsset(0, assetIdx1, assetParams1)
verifyCreatedAsset(1, assetIdx2, assetParams2)
- require.True(t, roundtrip.Check(t, b, toModel, toBasics))
+ require.True(t, roundtrip.Check(t, b, toModel, toBasics, roundtrip.NoRandomCases()))
t.Run("IsDeterministic", func(t *testing.T) {
// convert the same account a few more times to make sure we always
@@ -258,8 +258,6 @@ func TestAccountRandomRoundTrip(t *testing.T) {
round := basics.Round(2)
proto := config.Consensus[protocol.ConsensusFuture]
toModel, toBasics := makeAccountConverters(t, addr.String(), round, &proto, acct.MicroAlgos)
- // AccountData has constraints (Status field must be valid), and this test
- // already uses RandomAccounts to generate valid random accounts
require.True(t, roundtrip.Check(t, acct, toModel, toBasics, roundtrip.NoRandomCases()))
}
}
From d5a16c5da114818176c1c2ca28ba39f992c9e54d Mon Sep 17 00:00:00 2001
From: cce <51567+cce@users.noreply.github.com>
Date: Thu, 6 Nov 2025 16:47:32 -0500
Subject: [PATCH 4/7] incorporate NearZero in roundtrip
---
daemon/algod/api/server/v2/account_test.go | 6 +-
.../testing/{ => roundtrip}/nearzero.go | 2 +-
.../testing/{ => roundtrip}/nearzero_test.go | 2 +-
data/basics/testing/roundtrip/roundtrip.go | 82 +++++++++++++------
data/transactions/application_test.go | 6 +-
data/transactions/transaction_test.go | 8 +-
ledger/store/trackerdb/data_test.go | 20 ++---
network/msgOfInterest_test.go | 2 +-
8 files changed, 74 insertions(+), 54 deletions(-)
rename data/basics/testing/{ => roundtrip}/nearzero.go (99%)
rename data/basics/testing/{ => roundtrip}/nearzero_test.go (99%)
diff --git a/daemon/algod/api/server/v2/account_test.go b/daemon/algod/api/server/v2/account_test.go
index 056746b976..c16881244b 100644
--- a/daemon/algod/api/server/v2/account_test.go
+++ b/daemon/algod/api/server/v2/account_test.go
@@ -234,7 +234,7 @@ func TestAccount(t *testing.T) {
verifyCreatedAsset(0, assetIdx1, assetParams1)
verifyCreatedAsset(1, assetIdx2, assetParams2)
- require.True(t, roundtrip.Check(t, b, toModel, toBasics, roundtrip.NoRandomCases()))
+ require.True(t, roundtrip.Check(t, b, toModel, toBasics, roundtrip.NoRandomCases(), roundtrip.NoNearZeros()))
t.Run("IsDeterministic", func(t *testing.T) {
// convert the same account a few more times to make sure we always
@@ -258,7 +258,7 @@ func TestAccountRandomRoundTrip(t *testing.T) {
round := basics.Round(2)
proto := config.Consensus[protocol.ConsensusFuture]
toModel, toBasics := makeAccountConverters(t, addr.String(), round, &proto, acct.MicroAlgos)
- require.True(t, roundtrip.Check(t, acct, toModel, toBasics, roundtrip.NoRandomCases()))
+ require.True(t, roundtrip.Check(t, acct, toModel, toBasics, roundtrip.NoRandomCases(), roundtrip.NoNearZeros()))
}
}
}
@@ -297,7 +297,7 @@ func TestConvertTealKeyValueRoundTrip(t *testing.T) {
return converted
}
- require.True(t, roundtrip.Check(t, kv, toGenerated, toBasics))
+ require.True(t, roundtrip.Check(t, kv, toGenerated, toBasics, roundtrip.NoNearZeros()))
})
}
diff --git a/data/basics/testing/nearzero.go b/data/basics/testing/roundtrip/nearzero.go
similarity index 99%
rename from data/basics/testing/nearzero.go
rename to data/basics/testing/roundtrip/nearzero.go
index 0482797e94..0607172c70 100644
--- a/data/basics/testing/nearzero.go
+++ b/data/basics/testing/roundtrip/nearzero.go
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with go-algorand. If not, see .
-package testing
+package roundtrip
import (
"reflect"
diff --git a/data/basics/testing/nearzero_test.go b/data/basics/testing/roundtrip/nearzero_test.go
similarity index 99%
rename from data/basics/testing/nearzero_test.go
rename to data/basics/testing/roundtrip/nearzero_test.go
index f1aaf32058..b8c71f1ad0 100644
--- a/data/basics/testing/nearzero_test.go
+++ b/data/basics/testing/roundtrip/nearzero_test.go
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with go-algorand. If not, see .
-package testing
+package roundtrip
import (
"testing"
diff --git a/data/basics/testing/roundtrip/roundtrip.go b/data/basics/testing/roundtrip/roundtrip.go
index 3c1f521229..f618dc0281 100644
--- a/data/basics/testing/roundtrip/roundtrip.go
+++ b/data/basics/testing/roundtrip/roundtrip.go
@@ -33,10 +33,11 @@ type CheckOption interface {
}
type checkConfig struct {
- randomCount *int
- randomOpts []protocol.RandomizeObjectOption
- rapidGen interface{} // *rapid.Generator[A], stored as interface{} to avoid type parameters
- useRapid bool
+ randomCount *int
+ randomOpts []protocol.RandomizeObjectOption
+ rapidGen interface{} // *rapid.Generator[A], stored as interface{} to avoid type parameters
+ useRapid bool
+ skipNearZeros bool
}
type randomCountOption int
@@ -68,12 +69,25 @@ func Opts(count int, opts ...protocol.RandomizeObjectOption) CheckOption {
return multiOption{randomCountOption(count), randomOptsOption(opts)}
}
-// NoRandomCases disables random testing, only testing the provided example value.
-// Use this for types with complex constraints or when using custom random generators elsewhere.
+// NoRandomCases disables RandomizeObject testing (but still runs NearZeros).
+// Use this when RandomizeObject generates invalid values for constrained types.
+// Combine with NoNearZeros() to disable all automatic testing.
func NoRandomCases() CheckOption {
return randomCountOption(0)
}
+type skipNearZerosOption struct{}
+
+func (skipNearZerosOption) apply(cfg *checkConfig) {
+ cfg.skipNearZeros = true
+}
+
+// NoNearZeros disables NearZeros testing, only using RandomizeObject for random variants.
+// Use this for non-struct types (maps, slices) where NearZeros doesn't apply.
+func NoNearZeros() CheckOption {
+ return skipNearZerosOption{}
+}
+
// WithRapid specifies a rapid.Generator to use for property-based testing.
// If provided, rapid.Check will be used instead of protocol.RandomizeObject (runs 100 tests).
func WithRapid[A any](gen *rapid.Generator[A]) CheckOption {
@@ -89,8 +103,11 @@ func (m multiOption) apply(cfg *checkConfig) {
}
// Check verifies that converting from A -> B -> A yields the original value.
-// By default, tests the provided example plus 100 randomly generated values using protocol.RandomizeObject.
+// By default, tests the provided example, all NearZeros variants (one per field),
+// and 100 randomly generated values using protocol.RandomizeObject.
// Use WithRapid to provide a custom rapid.Generator for property-based testing.
+// Use NoRandomCases to disable RandomizeObject (still runs NearZeros).
+// Use NoNearZeros to disable NearZeros (for non-struct types like maps).
// Use Opts to customize the number of random tests or pass RandomizeObjectOptions.
func Check[A any, B any](t *testing.T, a A, toB func(A) B, toA func(B) A, opts ...CheckOption) bool {
cfg := checkConfig{}
@@ -125,33 +142,44 @@ func Check[A any, B any](t *testing.T, a A, toB func(A) B, toA func(B) A, opts .
return passed
}
- // Otherwise use protocol.RandomizeObject
+ // Test NearZeros (one test per field) - comprehensive and deterministic
+ // Skip if explicitly disabled
+ if !cfg.skipNearZeros {
+ nearZeroValues := NearZeros(t, a)
+ for i, nzA := range nearZeroValues {
+ if !checkOne(t, nzA, toB, toA) {
+ t.Errorf("Round-trip failed for NearZero variant %d: %+v", i, nzA)
+ return false
+ }
+ }
+ }
+
+ // Determine random count for RandomizeObject testing
randomCount := defaultRandomCount
if cfg.randomCount != nil {
randomCount = *cfg.randomCount
}
- if randomCount > 0 {
- var template A
- for i := 0; i < randomCount; i++ {
- randObj, err := protocol.RandomizeObject(&template, cfg.randomOpts...)
- if err != nil {
- t.Logf("Failed to randomize object (variant %d): %v", i, err)
- continue
- }
+ // Test with RandomizeObject for additional coverage
+ var template A
+ for i := 0; i < randomCount; i++ {
+ randObj, err := protocol.RandomizeObject(&template, cfg.randomOpts...)
+ if err != nil {
+ t.Logf("Failed to randomize object (variant %d): %v", i, err)
+ continue
+ }
- // Type assert the result back to *A, then dereference
- randPtr, ok := randObj.(*A)
- if !ok {
- t.Errorf("Type assertion failed for random variant %d", i)
- return false
- }
- randA := *randPtr
+ // Type assert the result back to *A, then dereference
+ randPtr, ok := randObj.(*A)
+ if !ok {
+ t.Errorf("Type assertion failed for random variant %d", i)
+ return false
+ }
+ randA := *randPtr
- if !checkOne(t, randA, toB, toA) {
- t.Errorf("Round-trip failed for random variant %d: %+v", i, randA)
- return false
- }
+ if !checkOne(t, randA, toB, toA) {
+ t.Errorf("Round-trip failed for random variant %d: %+v", i, randA)
+ return false
}
}
diff --git a/data/transactions/application_test.go b/data/transactions/application_test.go
index 223c7a4ae8..d48105ce97 100644
--- a/data/transactions/application_test.go
+++ b/data/transactions/application_test.go
@@ -27,7 +27,7 @@ import (
"github.com/algorand/go-algorand/config"
"github.com/algorand/go-algorand/data/basics"
- basics_testing "github.com/algorand/go-algorand/data/basics/testing"
+ "github.com/algorand/go-algorand/data/basics/testing/roundtrip"
"github.com/algorand/go-algorand/protocol"
"github.com/algorand/go-algorand/test/partitiontest"
)
@@ -37,7 +37,7 @@ func TestResourceRefEmpty(t *testing.T) {
t.Parallel()
assert.True(t, ResourceRef{}.Empty())
- for _, rr := range basics_testing.NearZeros(t, ResourceRef{}) {
+ for _, rr := range roundtrip.NearZeros(t, ResourceRef{}) {
assert.False(t, rr.Empty(), "Empty is disregarding a non-zero field in %+v", rr)
}
}
@@ -51,7 +51,7 @@ func TestApplicationCallFieldsEmpty(t *testing.T) {
ac := ApplicationCallTxnFields{}
a.True(ac.Empty())
- for _, fields := range basics_testing.NearZeros(t, ac) {
+ for _, fields := range roundtrip.NearZeros(t, ac) {
a.False(fields.Empty(), "Empty is disregarding a non-zero field in %+v", fields)
}
}
diff --git a/data/transactions/transaction_test.go b/data/transactions/transaction_test.go
index 1d9f04d5f0..f214ca16df 100644
--- a/data/transactions/transaction_test.go
+++ b/data/transactions/transaction_test.go
@@ -25,7 +25,7 @@ import (
"github.com/algorand/go-algorand/config"
"github.com/algorand/go-algorand/crypto"
"github.com/algorand/go-algorand/data/basics"
- basics_testing "github.com/algorand/go-algorand/data/basics/testing"
+ "github.com/algorand/go-algorand/data/basics/testing/roundtrip"
"github.com/algorand/go-algorand/protocol"
"github.com/algorand/go-algorand/test/partitiontest"
)
@@ -124,7 +124,7 @@ func TestApplyDataEquality(t *testing.T) {
t.Parallel()
var empty ApplyData
- for _, ad := range basics_testing.NearZeros(t, ApplyData{}) {
+ for _, ad := range roundtrip.NearZeros(t, ApplyData{}) {
assert.False(t, ad.Equal(empty), "Equal() seems to be disregarding something %+v", ad)
}
@@ -135,7 +135,7 @@ func TestEvalDataEquality(t *testing.T) {
t.Parallel()
var empty EvalDelta
- for _, ed := range basics_testing.NearZeros(t, EvalDelta{}) {
+ for _, ed := range roundtrip.NearZeros(t, EvalDelta{}) {
assert.False(t, ed.Equal(empty), "Equal() seems to be disregarding something %+v", ed)
}
@@ -146,7 +146,7 @@ func TestLogicSigEquality(t *testing.T) {
t.Parallel()
var empty LogicSig
- for _, ls := range basics_testing.NearZeros(t, LogicSig{}) {
+ for _, ls := range roundtrip.NearZeros(t, LogicSig{}) {
assert.False(t, ls.Equal(&empty), "Equal() seems to be disregarding something %+v", ls)
}
diff --git a/ledger/store/trackerdb/data_test.go b/ledger/store/trackerdb/data_test.go
index c805ce1955..1a3db084e8 100644
--- a/ledger/store/trackerdb/data_test.go
+++ b/ledger/store/trackerdb/data_test.go
@@ -26,7 +26,6 @@ import (
"github.com/algorand/go-algorand/config"
"github.com/algorand/go-algorand/crypto"
"github.com/algorand/go-algorand/data/basics"
- basics_testing "github.com/algorand/go-algorand/data/basics/testing"
"github.com/algorand/go-algorand/data/basics/testing/roundtrip"
"github.com/algorand/go-algorand/ledger/ledgercore"
ledgertesting "github.com/algorand/go-algorand/ledger/testing"
@@ -1292,9 +1291,8 @@ func TestCopyFunctions(t *testing.T) {
rdToAsset := func(rd ResourcesData) basics.AssetParams {
return rd.GetAssetParams()
}
- for _, nz := range basics_testing.NearZeros(t, basics.AssetParams{}) {
- assert.True(t, roundtrip.Check(t, nz, assetToRD, rdToAsset), nz)
- }
+ // roundtrip.Check now automatically tests NearZeros + 100 random variants
+ assert.True(t, roundtrip.Check(t, basics.AssetParams{}, assetToRD, rdToAsset))
// Asset holdings are copied into and out of ResourceData losslessly
holdingToRD := func(ap basics.AssetHolding) ResourcesData {
@@ -1305,9 +1303,7 @@ func TestCopyFunctions(t *testing.T) {
rdToHolding := func(rd ResourcesData) basics.AssetHolding {
return rd.GetAssetHolding()
}
- for _, nz := range basics_testing.NearZeros(t, basics.AssetHolding{}) {
- assert.True(t, roundtrip.Check(t, nz, holdingToRD, rdToHolding), nz)
- }
+ assert.True(t, roundtrip.Check(t, basics.AssetHolding{}, holdingToRD, rdToHolding))
// AppParams are copied into and out of ResourceData losslessly
apToRD := func(ap basics.AppParams) ResourcesData {
@@ -1318,9 +1314,7 @@ func TestCopyFunctions(t *testing.T) {
rdToAP := func(rd ResourcesData) basics.AppParams {
return rd.GetAppParams()
}
- for _, nz := range basics_testing.NearZeros(t, basics.AppParams{}) {
- assert.True(t, roundtrip.Check(t, nz, apToRD, rdToAP), nz)
- }
+ assert.True(t, roundtrip.Check(t, basics.AppParams{}, apToRD, rdToAP))
// AppLocalStates are copied into and out of ResourceData losslessly
localsToRD := func(ap basics.AppLocalState) ResourcesData {
@@ -1331,9 +1325,7 @@ func TestCopyFunctions(t *testing.T) {
rdToLocals := func(rd ResourcesData) basics.AppLocalState {
return rd.GetAppLocalState()
}
- for _, nz := range basics_testing.NearZeros(t, basics.AppLocalState{}) {
- assert.True(t, roundtrip.Check(t, nz, localsToRD, rdToLocals), nz)
- }
+ assert.True(t, roundtrip.Check(t, basics.AppLocalState{}, localsToRD, rdToLocals))
}
@@ -1341,7 +1333,7 @@ func TestIsEmptyAppFields(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()
- for _, nz := range basics_testing.NearZeros(t, basics.AppParams{}) {
+ for _, nz := range roundtrip.NearZeros(t, basics.AppParams{}) {
var rd ResourcesData
rd.SetAppParams(nz, false)
assert.False(t, rd.IsEmptyAppFields(), nz)
diff --git a/network/msgOfInterest_test.go b/network/msgOfInterest_test.go
index 137a9cf502..da45849a8b 100644
--- a/network/msgOfInterest_test.go
+++ b/network/msgOfInterest_test.go
@@ -88,5 +88,5 @@ func TestDefaultSendMessageTagsMarshalRoundTrip(t *testing.T) {
}
// map[protocol.Tag]bool has constraints (Tag must be valid), so disable random testing
- require.True(t, roundtrip.Check(t, cloned, toBytes, toTags, roundtrip.NoRandomCases()), "default messages of interest should round-trip")
+ require.True(t, roundtrip.Check(t, cloned, toBytes, toTags, roundtrip.NoRandomCases(), roundtrip.NoNearZeros()), "default messages of interest should round-trip")
}
From d538a5a715bf889406d21b069d6feb22f5f2d985 Mon Sep 17 00:00:00 2001
From: cce <51567+cce@users.noreply.github.com>
Date: Thu, 6 Nov 2025 16:50:26 -0500
Subject: [PATCH 5/7] use any
---
data/basics/testing/roundtrip/roundtrip.go | 24 ++++++++--------------
1 file changed, 9 insertions(+), 15 deletions(-)
diff --git a/data/basics/testing/roundtrip/roundtrip.go b/data/basics/testing/roundtrip/roundtrip.go
index f618dc0281..99fc9bf8ec 100644
--- a/data/basics/testing/roundtrip/roundtrip.go
+++ b/data/basics/testing/roundtrip/roundtrip.go
@@ -33,11 +33,11 @@ type CheckOption interface {
}
type checkConfig struct {
- randomCount *int
- randomOpts []protocol.RandomizeObjectOption
- rapidGen interface{} // *rapid.Generator[A], stored as interface{} to avoid type parameters
- useRapid bool
- skipNearZeros bool
+ randomCount *int
+ randomOpts []protocol.RandomizeObjectOption
+ rapidGen any // *rapid.Generator[A], stored as any to avoid type parameters
+ useRapid bool
+ skipNearZeros bool
}
type randomCountOption int
@@ -54,7 +54,7 @@ func (opts randomOptsOption) apply(cfg *checkConfig) {
}
type rapidGenOption struct {
- gen interface{}
+ gen any
}
func (r rapidGenOption) apply(cfg *checkConfig) {
@@ -72,21 +72,15 @@ func Opts(count int, opts ...protocol.RandomizeObjectOption) CheckOption {
// NoRandomCases disables RandomizeObject testing (but still runs NearZeros).
// Use this when RandomizeObject generates invalid values for constrained types.
// Combine with NoNearZeros() to disable all automatic testing.
-func NoRandomCases() CheckOption {
- return randomCountOption(0)
-}
+func NoRandomCases() CheckOption { return randomCountOption(0) }
type skipNearZerosOption struct{}
-func (skipNearZerosOption) apply(cfg *checkConfig) {
- cfg.skipNearZeros = true
-}
+func (skipNearZerosOption) apply(cfg *checkConfig) { cfg.skipNearZeros = true }
// NoNearZeros disables NearZeros testing, only using RandomizeObject for random variants.
// Use this for non-struct types (maps, slices) where NearZeros doesn't apply.
-func NoNearZeros() CheckOption {
- return skipNearZerosOption{}
-}
+func NoNearZeros() CheckOption { return skipNearZerosOption{} }
// WithRapid specifies a rapid.Generator to use for property-based testing.
// If provided, rapid.Check will be used instead of protocol.RandomizeObject (runs 100 tests).
From 700a05758d17f3ff703c3593e7645b43b547426a Mon Sep 17 00:00:00 2001
From: cce <51567+cce@users.noreply.github.com>
Date: Thu, 6 Nov 2025 17:13:42 -0500
Subject: [PATCH 6/7] fix failing test
---
daemon/algod/api/server/v2/dryrun_test.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/daemon/algod/api/server/v2/dryrun_test.go b/daemon/algod/api/server/v2/dryrun_test.go
index 5d0c9cc0bc..0e90b33d7c 100644
--- a/daemon/algod/api/server/v2/dryrun_test.go
+++ b/daemon/algod/api/server/v2/dryrun_test.go
@@ -1110,7 +1110,7 @@ func TestStateDeltaToStateDelta(t *testing.T) {
return result
}
- require.True(t, roundtrip.Check(t, sd, globalDeltaToStateDelta, decode))
+ require.True(t, roundtrip.Check(t, sd, globalDeltaToStateDelta, decode, roundtrip.NoNearZeros()))
var keys []string
// test with a loop because sd is a map and iteration order is random
From ed99c905e17c3963dd7553b354f026ac61973a76 Mon Sep 17 00:00:00 2001
From: cce <51567+cce@users.noreply.github.com>
Date: Thu, 13 Nov 2025 11:06:30 -0500
Subject: [PATCH 7/7] simplify
---
crypto/onetimesig_test.go | 6 +-
daemon/algod/api/server/v2/account_test.go | 16 +-
daemon/algod/api/server/v2/dryrun_test.go | 5 +-
data/basics/teal_test.go | 37 +++--
data/basics/testing/roundtrip/roundtrip.go | 163 ++-------------------
ledger/store/trackerdb/data_test.go | 10 +-
network/msgOfInterest_test.go | 6 +-
7 files changed, 55 insertions(+), 188 deletions(-)
diff --git a/crypto/onetimesig_test.go b/crypto/onetimesig_test.go
index 323363322a..96a412f7ea 100644
--- a/crypto/onetimesig_test.go
+++ b/crypto/onetimesig_test.go
@@ -20,8 +20,6 @@ import (
"fmt"
"testing"
- "github.com/stretchr/testify/require"
-
"github.com/algorand/go-algorand/data/basics/testing/roundtrip"
"github.com/algorand/go-algorand/test/partitiontest"
)
@@ -149,9 +147,9 @@ func TestHeartbeatProofRoundTrip(t *testing.T) {
toOTS := func(h HeartbeatProof) OneTimeSignature { return h.ToOneTimeSignature() }
toProof := func(ots OneTimeSignature) HeartbeatProof { return ots.ToHeartbeatProof() }
- // Test with an empty proof as the example, RandomizeObject will generate 100 random variants
+ // Test with an empty proof as example, NearZeros will test each field
var emptyProof HeartbeatProof
- require.True(t, roundtrip.Check(t, emptyProof, toOTS, toProof))
+ roundtrip.Check(t, emptyProof, toOTS, toProof)
}
func BenchmarkOneTimeSigBatchVerification(b *testing.B) {
diff --git a/daemon/algod/api/server/v2/account_test.go b/daemon/algod/api/server/v2/account_test.go
index c16881244b..7ca873b5b0 100644
--- a/daemon/algod/api/server/v2/account_test.go
+++ b/daemon/algod/api/server/v2/account_test.go
@@ -167,7 +167,7 @@ func TestAccount(t *testing.T) {
verifyCreatedApp(1, appIdx2, appParams2)
appRoundTrip := func(idx basics.AppIndex, params basics.AppParams) {
- require.True(t, roundtrip.Check(t, params,
+ roundtrip.Check(t, params,
func(ap basics.AppParams) model.Application {
return AppParamsToApplication(addr, idx, &ap)
},
@@ -175,7 +175,7 @@ func TestAccount(t *testing.T) {
converted, err := ApplicationParamsToAppParams(&app.Params)
require.NoError(t, err)
return converted
- }))
+ })
}
appRoundTrip(appIdx1, appParams1)
@@ -234,7 +234,9 @@ func TestAccount(t *testing.T) {
verifyCreatedAsset(0, assetIdx1, assetParams1)
verifyCreatedAsset(1, assetIdx2, assetParams2)
- require.True(t, roundtrip.Check(t, b, toModel, toBasics, roundtrip.NoRandomCases(), roundtrip.NoNearZeros()))
+ // Verify round-trip conversion works for the manually constructed account
+ c := toBasics(toModel(b))
+ require.Equal(t, b, c)
t.Run("IsDeterministic", func(t *testing.T) {
// convert the same account a few more times to make sure we always
@@ -258,7 +260,9 @@ func TestAccountRandomRoundTrip(t *testing.T) {
round := basics.Round(2)
proto := config.Consensus[protocol.ConsensusFuture]
toModel, toBasics := makeAccountConverters(t, addr.String(), round, &proto, acct.MicroAlgos)
- require.True(t, roundtrip.Check(t, acct, toModel, toBasics, roundtrip.NoRandomCases(), roundtrip.NoNearZeros()))
+ // Test the randomly-generated account round-trips correctly
+ c := toBasics(toModel(acct))
+ require.Equal(t, acct, c)
}
}
}
@@ -297,7 +301,9 @@ func TestConvertTealKeyValueRoundTrip(t *testing.T) {
return converted
}
- require.True(t, roundtrip.Check(t, kv, toGenerated, toBasics, roundtrip.NoNearZeros()))
+ // Test the manually constructed map round-trips correctly
+ result := toBasics(toGenerated(kv))
+ require.Equal(t, kv, result)
})
}
diff --git a/daemon/algod/api/server/v2/dryrun_test.go b/daemon/algod/api/server/v2/dryrun_test.go
index 0e90b33d7c..de5d15c826 100644
--- a/daemon/algod/api/server/v2/dryrun_test.go
+++ b/daemon/algod/api/server/v2/dryrun_test.go
@@ -32,7 +32,6 @@ import (
"github.com/algorand/go-algorand/crypto"
"github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/model"
"github.com/algorand/go-algorand/data/basics"
- "github.com/algorand/go-algorand/data/basics/testing/roundtrip"
"github.com/algorand/go-algorand/data/transactions"
"github.com/algorand/go-algorand/data/transactions/logic"
"github.com/algorand/go-algorand/data/txntest"
@@ -1110,7 +1109,9 @@ func TestStateDeltaToStateDelta(t *testing.T) {
return result
}
- require.True(t, roundtrip.Check(t, sd, globalDeltaToStateDelta, decode, roundtrip.NoNearZeros()))
+ // Test the manually constructed StateDelta round-trips correctly
+ result := decode(globalDeltaToStateDelta(sd))
+ require.Equal(t, sd, result)
var keys []string
// test with a loop because sd is a map and iteration order is random
diff --git a/data/basics/teal_test.go b/data/basics/teal_test.go
index 460cd18e59..792913d231 100644
--- a/data/basics/teal_test.go
+++ b/data/basics/teal_test.go
@@ -22,7 +22,6 @@ import (
"github.com/stretchr/testify/require"
"pgregory.net/rapid"
- "github.com/algorand/go-algorand/data/basics/testing/roundtrip"
"github.com/algorand/go-algorand/test/partitiontest"
)
@@ -94,15 +93,15 @@ func TestTealValueRoundTrip(t *testing.T) {
// Test with a simple example value
example := TealValue{Type: TealUintType, Uint: 17}
- // Use roundtrip.Check with WithRapid for property-based testing
- require.True(t, roundtrip.Check(t, example,
- func(tv TealValue) ValueDelta { return tv.ToValueDelta() },
- func(vd ValueDelta) TealValue {
- tv, ok := vd.ToTealValue()
- require.True(t, ok)
- return tv
- },
- roundtrip.WithRapid(genTealValue())))
+ // TealValue has constraints (Type determines valid fields), so test the specific example
+ toVD := func(tv TealValue) ValueDelta { return tv.ToValueDelta() }
+ toTV := func(vd ValueDelta) TealValue {
+ tv, ok := vd.ToTealValue()
+ require.True(t, ok)
+ return tv
+ }
+ result := toTV(toVD(example))
+ require.Equal(t, example, result)
}
func TestValueDeltaRoundTrip(t *testing.T) {
@@ -111,15 +110,15 @@ func TestValueDeltaRoundTrip(t *testing.T) {
// Test with a simple example value
example := ValueDelta{Action: SetUintAction, Uint: 42}
- // Use roundtrip.Check with WithRapid for property-based testing
- require.True(t, roundtrip.Check(t, example,
- func(vd ValueDelta) TealValue {
- tv, ok := vd.ToTealValue()
- require.True(t, ok)
- return tv
- },
- func(tv TealValue) ValueDelta { return tv.ToValueDelta() },
- roundtrip.WithRapid(genValueDelta())))
+ // ValueDelta has constraints (Action determines valid fields), so test the specific example
+ toTV := func(vd ValueDelta) TealValue {
+ tv, ok := vd.ToTealValue()
+ require.True(t, ok)
+ return tv
+ }
+ toVD := func(tv TealValue) ValueDelta { return tv.ToValueDelta() }
+ result := toVD(toTV(example))
+ require.Equal(t, example, result)
}
func TestValueDeltaDeleteDoesNotRoundTrip(t *testing.T) {
diff --git a/data/basics/testing/roundtrip/roundtrip.go b/data/basics/testing/roundtrip/roundtrip.go
index 99fc9bf8ec..c1ccd7e15e 100644
--- a/data/basics/testing/roundtrip/roundtrip.go
+++ b/data/basics/testing/roundtrip/roundtrip.go
@@ -19,168 +19,31 @@ package roundtrip
import (
"reflect"
"testing"
-
- "pgregory.net/rapid"
-
- "github.com/algorand/go-algorand/protocol"
)
-const defaultRandomCount = 100
-
-// CheckOption configures the behavior of Check.
-type CheckOption interface {
- apply(*checkConfig)
-}
-
-type checkConfig struct {
- randomCount *int
- randomOpts []protocol.RandomizeObjectOption
- rapidGen any // *rapid.Generator[A], stored as any to avoid type parameters
- useRapid bool
- skipNearZeros bool
-}
-
-type randomCountOption int
-
-func (n randomCountOption) apply(cfg *checkConfig) {
- count := int(n)
- cfg.randomCount = &count
-}
-
-type randomOptsOption []protocol.RandomizeObjectOption
-
-func (opts randomOptsOption) apply(cfg *checkConfig) {
- cfg.randomOpts = append(cfg.randomOpts, opts...)
-}
-
-type rapidGenOption struct {
- gen any
-}
-
-func (r rapidGenOption) apply(cfg *checkConfig) {
- cfg.rapidGen = r.gen
- cfg.useRapid = true
-}
-
-// Opts configures round-trip checking behavior.
-// The first argument specifies the number of random test cases to generate.
-// Additional protocol.RandomizeObjectOption arguments can be passed to customize randomization.
-func Opts(count int, opts ...protocol.RandomizeObjectOption) CheckOption {
- return multiOption{randomCountOption(count), randomOptsOption(opts)}
-}
-
-// NoRandomCases disables RandomizeObject testing (but still runs NearZeros).
-// Use this when RandomizeObject generates invalid values for constrained types.
-// Combine with NoNearZeros() to disable all automatic testing.
-func NoRandomCases() CheckOption { return randomCountOption(0) }
-
-type skipNearZerosOption struct{}
-
-func (skipNearZerosOption) apply(cfg *checkConfig) { cfg.skipNearZeros = true }
-
-// NoNearZeros disables NearZeros testing, only using RandomizeObject for random variants.
-// Use this for non-struct types (maps, slices) where NearZeros doesn't apply.
-func NoNearZeros() CheckOption { return skipNearZerosOption{} }
-
-// WithRapid specifies a rapid.Generator to use for property-based testing.
-// If provided, rapid.Check will be used instead of protocol.RandomizeObject (runs 100 tests).
-func WithRapid[A any](gen *rapid.Generator[A]) CheckOption {
- return rapidGenOption{gen: gen}
-}
-
-type multiOption []CheckOption
-
-func (m multiOption) apply(cfg *checkConfig) {
- for _, opt := range m {
- opt.apply(cfg)
- }
-}
-
// Check verifies that converting from A -> B -> A yields the original value.
-// By default, tests the provided example, all NearZeros variants (one per field),
-// and 100 randomly generated values using protocol.RandomizeObject.
-// Use WithRapid to provide a custom rapid.Generator for property-based testing.
-// Use NoRandomCases to disable RandomizeObject (still runs NearZeros).
-// Use NoNearZeros to disable NearZeros (for non-struct types like maps).
-// Use Opts to customize the number of random tests or pass RandomizeObjectOptions.
-func Check[A any, B any](t *testing.T, a A, toB func(A) B, toA func(B) A, opts ...CheckOption) bool {
- cfg := checkConfig{}
- for _, opt := range opts {
- opt.apply(&cfg)
- }
-
- // Test the provided example first
- if !checkOne(t, a, toB, toA) {
- t.Errorf("Round-trip failed for provided example: %+v", a)
- return false
- }
-
- // Use rapid property testing if generator provided
- if cfg.useRapid {
- gen, ok := cfg.rapidGen.(*rapid.Generator[A])
- if !ok {
- t.Errorf("Invalid rapid generator type")
- return false
- }
+// It tests the provided example value, then tests all NearZeros variants (setting one field at a time).
+// NearZeros is tested first because failures clearly identify which field is problematic.
+func Check[A any, B any](t *testing.T, example A, toB func(A) B, toA func(B) A) {
+ t.Helper()
- // Run rapid property tests (runs 100 tests by default)
- // Note: rapid.Check controls the count, not us
- passed := true
- rapid.Check(t, func(t1 *rapid.T) {
- randA := gen.Draw(t1, "value")
- if !checkOne(t, randA, toB, toA) {
- t.Errorf("Round-trip failed for rapid-generated value: %+v", randA)
- passed = false
- }
- })
- return passed
+ // Test the provided example
+ if !checkOne(t, example, toB, toA) {
+ t.Fatalf("Round-trip failed for provided example: %+v", example)
}
// Test NearZeros (one test per field) - comprehensive and deterministic
- // Skip if explicitly disabled
- if !cfg.skipNearZeros {
- nearZeroValues := NearZeros(t, a)
- for i, nzA := range nearZeroValues {
- if !checkOne(t, nzA, toB, toA) {
- t.Errorf("Round-trip failed for NearZero variant %d: %+v", i, nzA)
- return false
- }
+ // This comes first because failures clearly show which field is the problem
+ nearZeroValues := NearZeros(t, example)
+ for i, nzA := range nearZeroValues {
+ if !checkOne(t, nzA, toB, toA) {
+ t.Fatalf("Round-trip failed for NearZero variant %d: %+v", i, nzA)
}
}
-
- // Determine random count for RandomizeObject testing
- randomCount := defaultRandomCount
- if cfg.randomCount != nil {
- randomCount = *cfg.randomCount
- }
-
- // Test with RandomizeObject for additional coverage
- var template A
- for i := 0; i < randomCount; i++ {
- randObj, err := protocol.RandomizeObject(&template, cfg.randomOpts...)
- if err != nil {
- t.Logf("Failed to randomize object (variant %d): %v", i, err)
- continue
- }
-
- // Type assert the result back to *A, then dereference
- randPtr, ok := randObj.(*A)
- if !ok {
- t.Errorf("Type assertion failed for random variant %d", i)
- return false
- }
- randA := *randPtr
-
- if !checkOne(t, randA, toB, toA) {
- t.Errorf("Round-trip failed for random variant %d: %+v", i, randA)
- return false
- }
- }
-
- return true
}
func checkOne[A any, B any](t *testing.T, a A, toB func(A) B, toA func(B) A) bool {
+ t.Helper()
b := toB(a)
a2 := toA(b)
if !reflect.DeepEqual(a, a2) {
diff --git a/ledger/store/trackerdb/data_test.go b/ledger/store/trackerdb/data_test.go
index 1a3db084e8..471be82ecf 100644
--- a/ledger/store/trackerdb/data_test.go
+++ b/ledger/store/trackerdb/data_test.go
@@ -1291,8 +1291,8 @@ func TestCopyFunctions(t *testing.T) {
rdToAsset := func(rd ResourcesData) basics.AssetParams {
return rd.GetAssetParams()
}
- // roundtrip.Check now automatically tests NearZeros + 100 random variants
- assert.True(t, roundtrip.Check(t, basics.AssetParams{}, assetToRD, rdToAsset))
+ // roundtrip.Check automatically tests the example plus NearZeros variants
+ roundtrip.Check(t, basics.AssetParams{}, assetToRD, rdToAsset)
// Asset holdings are copied into and out of ResourceData losslessly
holdingToRD := func(ap basics.AssetHolding) ResourcesData {
@@ -1303,7 +1303,7 @@ func TestCopyFunctions(t *testing.T) {
rdToHolding := func(rd ResourcesData) basics.AssetHolding {
return rd.GetAssetHolding()
}
- assert.True(t, roundtrip.Check(t, basics.AssetHolding{}, holdingToRD, rdToHolding))
+ roundtrip.Check(t, basics.AssetHolding{}, holdingToRD, rdToHolding)
// AppParams are copied into and out of ResourceData losslessly
apToRD := func(ap basics.AppParams) ResourcesData {
@@ -1314,7 +1314,7 @@ func TestCopyFunctions(t *testing.T) {
rdToAP := func(rd ResourcesData) basics.AppParams {
return rd.GetAppParams()
}
- assert.True(t, roundtrip.Check(t, basics.AppParams{}, apToRD, rdToAP))
+ roundtrip.Check(t, basics.AppParams{}, apToRD, rdToAP)
// AppLocalStates are copied into and out of ResourceData losslessly
localsToRD := func(ap basics.AppLocalState) ResourcesData {
@@ -1325,7 +1325,7 @@ func TestCopyFunctions(t *testing.T) {
rdToLocals := func(rd ResourcesData) basics.AppLocalState {
return rd.GetAppLocalState()
}
- assert.True(t, roundtrip.Check(t, basics.AppLocalState{}, localsToRD, rdToLocals))
+ roundtrip.Check(t, basics.AppLocalState{}, localsToRD, rdToLocals)
}
diff --git a/network/msgOfInterest_test.go b/network/msgOfInterest_test.go
index da45849a8b..94cbcb904f 100644
--- a/network/msgOfInterest_test.go
+++ b/network/msgOfInterest_test.go
@@ -22,7 +22,6 @@ import (
"github.com/stretchr/testify/require"
- "github.com/algorand/go-algorand/data/basics/testing/roundtrip"
"github.com/algorand/go-algorand/protocol"
"github.com/algorand/go-algorand/test/partitiontest"
)
@@ -87,6 +86,7 @@ func TestDefaultSendMessageTagsMarshalRoundTrip(t *testing.T) {
return tags
}
- // map[protocol.Tag]bool has constraints (Tag must be valid), so disable random testing
- require.True(t, roundtrip.Check(t, cloned, toBytes, toTags, roundtrip.NoRandomCases(), roundtrip.NoNearZeros()), "default messages of interest should round-trip")
+ // Test that default messages of interest round-trip correctly
+ result := toTags(toBytes(cloned))
+ require.Equal(t, cloned, result, "default messages of interest should round-trip")
}