From 1d12b9220a08adfcb48716dd486e1b2297e7bdf4 Mon Sep 17 00:00:00 2001 From: ttl33 <19664986+ttl33@users.noreply.github.com> Date: Wed, 25 Sep 2024 12:46:36 -0400 Subject: [PATCH 1/3] Fix: deterministically fetch perp info from state (#2341) (cherry picked from commit cc1b79546bc3a5ea548b91b46654b0a0f25035d9) # Conflicts: # protocol/testutil/constants/positions.go # protocol/x/subaccounts/keeper/subaccount.go # protocol/x/subaccounts/keeper/subaccount_test.go --- protocol/testutil/constants/positions.go | 100 ++++++++++++++++ protocol/x/subaccounts/keeper/subaccount.go | 22 +++- .../x/subaccounts/keeper/subaccount_test.go | 111 ++++++++++++++++++ 3 files changed, 227 insertions(+), 6 deletions(-) diff --git a/protocol/testutil/constants/positions.go b/protocol/testutil/constants/positions.go index 0dcf8e48cc..d46efa00d7 100644 --- a/protocol/testutil/constants/positions.go +++ b/protocol/testutil/constants/positions.go @@ -9,6 +9,7 @@ import ( var ( // Perpetual Positions. +<<<<<<< HEAD Long_Perp_1BTC_PositiveFunding = satypes.PerpetualPosition{ PerpetualId: 0, Quantums: dtypes.NewInt(100_000_000), // 1 BTC @@ -73,6 +74,105 @@ var ( PerpetualId: 1, Quantums: dtypes.NewIntFromBigInt(BigNegMaxUint64()), // 18,446,744,070 ETH, -$55,340,232,210,000 notional. } +======= + Long_Perp_1BTC_PositiveFunding = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(100_000_000), // 1 BTC + big.NewInt(0), + big.NewInt(0), + ) + Short_Perp_1ETH_NegativeFunding = *testutil.CreateSinglePerpetualPosition( + 1, + big.NewInt(-100_000_000), // 1 ETH + big.NewInt(-1), + big.NewInt(0), + ) + PerpetualPosition_OneBTCLong = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(100_000_000), // 1 BTC, $50,000 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_OneBTCShort = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(-100_000_000), // 1 BTC, -$50,000 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_OneTenthBTCLong = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(10_000_000), // 0.1 BTC, $5,000 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_OneTenthBTCShort = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(-10_000_000), // 0.1 BTC, -$5,000 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_OneHundredthBTCLong = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(1_000_000), // 0.01 BTC, $500 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_OneHundredthBTCShort = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(-1_000_000), // 0.01 BTC, -$500 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_FourThousandthsBTCLong = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(400_000), // 0.004 BTC, $200 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_FourThousandthsBTCShort = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(-400_000), // 0.004 BTC, -$200 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_OneAndHalfBTCLong = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(150_000_000), // 1.5 BTC, $75,000 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_OneTenthEthLong = *testutil.CreateSinglePerpetualPosition( + 1, + big.NewInt(100_000_000), // 0.1 ETH, $300 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_OneTenthEthShort = *testutil.CreateSinglePerpetualPosition( + 1, + big.NewInt(-100_000_000), // 0.1 ETH, -$300 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_MaxUint64EthLong = *testutil.CreateSinglePerpetualPosition( + 1, + big.NewInt(0).SetUint64(math.MaxUint64), // 18,446,744,070 ETH, $55,340,232,210,000 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_MaxUint64EthShort = *testutil.CreateSinglePerpetualPosition( + 1, + BigNegMaxUint64(), // 18,446,744,070 ETH, -$55,340,232,210,000 notional. + big.NewInt(0), + big.NewInt(0), + ) + // SOL positions + PerpetualPosition_OneSolLong = *testutil.CreateSinglePerpetualPosition( + 2, + big.NewInt(100_000_000_000), // 1 SOL + big.NewInt(0), + big.NewInt(0), + ) +>>>>>>> cc1b7954 (Fix: deterministically fetch perp info from state (#2341)) // Long position for arbitrary isolated market PerpetualPosition_OneISOLong = satypes.PerpetualPosition{ PerpetualId: 3, diff --git a/protocol/x/subaccounts/keeper/subaccount.go b/protocol/x/subaccounts/keeper/subaccount.go index 5a10c242a0..2af4014b19 100644 --- a/protocol/x/subaccounts/keeper/subaccount.go +++ b/protocol/x/subaccounts/keeper/subaccount.go @@ -1027,33 +1027,43 @@ func (k Keeper) GetAllRelevantPerpetuals( map[uint32]perptypes.PerpInfo, error, ) { - subaccountIds := make(map[types.SubaccountId]struct{}) - perpIds := make(map[uint32]struct{}) + subaccountIdsSet := make(map[types.SubaccountId]struct{}) + perpIdsSet := make(map[uint32]struct{}) // Add all relevant perpetuals in every update. for _, update := range updates { // If this subaccount has not been processed already, get all of its existing perpetuals. - if _, exists := subaccountIds[update.SubaccountId]; !exists { + if _, exists := subaccountIdsSet[update.SubaccountId]; !exists { sa := k.GetSubaccount(ctx, update.SubaccountId) for _, postition := range sa.PerpetualPositions { - perpIds[postition.PerpetualId] = struct{}{} + perpIdsSet[postition.PerpetualId] = struct{}{} } - subaccountIds[update.SubaccountId] = struct{}{} + subaccountIdsSet[update.SubaccountId] = struct{}{} } // Add all perpetuals in the update. for _, perpUpdate := range update.PerpetualUpdates { - perpIds[perpUpdate.GetId()] = struct{}{} + perpIdsSet[perpUpdate.GetId()] = struct{}{} } } + // Important: Sort the perpIds to ensure determinism! + sortedPerpIds := lib.GetSortedKeys[lib.Sortable[uint32]](perpIdsSet) + // Get all perpetual information from state. +<<<<<<< HEAD perpetuals := make(map[uint32]perptypes.PerpInfo, len(perpIds)) for perpId := range perpIds { perpetual, price, liquidityTier, err := k.perpetualsKeeper.GetPerpetualAndMarketPriceAndLiquidityTier(ctx, perpId) +======= + ltCache := make(map[uint32]perptypes.LiquidityTier) + perpInfos := make(perptypes.PerpInfos, len(sortedPerpIds)) + for _, perpId := range sortedPerpIds { + perpetual, price, err := k.perpetualsKeeper.GetPerpetualAndMarketPrice(ctx, perpId) +>>>>>>> cc1b7954 (Fix: deterministically fetch perp info from state (#2341)) if err != nil { return nil, err } diff --git a/protocol/x/subaccounts/keeper/subaccount_test.go b/protocol/x/subaccounts/keeper/subaccount_test.go index 747ff8cfb9..3dae58010a 100644 --- a/protocol/x/subaccounts/keeper/subaccount_test.go +++ b/protocol/x/subaccounts/keeper/subaccount_test.go @@ -10,6 +10,7 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/dydxprotocol/v4-chain/protocol/lib" + storetypes "cosmossdk.io/store/types" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/dydxprotocol/v4-chain/protocol/dtypes" @@ -5850,6 +5851,7 @@ func TestGetNetCollateralAndMarginRequirements(t *testing.T) { } } +<<<<<<< HEAD func TestIsValidStateTransitionForUndercollateralizedSubaccount_ZeroMarginRequirements(t *testing.T) { tests := map[string]struct { bigCurNetCollateral *big.Int @@ -5934,6 +5936,115 @@ func TestIsValidStateTransitionForUndercollateralizedSubaccount_ZeroMarginRequir tc.bigNewMaintenanceMargin, ), ) +======= +func TestGetAllRelevantPerpetuals_Deterministic(t *testing.T) { + tests := map[string]struct { + // state + perpetuals []perptypes.Perpetual + + // subaccount state + assetPositions []*types.AssetPosition + perpetualPositions []*types.PerpetualPosition + + // updates + assetUpdates []types.AssetUpdate + perpetualUpdates []types.PerpetualUpdate + }{ + "Gas used is deterministic when erroring on gas usage": { + assetPositions: testutil.CreateUsdcAssetPositions(big.NewInt(10_000_000_001)), // $10,000.000001 + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_NoMarginRequirement, + constants.EthUsd_NoMarginRequirement, + constants.SolUsd_20PercentInitial_10PercentMaintenance, + }, + perpetualPositions: []*types.PerpetualPosition{ + &constants.PerpetualPosition_OneBTCLong, + &constants.PerpetualPosition_OneTenthEthLong, + &constants.PerpetualPosition_OneSolLong, + }, + assetUpdates: []types.AssetUpdate{ + { + AssetId: constants.Usdc.Id, + BigQuantumsDelta: big.NewInt(1_000_000), // +1 USDC + }, + }, + perpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(-200_000_000), // -2 BTC + }, + { + PerpetualId: uint32(1), + BigQuantumsDelta: big.NewInt(250_000_000), // .25 ETH + }, + { + PerpetualId: uint32(2), + BigQuantumsDelta: big.NewInt(500_000_000), // .005 SOL + }, + }, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // Setup. + ctx, keeper, pricesKeeper, perpetualsKeeper, _, _, assetsKeeper, _, _, _, _ := keepertest.SubaccountsKeepers( + t, + true, + ) + keepertest.CreateTestMarkets(t, ctx, pricesKeeper) + keepertest.CreateTestLiquidityTiers(t, ctx, perpetualsKeeper) + keepertest.CreateTestPerpetuals(t, ctx, perpetualsKeeper) + for _, p := range tc.perpetuals { + perpetualsKeeper.SetPerpetualForTest(ctx, p) + } + require.NoError(t, keepertest.CreateUsdcAsset(ctx, assetsKeeper)) + + subaccount := createNSubaccount(keeper, ctx, 1, big.NewInt(1_000))[0] + subaccount.PerpetualPositions = tc.perpetualPositions + subaccount.AssetPositions = tc.assetPositions + keeper.SetSubaccount(ctx, subaccount) + subaccountId := *subaccount.Id + + update := types.Update{ + SubaccountId: subaccountId, + AssetUpdates: tc.assetUpdates, + PerpetualUpdates: tc.perpetualUpdates, + } + + // Execute. + gasUsedBefore := ctx.GasMeter().GasConsumed() + _, err := keeper.GetAllRelevantPerpetuals(ctx, []types.Update{update}) + require.NoError(t, err) + gasUsedAfter := ctx.GasMeter().GasConsumed() + + gasUsed := uint64(0) + // Run 100 times since it's highly unlikely gas usage is deterministic over 100 times if + // there's non-determinism. + for range 100 { + // divide by 2 so that the state read fails at least second to last time. + ctxWithLimitedGas := ctx.WithGasMeter(storetypes.NewGasMeter((gasUsedAfter - gasUsedBefore) / 2)) + + require.PanicsWithValue( + t, + storetypes.ErrorOutOfGas{Descriptor: "ReadFlat"}, + func() { + _, _ = keeper.GetAllRelevantPerpetuals(ctxWithLimitedGas, []types.Update{update}) + }, + ) + + if gasUsed == 0 { + gasUsed = ctxWithLimitedGas.GasMeter().GasConsumed() + require.Greater(t, gasUsed, uint64(0)) + } else { + require.Equal( + t, + gasUsed, + ctxWithLimitedGas.GasMeter().GasConsumed(), + "Gas usage when out of gas is not deterministic", + ) + } + } +>>>>>>> cc1b7954 (Fix: deterministically fetch perp info from state (#2341)) }) } } From ea240be8b2592f5f2d6cb2cc4b2dea73a43006c3 Mon Sep 17 00:00:00 2001 From: taehoon <19664986+ttl33@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:08:13 -0400 Subject: [PATCH 2/3] resolve merge conflicts --- protocol/testutil/constants/positions.go | 104 +----------------- protocol/x/subaccounts/keeper/subaccount.go | 11 +- .../x/subaccounts/keeper/subaccount_test.go | 14 ++- 3 files changed, 15 insertions(+), 114 deletions(-) diff --git a/protocol/testutil/constants/positions.go b/protocol/testutil/constants/positions.go index d46efa00d7..f75a9ac52a 100644 --- a/protocol/testutil/constants/positions.go +++ b/protocol/testutil/constants/positions.go @@ -9,7 +9,6 @@ import ( var ( // Perpetual Positions. -<<<<<<< HEAD Long_Perp_1BTC_PositiveFunding = satypes.PerpetualPosition{ PerpetualId: 0, Quantums: dtypes.NewInt(100_000_000), // 1 BTC @@ -74,105 +73,12 @@ var ( PerpetualId: 1, Quantums: dtypes.NewIntFromBigInt(BigNegMaxUint64()), // 18,446,744,070 ETH, -$55,340,232,210,000 notional. } -======= - Long_Perp_1BTC_PositiveFunding = *testutil.CreateSinglePerpetualPosition( - 0, - big.NewInt(100_000_000), // 1 BTC - big.NewInt(0), - big.NewInt(0), - ) - Short_Perp_1ETH_NegativeFunding = *testutil.CreateSinglePerpetualPosition( - 1, - big.NewInt(-100_000_000), // 1 ETH - big.NewInt(-1), - big.NewInt(0), - ) - PerpetualPosition_OneBTCLong = *testutil.CreateSinglePerpetualPosition( - 0, - big.NewInt(100_000_000), // 1 BTC, $50,000 notional. - big.NewInt(0), - big.NewInt(0), - ) - PerpetualPosition_OneBTCShort = *testutil.CreateSinglePerpetualPosition( - 0, - big.NewInt(-100_000_000), // 1 BTC, -$50,000 notional. - big.NewInt(0), - big.NewInt(0), - ) - PerpetualPosition_OneTenthBTCLong = *testutil.CreateSinglePerpetualPosition( - 0, - big.NewInt(10_000_000), // 0.1 BTC, $5,000 notional. - big.NewInt(0), - big.NewInt(0), - ) - PerpetualPosition_OneTenthBTCShort = *testutil.CreateSinglePerpetualPosition( - 0, - big.NewInt(-10_000_000), // 0.1 BTC, -$5,000 notional. - big.NewInt(0), - big.NewInt(0), - ) - PerpetualPosition_OneHundredthBTCLong = *testutil.CreateSinglePerpetualPosition( - 0, - big.NewInt(1_000_000), // 0.01 BTC, $500 notional. - big.NewInt(0), - big.NewInt(0), - ) - PerpetualPosition_OneHundredthBTCShort = *testutil.CreateSinglePerpetualPosition( - 0, - big.NewInt(-1_000_000), // 0.01 BTC, -$500 notional. - big.NewInt(0), - big.NewInt(0), - ) - PerpetualPosition_FourThousandthsBTCLong = *testutil.CreateSinglePerpetualPosition( - 0, - big.NewInt(400_000), // 0.004 BTC, $200 notional. - big.NewInt(0), - big.NewInt(0), - ) - PerpetualPosition_FourThousandthsBTCShort = *testutil.CreateSinglePerpetualPosition( - 0, - big.NewInt(-400_000), // 0.004 BTC, -$200 notional. - big.NewInt(0), - big.NewInt(0), - ) - PerpetualPosition_OneAndHalfBTCLong = *testutil.CreateSinglePerpetualPosition( - 0, - big.NewInt(150_000_000), // 1.5 BTC, $75,000 notional. - big.NewInt(0), - big.NewInt(0), - ) - PerpetualPosition_OneTenthEthLong = *testutil.CreateSinglePerpetualPosition( - 1, - big.NewInt(100_000_000), // 0.1 ETH, $300 notional. - big.NewInt(0), - big.NewInt(0), - ) - PerpetualPosition_OneTenthEthShort = *testutil.CreateSinglePerpetualPosition( - 1, - big.NewInt(-100_000_000), // 0.1 ETH, -$300 notional. - big.NewInt(0), - big.NewInt(0), - ) - PerpetualPosition_MaxUint64EthLong = *testutil.CreateSinglePerpetualPosition( - 1, - big.NewInt(0).SetUint64(math.MaxUint64), // 18,446,744,070 ETH, $55,340,232,210,000 notional. - big.NewInt(0), - big.NewInt(0), - ) - PerpetualPosition_MaxUint64EthShort = *testutil.CreateSinglePerpetualPosition( - 1, - BigNegMaxUint64(), // 18,446,744,070 ETH, -$55,340,232,210,000 notional. - big.NewInt(0), - big.NewInt(0), - ) // SOL positions - PerpetualPosition_OneSolLong = *testutil.CreateSinglePerpetualPosition( - 2, - big.NewInt(100_000_000_000), // 1 SOL - big.NewInt(0), - big.NewInt(0), - ) ->>>>>>> cc1b7954 (Fix: deterministically fetch perp info from state (#2341)) + PerpetualPosition_OneSolLong = satypes.PerpetualPosition{ + PerpetualId: 2, + Quantums: dtypes.NewInt(100_000_000_000), // 1 SOL + FundingIndex: dtypes.NewInt(0), + } // Long position for arbitrary isolated market PerpetualPosition_OneISOLong = satypes.PerpetualPosition{ PerpetualId: 3, diff --git a/protocol/x/subaccounts/keeper/subaccount.go b/protocol/x/subaccounts/keeper/subaccount.go index 2af4014b19..a5084857f3 100644 --- a/protocol/x/subaccounts/keeper/subaccount.go +++ b/protocol/x/subaccounts/keeper/subaccount.go @@ -1051,19 +1051,12 @@ func (k Keeper) GetAllRelevantPerpetuals( sortedPerpIds := lib.GetSortedKeys[lib.Sortable[uint32]](perpIdsSet) // Get all perpetual information from state. -<<<<<<< HEAD - perpetuals := make(map[uint32]perptypes.PerpInfo, len(perpIds)) - for perpId := range perpIds { + perpetuals := make(map[uint32]perptypes.PerpInfo, len(sortedPerpIds)) + for _, perpId := range sortedPerpIds { perpetual, price, liquidityTier, err := k.perpetualsKeeper.GetPerpetualAndMarketPriceAndLiquidityTier(ctx, perpId) -======= - ltCache := make(map[uint32]perptypes.LiquidityTier) - perpInfos := make(perptypes.PerpInfos, len(sortedPerpIds)) - for _, perpId := range sortedPerpIds { - perpetual, price, err := k.perpetualsKeeper.GetPerpetualAndMarketPrice(ctx, perpId) ->>>>>>> cc1b7954 (Fix: deterministically fetch perp info from state (#2341)) if err != nil { return nil, err } diff --git a/protocol/x/subaccounts/keeper/subaccount_test.go b/protocol/x/subaccounts/keeper/subaccount_test.go index 3dae58010a..35978ebf0b 100644 --- a/protocol/x/subaccounts/keeper/subaccount_test.go +++ b/protocol/x/subaccounts/keeper/subaccount_test.go @@ -18,6 +18,7 @@ import ( bank_testutil "github.com/dydxprotocol/v4-chain/protocol/testutil/bank" big_testutil "github.com/dydxprotocol/v4-chain/protocol/testutil/big" "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" + keepertest "github.com/dydxprotocol/v4-chain/protocol/testutil/keeper" testutil "github.com/dydxprotocol/v4-chain/protocol/testutil/keeper" "github.com/dydxprotocol/v4-chain/protocol/testutil/nullify" perptest "github.com/dydxprotocol/v4-chain/protocol/testutil/perpetuals" @@ -5851,7 +5852,6 @@ func TestGetNetCollateralAndMarginRequirements(t *testing.T) { } } -<<<<<<< HEAD func TestIsValidStateTransitionForUndercollateralizedSubaccount_ZeroMarginRequirements(t *testing.T) { tests := map[string]struct { bigCurNetCollateral *big.Int @@ -5936,7 +5936,10 @@ func TestIsValidStateTransitionForUndercollateralizedSubaccount_ZeroMarginRequir tc.bigNewMaintenanceMargin, ), ) -======= + }) + } +} + func TestGetAllRelevantPerpetuals_Deterministic(t *testing.T) { tests := map[string]struct { // state @@ -5951,7 +5954,7 @@ func TestGetAllRelevantPerpetuals_Deterministic(t *testing.T) { perpetualUpdates []types.PerpetualUpdate }{ "Gas used is deterministic when erroring on gas usage": { - assetPositions: testutil.CreateUsdcAssetPositions(big.NewInt(10_000_000_001)), // $10,000.000001 + assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(10_000_000_001)), // $10,000.000001 perpetuals: []perptypes.Perpetual{ constants.BtcUsd_NoMarginRequirement, constants.EthUsd_NoMarginRequirement, @@ -5987,7 +5990,7 @@ func TestGetAllRelevantPerpetuals_Deterministic(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { // Setup. - ctx, keeper, pricesKeeper, perpetualsKeeper, _, _, assetsKeeper, _, _, _, _ := keepertest.SubaccountsKeepers( + ctx, keeper, pricesKeeper, perpetualsKeeper, _, _, assetsKeeper, _, _ := keepertest.SubaccountsKeepers( t, true, ) @@ -6026,7 +6029,7 @@ func TestGetAllRelevantPerpetuals_Deterministic(t *testing.T) { require.PanicsWithValue( t, - storetypes.ErrorOutOfGas{Descriptor: "ReadFlat"}, + storetypes.ErrorOutOfGas{Descriptor: "ReadPerByte"}, func() { _, _ = keeper.GetAllRelevantPerpetuals(ctxWithLimitedGas, []types.Update{update}) }, @@ -6044,7 +6047,6 @@ func TestGetAllRelevantPerpetuals_Deterministic(t *testing.T) { ) } } ->>>>>>> cc1b7954 (Fix: deterministically fetch perp info from state (#2341)) }) } } From 980fd31875d6b8d878392ff7952b698092cd6f37 Mon Sep 17 00:00:00 2001 From: taehoon <19664986+ttl33@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:16:04 -0400 Subject: [PATCH 3/3] add extra line: re-triggering github workflows --- protocol/x/subaccounts/keeper/subaccount_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/protocol/x/subaccounts/keeper/subaccount_test.go b/protocol/x/subaccounts/keeper/subaccount_test.go index 35978ebf0b..fe0aaf9561 100644 --- a/protocol/x/subaccounts/keeper/subaccount_test.go +++ b/protocol/x/subaccounts/keeper/subaccount_test.go @@ -5987,6 +5987,7 @@ func TestGetAllRelevantPerpetuals_Deterministic(t *testing.T) { }, }, } + for name, tc := range tests { t.Run(name, func(t *testing.T) { // Setup.