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") }