mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-02 18:41:01 +08:00
fix(trader): harden API calls with timeouts, strict balance parsing, error context
- binance/bybit/gate: SDK default http.DefaultClient has no timeout; use a dedicated 30s-timeout client so a hung connection cannot stall the loop - bybit: stop mutating http.DefaultClient.Transport, which leaked the referer header into every other HTTP request in the process - add types.ParseFloatField: empty exchange fields stay zero, but malformed numeric values now surface as errors instead of silently becoming zero balances (applied to GetBalance across 8 exchanges) - wrap order/market-data errors in auto_trader_orders and okx cancel paths with symbol context; log per-order cancel failures in okx CancelAllOrders
This commit is contained in:
@@ -36,14 +36,21 @@ func (t *AsterTrader) GetBalance() (map[string]interface{}, error) {
|
||||
foundUSDT = true
|
||||
|
||||
// Parse Aster fields (reference: https://github.com/asterdex/api-docs)
|
||||
var parseErr error
|
||||
if avail, ok := bal["availableBalance"].(string); ok {
|
||||
availableBalance, _ = strconv.ParseFloat(avail, 64)
|
||||
if availableBalance, parseErr = types.ParseFloatField("availableBalance", avail); parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
}
|
||||
if unpnl, ok := bal["crossUnPnl"].(string); ok {
|
||||
crossUnPnl, _ = strconv.ParseFloat(unpnl, 64)
|
||||
if crossUnPnl, parseErr = types.ParseFloatField("crossUnPnl", unpnl); parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
}
|
||||
if cwb, ok := bal["crossWalletBalance"].(string); ok {
|
||||
crossWalletBalance, _ = strconv.ParseFloat(cwb, 64)
|
||||
if crossWalletBalance, parseErr = types.ParseFloatField("crossWalletBalance", cwb); parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func (at *AutoTrader) executeOpenLongWithRecord(decision *kernel.Decision, actio
|
||||
// Get current price
|
||||
marketData, err := market.GetWithExchange(decision.Symbol, at.exchange)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to get market data for %s: %w", decision.Symbol, err)
|
||||
}
|
||||
|
||||
// Get balance (needed for multiple checks)
|
||||
@@ -117,7 +117,7 @@ func (at *AutoTrader) executeOpenLongWithRecord(decision *kernel.Decision, actio
|
||||
// Open position
|
||||
order, err := at.trader.OpenLong(decision.Symbol, quantity, decision.Leverage)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to open long position for %s: %w", decision.Symbol, err)
|
||||
}
|
||||
|
||||
// Record order ID
|
||||
@@ -170,7 +170,7 @@ func (at *AutoTrader) executeOpenShortWithRecord(decision *kernel.Decision, acti
|
||||
// Get current price
|
||||
marketData, err := market.GetWithExchange(decision.Symbol, at.exchange)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to get market data for %s: %w", decision.Symbol, err)
|
||||
}
|
||||
|
||||
// Get balance (needed for multiple checks)
|
||||
@@ -234,7 +234,7 @@ func (at *AutoTrader) executeOpenShortWithRecord(decision *kernel.Decision, acti
|
||||
// Open position
|
||||
order, err := at.trader.OpenShort(decision.Symbol, quantity, decision.Leverage)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to open short position for %s: %w", decision.Symbol, err)
|
||||
}
|
||||
|
||||
// Record order ID
|
||||
@@ -269,7 +269,7 @@ func (at *AutoTrader) executeCloseLongWithRecord(decision *kernel.Decision, acti
|
||||
// Get current price
|
||||
marketData, err := market.GetWithExchange(decision.Symbol, at.exchange)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to get market data for %s: %w", decision.Symbol, err)
|
||||
}
|
||||
actionRecord.Price = marketData.CurrentPrice
|
||||
|
||||
@@ -311,7 +311,7 @@ func (at *AutoTrader) executeCloseLongWithRecord(decision *kernel.Decision, acti
|
||||
// Close position
|
||||
order, err := at.trader.CloseLong(decision.Symbol, 0) // 0 = close all
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to close long position for %s: %w", decision.Symbol, err)
|
||||
}
|
||||
|
||||
// Record order ID
|
||||
@@ -333,7 +333,7 @@ func (at *AutoTrader) executeCloseShortWithRecord(decision *kernel.Decision, act
|
||||
// Get current price
|
||||
marketData, err := market.GetWithExchange(decision.Symbol, at.exchange)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to get market data for %s: %w", decision.Symbol, err)
|
||||
}
|
||||
actionRecord.Price = marketData.CurrentPrice
|
||||
|
||||
@@ -375,7 +375,7 @@ func (at *AutoTrader) executeCloseShortWithRecord(decision *kernel.Decision, act
|
||||
// Close position
|
||||
order, err := at.trader.CloseShort(decision.Symbol, 0) // 0 = close all
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to close short position for %s: %w", decision.Symbol, err)
|
||||
}
|
||||
|
||||
// Record order ID
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"nofx/hook"
|
||||
"nofx/logger"
|
||||
"strings"
|
||||
@@ -63,6 +64,9 @@ type FuturesTrader struct {
|
||||
// NewFuturesTrader creates futures trader
|
||||
func NewFuturesTrader(apiKey, secretKey string, userId string) *FuturesTrader {
|
||||
client := futures.NewClient(apiKey, secretKey)
|
||||
// The SDK defaults to http.DefaultClient, which has no timeout — a hung
|
||||
// connection would stall the trading loop indefinitely.
|
||||
client.HTTPClient = &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
hookRes := hook.HookExec[hook.NewBinanceTraderResult](hook.NEW_BINANCE_TRADER, userId, client)
|
||||
if hookRes != nil && hookRes.GetResult() != nil {
|
||||
|
||||
@@ -30,9 +30,17 @@ func (t *FuturesTrader) GetBalance() (map[string]interface{}, error) {
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
result["totalWalletBalance"], _ = strconv.ParseFloat(account.TotalWalletBalance, 64)
|
||||
result["availableBalance"], _ = strconv.ParseFloat(account.AvailableBalance, 64)
|
||||
result["totalUnrealizedProfit"], _ = strconv.ParseFloat(account.TotalUnrealizedProfit, 64)
|
||||
for field, value := range map[string]string{
|
||||
"totalWalletBalance": account.TotalWalletBalance,
|
||||
"availableBalance": account.AvailableBalance,
|
||||
"totalUnrealizedProfit": account.TotalUnrealizedProfit,
|
||||
} {
|
||||
parsed, parseErr := types.ParseFloatField(field, value)
|
||||
if parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
result[field] = parsed
|
||||
}
|
||||
|
||||
logger.Infof("✓ Binance API returned: total balance=%s, available=%s, unrealized PnL=%s",
|
||||
account.TotalWalletBalance,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"nofx/logger"
|
||||
"nofx/trader/types"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -43,9 +44,15 @@ func (t *BitgetTrader) GetBalance() (map[string]interface{}, error) {
|
||||
var totalEquity, availableBalance, unrealizedPnL float64
|
||||
for _, acc := range accounts {
|
||||
if acc.MarginCoin == "USDT" {
|
||||
totalEquity, _ = strconv.ParseFloat(acc.AccountEquity, 64)
|
||||
availableBalance, _ = strconv.ParseFloat(acc.Available, 64)
|
||||
unrealizedPnL, _ = strconv.ParseFloat(acc.UnrealizedPL, 64)
|
||||
if totalEquity, err = types.ParseFloatField("accountEquity", acc.AccountEquity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if availableBalance, err = types.ParseFloatField("available", acc.Available); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if unrealizedPnL, err = types.ParseFloatField("unrealizedPL", acc.UnrealizedPL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Infof("✓ [Bitget] Balance: equity=%.2f, available=%.2f", totalEquity, availableBalance)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -45,16 +45,22 @@ func NewBybitTrader(apiKey, secretKey string) *BybitTrader {
|
||||
|
||||
client := bybit.NewBybitHttpClient(apiKey, secretKey, bybit.WithBaseURL(bybit.MAINNET))
|
||||
|
||||
// Set HTTP transport
|
||||
if client != nil && client.HTTPClient != nil {
|
||||
defaultTransport := client.HTTPClient.Transport
|
||||
if defaultTransport == nil {
|
||||
defaultTransport = http.DefaultTransport
|
||||
// Set HTTP transport. Use a dedicated client instead of mutating the
|
||||
// SDK default (http.DefaultClient): mutating it would leak the referer
|
||||
// header to every other request in the process, and the default client
|
||||
// has no timeout, so a hung connection would stall the trading loop.
|
||||
if client != nil {
|
||||
defaultTransport := http.DefaultTransport
|
||||
if client.HTTPClient != nil && client.HTTPClient != http.DefaultClient && client.HTTPClient.Transport != nil {
|
||||
defaultTransport = client.HTTPClient.Transport
|
||||
}
|
||||
|
||||
client.HTTPClient.Transport = &headerRoundTripper{
|
||||
base: defaultTransport,
|
||||
refererID: src,
|
||||
client.HTTPClient = &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &headerRoundTripper{
|
||||
base: defaultTransport,
|
||||
refererID: src,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,19 +51,28 @@ func (t *BybitTrader) GetBalance() (map[string]interface{}, error) {
|
||||
|
||||
if len(list) > 0 {
|
||||
account, _ := list[0].(map[string]interface{})
|
||||
var parseErr error
|
||||
if equityStr, ok := account["totalEquity"].(string); ok {
|
||||
totalEquity, _ = strconv.ParseFloat(equityStr, 64)
|
||||
if totalEquity, parseErr = types.ParseFloatField("totalEquity", equityStr); parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
}
|
||||
if availStr, ok := account["totalAvailableBalance"].(string); ok {
|
||||
availableBalance, _ = strconv.ParseFloat(availStr, 64)
|
||||
if availableBalance, parseErr = types.ParseFloatField("totalAvailableBalance", availStr); parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
}
|
||||
// Bybit UNIFIED account wallet balance field
|
||||
if walletStr, ok := account["totalWalletBalance"].(string); ok {
|
||||
totalWalletBalance, _ = strconv.ParseFloat(walletStr, 64)
|
||||
if totalWalletBalance, parseErr = types.ParseFloatField("totalWalletBalance", walletStr); parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
}
|
||||
// Bybit perpetual contract unrealized PnL
|
||||
if uplStr, ok := account["totalPerpUPL"].(string); ok {
|
||||
totalPerpUPL, _ = strconv.ParseFloat(uplStr, 64)
|
||||
if totalPerpUPL, parseErr = types.ParseFloatField("totalPerpUPL", uplStr); parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package gate
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"nofx/trader/types"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -34,6 +35,9 @@ type GateTrader struct {
|
||||
func NewGateTrader(apiKey, secretKey string) *GateTrader {
|
||||
config := gateapi.NewConfiguration()
|
||||
config.AddDefaultHeader("X-Gate-Channel-Id", "nofx")
|
||||
// The SDK default HTTP client has no timeout — a hung connection would
|
||||
// stall the trading loop indefinitely.
|
||||
config.HTTPClient = &http.Client{Timeout: 30 * time.Second}
|
||||
client := gateapi.NewAPIClient(config)
|
||||
|
||||
ctx := context.WithValue(context.Background(),
|
||||
|
||||
@@ -27,9 +27,18 @@ func (t *GateTrader) GetBalance() (map[string]interface{}, error) {
|
||||
return nil, fmt.Errorf("failed to get balance: %w", err)
|
||||
}
|
||||
|
||||
total, _ := strconv.ParseFloat(accounts.Total, 64)
|
||||
available, _ := strconv.ParseFloat(accounts.Available, 64)
|
||||
unrealizedPnl, _ := strconv.ParseFloat(accounts.UnrealisedPnl, 64)
|
||||
total, err := types.ParseFloatField("total", accounts.Total)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
available, err := types.ParseFloatField("available", accounts.Available)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
unrealizedPnl, err := types.ParseFloatField("unrealisedPnl", accounts.UnrealisedPnl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"totalWalletBalance": total,
|
||||
|
||||
@@ -47,16 +47,25 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
||||
var summaryType string
|
||||
var summary interface{}
|
||||
|
||||
var parseErr error
|
||||
if t.isCrossMargin {
|
||||
// Cross margin mode: use CrossMarginSummary
|
||||
accountValue, _ = strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64)
|
||||
totalMarginUsed, _ = strconv.ParseFloat(accountState.CrossMarginSummary.TotalMarginUsed, 64)
|
||||
if accountValue, parseErr = types.ParseFloatField("accountValue", accountState.CrossMarginSummary.AccountValue); parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
if totalMarginUsed, parseErr = types.ParseFloatField("totalMarginUsed", accountState.CrossMarginSummary.TotalMarginUsed); parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
summaryType = "CrossMarginSummary (cross margin)"
|
||||
summary = accountState.CrossMarginSummary
|
||||
} else {
|
||||
// Isolated margin mode: use MarginSummary
|
||||
accountValue, _ = strconv.ParseFloat(accountState.MarginSummary.AccountValue, 64)
|
||||
totalMarginUsed, _ = strconv.ParseFloat(accountState.MarginSummary.TotalMarginUsed, 64)
|
||||
if accountValue, parseErr = types.ParseFloatField("accountValue", accountState.MarginSummary.AccountValue); parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
if totalMarginUsed, parseErr = types.ParseFloatField("totalMarginUsed", accountState.MarginSummary.TotalMarginUsed); parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
summaryType = "MarginSummary (isolated margin)"
|
||||
summary = accountState.MarginSummary
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"nofx/logger"
|
||||
"nofx/trader/types"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -113,11 +114,26 @@ func (t *LighterTraderV2) GetAccountBalance() (*AccountBalance, error) {
|
||||
}
|
||||
|
||||
// Parse string values to float64
|
||||
availableBalance, _ := strconv.ParseFloat(accountInfo.AvailableBalance, 64)
|
||||
collateral, _ := strconv.ParseFloat(accountInfo.Collateral, 64)
|
||||
crossAssetValue, _ := strconv.ParseFloat(accountInfo.CrossAssetValue, 64)
|
||||
totalEquity, _ := strconv.ParseFloat(accountInfo.TotalEquity, 64)
|
||||
unrealizedPnl, _ := strconv.ParseFloat(accountInfo.UnrealizedPnl, 64)
|
||||
availableBalance, err := types.ParseFloatField("available_balance", accountInfo.AvailableBalance)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
collateral, err := types.ParseFloatField("collateral", accountInfo.Collateral)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
crossAssetValue, err := types.ParseFloatField("cross_asset_value", accountInfo.CrossAssetValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
totalEquity, err := types.ParseFloatField("total_equity", accountInfo.TotalEquity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
unrealizedPnl, err := types.ParseFloatField("unrealized_pnl", accountInfo.UnrealizedPnl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Use collateral as total equity if total_equity is 0
|
||||
if totalEquity == 0 {
|
||||
|
||||
@@ -55,13 +55,20 @@ func (t *OKXTrader) GetBalance() (map[string]interface{}, error) {
|
||||
var usdtAvail, usdtUPL float64
|
||||
for _, detail := range balance.Details {
|
||||
if detail.Ccy == "USDT" {
|
||||
usdtAvail, _ = strconv.ParseFloat(detail.AvailBal, 64)
|
||||
usdtUPL, _ = strconv.ParseFloat(detail.UPL, 64)
|
||||
if usdtAvail, err = types.ParseFloatField("availBal", detail.AvailBal); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if usdtUPL, err = types.ParseFloatField("upl", detail.UPL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
totalEq, _ := strconv.ParseFloat(balance.TotalEq, 64)
|
||||
totalEq, err := types.ParseFloatField("totalEq", balance.TotalEq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"totalWalletBalance": totalEq,
|
||||
|
||||
@@ -508,7 +508,7 @@ func (t *OKXTrader) cancelAlgoOrders(symbol string, orderType string) error {
|
||||
path := fmt.Sprintf("%s?instType=SWAP&instId=%s&ordType=conditional", okxAlgoPendingPath, instId)
|
||||
data, err := t.doRequest("GET", path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to get pending algo orders for %s: %w", symbol, err)
|
||||
}
|
||||
|
||||
var orders []struct {
|
||||
@@ -517,7 +517,7 @@ func (t *OKXTrader) cancelAlgoOrders(symbol string, orderType string) error {
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &orders); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to parse pending algo orders for %s: %w", symbol, err)
|
||||
}
|
||||
|
||||
canceledCount := 0
|
||||
@@ -552,7 +552,7 @@ func (t *OKXTrader) CancelAllOrders(symbol string) error {
|
||||
path := fmt.Sprintf("%s?instType=SWAP&instId=%s", okxPendingOrdersPath, instId)
|
||||
data, err := t.doRequest("GET", path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to get pending orders for %s: %w", symbol, err)
|
||||
}
|
||||
|
||||
var orders []struct {
|
||||
@@ -561,7 +561,7 @@ func (t *OKXTrader) CancelAllOrders(symbol string) error {
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &orders); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to parse pending orders for %s: %w", symbol, err)
|
||||
}
|
||||
|
||||
// Batch cancel
|
||||
@@ -570,11 +570,15 @@ func (t *OKXTrader) CancelAllOrders(symbol string) error {
|
||||
"instId": order.InstId,
|
||||
"ordId": order.OrdId,
|
||||
}
|
||||
t.doRequest("POST", okxCancelOrderPath, body)
|
||||
if _, err := t.doRequest("POST", okxCancelOrderPath, body); err != nil {
|
||||
logger.Infof(" ⚠ Failed to cancel order %s for %s: %v", order.OrdId, symbol, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Also cancel algo orders
|
||||
t.cancelAlgoOrders(symbol, "")
|
||||
if err := t.cancelAlgoOrders(symbol, ""); err != nil {
|
||||
logger.Infof(" ⚠ Failed to cancel algo orders for %s: %v", symbol, err)
|
||||
}
|
||||
|
||||
if len(orders) > 0 {
|
||||
logger.Infof(" ✓ Canceled all pending orders for %s", symbol)
|
||||
|
||||
22
trader/types/parse.go
Normal file
22
trader/types/parse.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ParseFloatField parses a numeric string field from an exchange API response.
|
||||
// An empty string is treated as zero, since exchanges commonly omit fields
|
||||
// that have no value (e.g. unrealized PnL with no open position). A non-empty
|
||||
// value that fails to parse returns an error naming the field, so a malformed
|
||||
// API response surfaces instead of silently becoming a zero balance.
|
||||
func ParseFloatField(field, value string) (float64, error) {
|
||||
if value == "" {
|
||||
return 0, nil
|
||||
}
|
||||
v, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse %s value %q: %w", field, value, err)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
44
trader/types/parse_test.go
Normal file
44
trader/types/parse_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseFloatField(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
field string
|
||||
input string
|
||||
want float64
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "normal value", field: "totalEq", input: "1234.56", want: 1234.56},
|
||||
{name: "zero", field: "totalEq", input: "0", want: 0},
|
||||
{name: "negative", field: "upl", input: "-12.5", want: -12.5},
|
||||
{name: "empty string treated as zero", field: "upl", input: "", want: 0},
|
||||
{name: "garbage returns error", field: "totalEq", input: "abc", wantErr: true},
|
||||
{name: "null literal returns error", field: "totalEq", input: "null", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseFloatField(tt.field, tt.input)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("ParseFloatField(%q, %q) expected error, got nil", tt.field, tt.input)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.field) {
|
||||
t.Errorf("error %q should mention field name %q", err.Error(), tt.field)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("ParseFloatField(%q, %q) unexpected error: %v", tt.field, tt.input, err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("ParseFloatField(%q, %q) = %v, want %v", tt.field, tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user