Files
gocryptotrader/common/common_test.go
Ryan O'Hara-Reid dcf596c72b bybit: enable multiconnection handling across websocket endpoints (#1670)
* glorious: whooops

* gk: nits

* Leak issue and edge case

* Websocket: Add SendMessageReturnResponses

* whooooooopsie

* gk: nitssssss

* Update exchanges/stream/stream_match.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/stream/stream_match_test.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* linter: appease the linter gods

* gk: nits

* gk: drain brain

* started

* more changes before merge match pr

* gateio: still building out

* gateio: finish spot

* fix up tests in gateio

* Add tests for stream package

* rm unused field

* glorious: nits

* rn files, specifically set function names to asset and offload routing to websocket type.

* linter: fix

* Add futures websocket request support

* gateio: integrate with IBOTExchange (cherry pick my nose)

* linter: fix

* glorious: nits

* add counter and update gateio

* fix collision issue

* Update exchanges/stream/websocket.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits

* add tests

* linter: fix

* After merge

* Add error connection info

* upgrade to upstream merge

* Fix edge case where it does not reconnect made by an already closed connection

* stream coverage

* glorious: nits

* glorious: nits removed asset error handling in stream package

* linter: fix

* rm block

* Add basic readme

* fix asset enabled flush cycle for multi connection

* spella: fix

* linter: fix

* Add glorious suggestions, fix some race thing

* reinstate name before any routine gets spawned

* stop on error in mock tests

* glorious: nits

* Set correct price

* glorious: nits found in CI build

* Add test for drain, bumped wait times as there seems to be something happening on macos CI builds, used context.WithTimeout because its instant.

* mutex across shutdown and connect for protection

* lint: fix

* test time withoffset, reinstate stop

* fix whoops

* const trafficCheckInterval; rm testmain

* y

* fix lint

* bump time check window

* stream: fix intermittant test failures while testing routines and remove code that is not needed.

* spells

* cant do what I did

* protect race due to routine.

* update testURL

* use mock websocket connection instead of test URL's

* linter: fix

* remove url because its throwing errors on CI builds

* connections drop all the time, don't need to worry about not being able to echo back ws data as it can be easily reviewed _test file side.

* remove another superfluous url thats not really set up for this

* spawn overwatch routine when there is no errors, inline checker instead of waiting for a time period, add sleep inline with echo handler as this is really quick and wanted to ensure that latency is handing correctly

* linter: fixerino uperino

* fix ID bug, why I do this, I don't know.

* glorious: panix

* linter: things

* whoops

* dont need to make consecutive Unix() calls

* websocket: fix potential panic on error and no responses and adding waitForResponses

* bybit: enable multiconnection handling across websocket endpoints

* rm debug lines

* rm json parser and handle in json package instead

* in favour of json package unmarshalling

* fix processing issues with tickers

* linter: fix

* linter: fix again

* * change field name OutboundRequestSignature to WrapperDefinedConnectionSignature for agnostic inbound and outbound connections.
* change method name GetOutboundConnection to GetConnection for agnostic inbound and outbound connections.
* drop outbound field map for improved performance just using a range and field check (less complex as well)
* change field name connections to connectionToWrapper for better clarity

* spells and magic and wands

* merge: fixup

* linter: fix

* spelling: fix

* glorious: nits

* comparable check for signature

* mv err var

* glorious: nits and stuff

* attempt to fix race

* linter: fix

* fix tests

* types/time: strict usage of time type for usage with unix timestamps

* fix tests etc

* glorious: nits

* gk: nits; engine log cleanup

* gk: nits; OCD

* gk: nits; move function change file names

* gk: nits; 🚀

* gk: nits; convert variadic function and message inspection to interface and include a specific function for that handling so as to not need nil on every call

* gk: nits; continued

* gk: engine nits; rm loaded exchange

* gk: nits; drop WebsocketLoginResponse

* stream: Add match method EnsureMatchWithData

* gk: nits; rn Inspect to IsFinal

* gk: nits; rn to MessageFilter

* linter: fix

* gateio: update rate limit definitions (cherry-pick)

* Add test and missing

* Shared REST rate limit definitions with Websocket service, set lookup item to nil for systems that do not require rate limiting; add glorious nit

* integrate rate limits for websocket trading spot

* bybit: split public and private processing to dedicated handler add supporting function and tests

* use correct handler for private inbound connection

* conform to match upstream changes

* standardise names to upstream style

* fix wrapper standards test when sending a auth request through a websocket connection

* whoops

* Update exchanges/gateio/gateio_types.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits

* linter: fix

* linter: overload

* whoops

* spelling fixes on recent merge

* glorious: nits

* linter: fix?

* glorious: nits

* gk: assert errors touched

* gk: unexport derive functions

* gk: nitssssssss

* fix test

* gk: nitters v1

* gk: http status

* gk/nits: Add getAssetFromFuturesPair

* gk: nits single response when submitting

* gk: new pair with delimiter in tests

* gk: param update slice to slice of pointers

* gk: add asset type in params, includes t.Context() for tests

* linter: fix

* linter: fix

* fix merge whoopsie

* glorious: nits

* gk: nit

* linter: fix

* glorious: nits

* linter/misc: fix and remove meows

* okx: update requestID gen func without func wrapping

* RM: functions not needed

* Update docs/ADD_NEW_EXCHANGE.md

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* gk: nitsssssss

* linter: fix

* Update exchanges/bybit/bybit_test.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/bybit/bybit_test.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* gk: nit words

* cranktakular: nits

* linter: fix

* cranktakular: nits and expand coverage

* linter: fix?

* misc fix

* cranktakular: missing nit which I thumbed up but did not do. Sillllllly billlyyyy nilllyyy

* cranktakular: nits

* cranktakular: purge DCP ref/handling and add another TODO

* Update exchanges/bybit/bybit_websocket.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits

* fix test

* fix alignment issue and rm println

* Update exchanges/bybit/bybit_websocket.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* Update exchanges/bybit/bybit_websocket.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: fix

* Update exchanges/bybit/bybit_websocket.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update common/common.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update common/common_test.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/bybit/bybit_test.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* gk: nits

* gk: nit with test

---------

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
2025-08-08 14:22:29 +10:00

683 lines
20 KiB
Go

package common
import (
"errors"
"fmt"
"net/http"
"net/url"
"os"
"os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/common/file"
)
func TestSendHTTPRequest(t *testing.T) {
// t.Parallel() not used to maintain code coverage for assigning the default
// HTTPClient.
methodPost := "pOst"
methodGet := "GeT"
methodDelete := "dEleTe"
methodGarbage := "ding"
headers := make(map[string]string)
headers["Content-Type"] = "application/x-www-form-urlencoded"
_, err := SendHTTPRequest(t.Context(),
methodGarbage, "https://www.google.com", headers,
strings.NewReader(""), true,
)
if err == nil {
t.Error("Expected error 'invalid HTTP method specified'")
}
_, err = SendHTTPRequest(t.Context(),
methodPost, "https://www.google.com", headers,
strings.NewReader(""), true,
)
if err != nil {
t.Error(err)
}
_, err = SendHTTPRequest(t.Context(),
methodGet, "https://www.google.com", headers,
strings.NewReader(""), true,
)
if err != nil {
t.Error(err)
}
err = SetHTTPUserAgent("GCTbot/1337.69 (+http://www.lol.com/)")
require.NoError(t, err)
_, err = SendHTTPRequest(t.Context(),
methodDelete, "https://www.google.com", headers,
strings.NewReader(""), true,
)
if err != nil {
t.Error(err)
}
_, err = SendHTTPRequest(t.Context(),
methodGet, ":missingprotocolscheme", headers,
strings.NewReader(""), true,
)
if err == nil {
t.Error("Common HTTPRequest accepted missing protocol")
}
_, err = SendHTTPRequest(t.Context(),
methodGet, "test://unsupportedprotocolscheme", headers,
strings.NewReader(""), true,
)
if err == nil {
t.Error("Common HTTPRequest accepted invalid protocol")
}
}
func TestSetHTTPClientWithTimeout(t *testing.T) {
t.Parallel()
err := SetHTTPClientWithTimeout(-0)
require.ErrorIs(t, err, errCannotSetInvalidTimeout)
err = SetHTTPClientWithTimeout(time.Second * 15)
require.NoError(t, err)
}
func TestSetHTTPUserAgent(t *testing.T) {
t.Parallel()
err := SetHTTPUserAgent("")
require.ErrorIs(t, err, errUserAgentInvalid)
err = SetHTTPUserAgent("testy test")
require.NoError(t, err)
}
func TestSetHTTPClient(t *testing.T) {
t.Parallel()
err := SetHTTPClient(nil)
require.ErrorIs(t, err, errHTTPClientInvalid)
err = SetHTTPClient(new(http.Client))
require.NoError(t, err)
}
func TestIsEnabled(t *testing.T) {
t.Parallel()
expected := "Enabled"
actual := IsEnabled(true)
if actual != expected {
t.Errorf("Expected %s. Actual %s", expected, actual)
}
expected = "Disabled"
actual = IsEnabled(false)
if actual != expected {
t.Errorf("Expected %s. Actual %s", expected, actual)
}
}
func TestIsValidCryptoAddress(t *testing.T) {
t.Parallel()
tests := []struct {
name, addr, code string
err error
}{
{"Valid BTC legacy", "1Mz7153HMuxXTuR2R1t78mGSdzaAtNbBWX", "bTC", nil},
{"Valid BTC bech32", "bc1qw508d6qejxtdg4y5r3zarvaly0c5xw7kv8f3t4", "bTC", nil},
{"Invalid BTC (too long)", "an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", "bTC", ErrAddressIsEmptyOrInvalid},
{"Valid BTC bech32 (longer)", "bc1qc7slrfxkknqcq2jevvvkdgvrt8080852dfjewde450xdlk4ugp7szw5tk9", "bTC", nil},
{"Invalid BTC (starts with 0)", "0Mz7153HMuxXTuR2R1t78mGSdzaAtNbBWX", "bTC", ErrAddressIsEmptyOrInvalid},
{"Invalid LTC (BTC address)", "1Mz7153HMuxXTuR2R1t78mGSdzaAtNbBWX", "lTc", ErrAddressIsEmptyOrInvalid},
{"Valid LTC", "3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj", "lTc", nil},
{"Invalid LTC (starts with N)", "NCDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj", "lTc", ErrAddressIsEmptyOrInvalid},
{"Valid ETH", "0xb794f5ea0ba39494ce839613fffba74279579268", "eth", nil},
{"Invalid ETH (starts with xx)", "xxb794f5ea0ba39494ce839613fffba74279579268", "eth", ErrAddressIsEmptyOrInvalid},
{"Unsupported crypto", "xxb794f5ea0ba39494ce839613fffba74279579268", "wif", ErrUnsupportedCryptocurrency},
{"Empty address", "", "btc", ErrAddressIsEmptyOrInvalid},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
require.ErrorIs(t, IsValidCryptoAddress(tc.addr, tc.code), tc.err)
})
}
}
func TestSliceDifference(t *testing.T) {
t.Parallel()
assert.ElementsMatch(t, []string{"world", "go"}, SliceDifference([]string{"hello", "world"}, []string{"hello", "go"}))
assert.ElementsMatch(t, []int64{1, 2, 5, 6}, SliceDifference([]int64{1, 2, 3, 4}, []int64{3, 4, 5, 6}))
assert.ElementsMatch(t, []float64{1.1, 4.4}, SliceDifference([]float64{1.1, 2.2, 3.3}, []float64{2.2, 3.3, 4.4}))
type mixedType struct {
A string
B int
}
assert.ElementsMatch(t, []mixedType{{"A", 1}, {"D", 4}}, SliceDifference([]mixedType{{"A", 1}, {"B", 2}, {"C", 3}}, []mixedType{{"B", 2}, {"C", 3}, {"D", 4}}))
assert.ElementsMatch(t, []int{1, 2, 3}, SliceDifference([]int{}, []int{1, 2, 3}))
assert.ElementsMatch(t, []int{1, 2, 3}, SliceDifference([]int{1, 2, 3}, []int{}))
assert.Empty(t, SliceDifference([]int{}, []int{}))
}
func TestStringSliceContains(t *testing.T) {
t.Parallel()
originalHaystack := []string{"hello", "world", "USDT", "Contains", "string"}
assert.True(t, StringSliceContains(originalHaystack, "USD"), "Should contain 'USD'")
assert.False(t, StringSliceContains(originalHaystack, "thing"), "Should not contain 'thing'")
}
func TestStringSliceCompareInsensitive(t *testing.T) {
t.Parallel()
originalHaystack := []string{"hello", "WoRld", "USDT", "Contains", "string"}
assert.False(t, StringSliceCompareInsensitive(originalHaystack, "USD"), "Should not contain 'USD'")
assert.True(t, StringSliceCompareInsensitive(originalHaystack, "WORLD"), "Should find 'WoRld'")
}
func TestStringSliceContainsInsensitive(t *testing.T) {
t.Parallel()
originalHaystack := []string{"bLa", "BrO", "sUp"}
assert.True(t, StringSliceContainsInsensitive(originalHaystack, "Bla"), "Should contain 'Bla'")
assert.False(t, StringSliceContainsInsensitive(originalHaystack, "ning"), "Should not contain 'ning'")
}
func TestYesOrNo(t *testing.T) {
t.Parallel()
if !YesOrNo("y") {
t.Error("Common YesOrNo Error.")
}
if !YesOrNo("yes") {
t.Error("Common YesOrNo Error.")
}
if YesOrNo("ding") {
t.Error("Common YesOrNo Error.")
}
}
func TestEncodeURLValues(t *testing.T) {
t.Parallel()
urlstring := "https://www.test.com"
expectedOutput := `https://www.test.com?env=TEST%2FDATABASE&format=json`
values := url.Values{}
values.Set("format", "json")
values.Set("env", "TEST/DATABASE")
output := EncodeURLValues(urlstring, values)
if output != expectedOutput {
t.Error("common EncodeURLValues error")
}
}
func TestExtractHostOrDefault(t *testing.T) {
t.Parallel()
assert.Equal(t, "localhost", ExtractHostOrDefault("localhost:1337"))
assert.Equal(t, "localhost", ExtractHostOrDefault(":1337"))
assert.Equal(t, "192.168.1.100", ExtractHostOrDefault("192.168.1.100:1337"))
}
func TestExtractPortOrDefault(t *testing.T) {
t.Parallel()
assert.Equal(t, 1337, ExtractPortOrDefault("localhost:1337"))
assert.Equal(t, 80, ExtractPortOrDefault("localhost"))
}
func TestGetURIPath(t *testing.T) {
t.Parallel()
// mapping of input vs expected result
testTable := map[string]string{
"https://api.pro.coinbase.com/accounts": "/accounts",
"https://api.pro.coinbase.com/accounts?a=1&b=2": "/accounts?a=1&b=2",
"http://www.google.com/accounts?!@#$%;^^": "",
}
for testInput, expectedOutput := range testTable {
assert.Equal(t, expectedOutput, GetURIPath(testInput))
}
}
func TestGetExecutablePath(t *testing.T) {
t.Parallel()
if _, err := GetExecutablePath(); err != nil {
t.Errorf("Common GetExecutablePath. Error: %s", err)
}
}
func TestGetDefaultDataDir(t *testing.T) {
switch runtime.GOOS {
case "windows":
dir, ok := os.LookupEnv("APPDATA")
if !ok {
t.Fatal("APPDATA is not set")
}
dir = filepath.Join(dir, "GoCryptoTrader")
actualOutput := GetDefaultDataDir(runtime.GOOS)
if actualOutput != dir {
t.Fatalf("Unexpected result. Got: %v Expected: %v", actualOutput, dir)
}
default:
var dir string
usr, err := user.Current()
if err == nil {
dir = usr.HomeDir
} else {
var err error
dir, err = os.UserHomeDir()
if err != nil {
dir = "."
}
}
dir = filepath.Join(dir, ".gocryptotrader")
actualOutput := GetDefaultDataDir(runtime.GOOS)
if actualOutput != dir {
t.Fatalf("Unexpected result. Got: %v Expected: %v", actualOutput, dir)
}
}
}
func TestCreateDir(t *testing.T) {
switch runtime.GOOS {
case "windows":
// test for looking up an invalid directory
err := CreateDir("")
if err == nil {
t.Fatal("expected err due to invalid path, but got nil")
}
// test for a directory that exists
dir, ok := os.LookupEnv("TEMP")
if !ok {
t.Fatal("LookupEnv failed. TEMP is not set")
}
err = CreateDir(dir)
if err != nil {
t.Fatalf("CreateDir failed. Err: %v", err)
}
// test for creating a directory
dir, ok = os.LookupEnv("APPDATA")
if !ok {
t.Fatal("LookupEnv failed. APPDATA is not set")
}
dir = filepath.Join(dir, "GoCryptoTrader", "TestFileASDFG")
err = CreateDir(dir)
if err != nil {
t.Fatalf("CreateDir failed. Err: %v", err)
}
err = os.Remove(dir)
if err != nil {
t.Fatalf("Failed to remove file. Err: %v", err)
}
default:
err := CreateDir("")
if err == nil {
t.Fatal("expected err due to invalid path, but got nil")
}
dir := "/home"
err = CreateDir(dir)
if err != nil {
t.Fatalf("CreateDir failed. Err: %v", err)
}
var ok bool
dir, ok = os.LookupEnv("HOME")
if !ok {
t.Fatal("LookupEnv of HOME failed")
}
dir = filepath.Join(dir, ".gocryptotrader", "TestFileASFG")
err = CreateDir(dir)
if err != nil {
t.Errorf("CreateDir failed. Err: %s", err)
}
err = os.Remove(dir)
if err != nil {
t.Fatalf("Failed to remove file. Err: %v", err)
}
}
}
func TestChangePermission(t *testing.T) {
t.Parallel()
testDir := filepath.Join(os.TempDir(), "TestFileASDFGHJ")
switch runtime.GOOS {
case "windows":
err := ChangePermission("*")
if err == nil {
t.Fatal("expected an error on non-existent path")
}
err = os.Mkdir(testDir, 0o777)
if err != nil {
t.Fatalf("Mkdir failed. Err: %v", err)
}
err = ChangePermission(testDir)
if err != nil {
t.Fatalf("ChangePerm was unsuccessful. Err: %v", err)
}
_, err = os.Stat(testDir)
if err != nil {
t.Fatalf("os.Stat failed. Err: %v", err)
}
err = os.Remove(testDir)
if err != nil {
t.Fatalf("os.Remove failed. Err: %v", err)
}
default:
err := ChangePermission("")
if err == nil {
t.Fatal("expected an error on non-existent path")
}
err = os.Mkdir(testDir, 0o777)
if err != nil {
t.Fatalf("Mkdir failed. Err: %v", err)
}
err = ChangePermission(testDir)
if err != nil {
t.Fatalf("ChangePerm was unsuccessful. Err: %v", err)
}
var a os.FileInfo
a, err = os.Stat(testDir)
if err != nil {
t.Fatalf("os.Stat failed. Err: %v", err)
}
if a.Mode().Perm() != file.DefaultPermissionOctal {
t.Fatalf("expected file permissions differ. expecting file.DefaultPermissionOctal got %#o", a.Mode().Perm())
}
err = os.Remove(testDir)
if err != nil {
t.Fatalf("os.Remove failed. Err: %v", err)
}
}
}
func TestAddPaddingOnUpperCase(t *testing.T) {
t.Parallel()
testCases := []struct {
Supplied string
Expected string
}{
{
// empty
},
{
Supplied: "ExpectedHTTPRainbow",
Expected: "Expected HTTP Rainbow",
},
{
Supplied: "SmellyCatSmellsBad",
Expected: "Smelly Cat Smells Bad",
},
{
Supplied: "Gronk",
Expected: "Gronk",
},
}
for x := range testCases {
if received := AddPaddingOnUpperCase(testCases[x].Supplied); received != testCases[x].Expected {
t.Fatalf("received '%v' but expected '%v'", received, testCases[x].Expected)
}
}
}
func TestErrors(t *testing.T) {
t.Parallel()
e1 := errors.New("inconsistent gravity")
e2 := errors.New("barely marginal interest in your story")
e3 := errors.New("error making dinner")
e4 := errors.New("inconsistent gravy")
e5 := errors.New("add vodka")
// Nil tests
assert.NoError(t, AppendError(nil, nil), "Append nil to nil should nil")
assert.Same(t, AppendError(e1, nil), e1, "Append nil to e1 should e1")
assert.Same(t, AppendError(nil, e2), e2, "Append e2 to nil should e2")
// Vanila error tests
err := AppendError(AppendError(AppendError(nil, e1), e2), e1)
assert.ErrorContains(t, err, "inconsistent gravity, barely marginal interest in your story, inconsistent gravity", "Should format consistently")
assert.ErrorIs(t, err, e1, "Should have inconsistent gravity")
assert.ErrorIs(t, err, e2, "Should be bored by your witty tales")
err = ExcludeError(err, e2)
assert.ErrorIs(t, err, e1, "Should still be bored")
assert.NotErrorIs(t, err, e2, "Should not be an e2")
me, ok := err.(*multiError)
if assert.True(t, ok, "Should be a multiError") {
assert.Len(t, me.errs, 2, "Should only have 2 errors")
}
err = ExcludeError(err, e1)
assert.NoError(t, err, "Error should be empty")
err = ExcludeError(err, e1)
assert.NoError(t, err, "Excluding a nil error should be okay")
// Wrapped error tests
err = fmt.Errorf("%w: %w", e3, fmt.Errorf("%w: %w", e4, e5))
assert.ErrorIs(t, ExcludeError(err, e4), e3, "Excluding e4 should retain e3")
assert.ErrorIs(t, ExcludeError(err, e4), e5, "Excluding e4 should retain the vanilla co-wrapped e5")
assert.NotErrorIs(t, ExcludeError(err, e4), e4, "e4 should be excluded")
assert.ErrorIs(t, ExcludeError(err, e5), e3, "Excluding e5 should retain e3")
assert.ErrorIs(t, ExcludeError(err, e5), e4, "Excluding e5 should retain the vanilla co-wrapped e4")
assert.NotErrorIs(t, ExcludeError(err, e5), e5, "e5 should be excluded")
// Hybrid tests
err = AppendError(fmt.Errorf("%w: %w", e4, e5), e3)
assert.ErrorIs(t, ExcludeError(err, e4), e3, "Excluding e4 should retain e3")
assert.ErrorIs(t, ExcludeError(err, e4), e5, "Excluding e4 should retain the vanilla co-wrapped e5")
assert.NotErrorIs(t, ExcludeError(err, e4), e4, "e4 should be excluded")
assert.ErrorIs(t, ExcludeError(err, e5), e3, "Excluding e5 should retain e3")
assert.ErrorIs(t, ExcludeError(err, e5), e4, "Excluding e5 should retain the vanilla co-wrapped e4")
assert.NotErrorIs(t, ExcludeError(err, e5), e5, "e4 should be excluded")
// Formatting retention
err = AppendError(e1, fmt.Errorf("%w: Run out of %q: %w", e3, "sausages", e5))
assert.ErrorIs(t, err, e1, "Should be an e1")
assert.ErrorIs(t, err, e3, "Should be an e3")
assert.ErrorIs(t, err, e5, "Should be an e5")
assert.ErrorContains(t, err, "sausages", "Should know about secret sausages")
}
func TestParseStartEndDate(t *testing.T) {
t.Parallel()
pt := time.Date(1999, 1, 1, 0, 0, 0, 0, time.Local)
ft := time.Date(2222, 1, 1, 0, 0, 0, 0, time.Local)
et := time.Date(2020, 1, 1, 1, 0, 0, 0, time.Local)
nt := time.Time{}
err := StartEndTimeCheck(nt, nt)
assert.ErrorIs(t, err, ErrDateUnset)
err = StartEndTimeCheck(et, nt)
assert.ErrorIs(t, err, ErrDateUnset)
err = StartEndTimeCheck(et, zeroValueUnix)
assert.ErrorIs(t, err, ErrDateUnset)
err = StartEndTimeCheck(zeroValueUnix, et)
assert.ErrorIs(t, err, ErrDateUnset)
err = StartEndTimeCheck(et, et)
assert.ErrorIs(t, err, ErrStartEqualsEnd)
err = StartEndTimeCheck(et, pt)
assert.ErrorIs(t, err, ErrStartAfterEnd)
err = StartEndTimeCheck(ft, ft.Add(time.Hour))
assert.ErrorIs(t, err, ErrStartAfterTimeNow)
err = StartEndTimeCheck(pt, et)
assert.NoError(t, err)
}
func TestGetAssertError(t *testing.T) {
err := GetTypeAssertError("*[]string", float64(0))
if err.Error() != "type assert failure from float64 to *[]string" {
t.Fatal(err)
}
err = GetTypeAssertError("<nil>", nil)
if err.Error() != "type assert failure from <nil> to <nil>" {
t.Fatal(err)
}
err = GetTypeAssertError("bruh", struct{}{})
require.ErrorIs(t, err, ErrTypeAssertFailure)
err = GetTypeAssertError("string", struct{}{})
if err.Error() != "type assert failure from struct {} to string" {
t.Errorf("unexpected error message: %v", err)
}
err = GetTypeAssertError("string", struct{}{}, "bidSize")
if err.Error() != "type assert failure from struct {} to string for: bidSize" {
t.Errorf("unexpected error message: %v", err)
}
}
func TestMatchesEmailPattern(t *testing.T) {
success := MatchesEmailPattern("someone semail")
if success {
t.Error("MatchesEmailPattern() unexpected test validation result")
}
success = MatchesEmailPattern("someone esemail@gmail")
if success {
t.Error("MatchesEmailPattern() unexpected test validation result")
}
success = MatchesEmailPattern("123@gmail")
if !success {
t.Error("MatchesEmailPattern() unexpected test validation result")
}
success = MatchesEmailPattern("someonesemail@email.com")
if !success {
t.Error("MatchesEmailPattern() unexpected test validation result")
}
}
func TestGenerateRandomString(t *testing.T) {
t.Parallel()
sample, err := GenerateRandomString(5, NumberCharacters)
if err != nil {
t.Errorf("GenerateRandomString() %v", err)
}
value, err := strconv.Atoi(sample)
if len(sample) != 5 || err != nil || value < 0 {
t.Error("GenerateRandomString() unexpected test validation result")
}
sample, err = GenerateRandomString(5)
if err != nil {
t.Errorf("GenerateRandomString() %v", err)
}
values, err := strconv.ParseInt(sample, 10, 64)
if len(sample) != 5 || err != nil || values < 0 {
t.Error("GenerateRandomString() unexpected test validation result")
}
_, err = GenerateRandomString(1, "")
if err == nil {
t.Errorf("GenerateRandomString() expecting %s, but found %v", "invalid characters, character must not be empty", err)
}
sample, err = GenerateRandomString(0, "")
if err != nil && !strings.Contains(err.Error(), "invalid length") {
t.Errorf("GenerateRandomString() %v", err)
}
if sample != "" {
t.Error("GenerateRandomString() unexpected test validation result")
}
}
// TestErrorCollector exercises the error collector
func TestErrorCollector(t *testing.T) {
e := CollectErrors(4)
for i := range 4 {
go func() {
if i%2 == 0 {
e.C <- errors.New("Collected error")
} else {
e.C <- nil
}
e.Wg.Done()
}()
}
v := e.Collect()
errs, ok := v.(*multiError)
require.True(t, ok, "Must return a multiError")
assert.Len(t, errs.Unwrap(), 2, "Should have 2 errors")
}
// TestBatch ensures the Batch function does not regress into common behavioural faults if implementation changes
func TestBatch(t *testing.T) {
s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
b := Batch(s, 3)
require.Len(t, b, 4)
assert.Len(t, b[0], 3)
assert.Len(t, b[3], 1)
b[0][0] = 42
assert.Equal(t, 1, s[0], "Changing the batches should not change the source")
require.NotPanics(t, func() { Batch(s, -1) }, "Must not panic on negative batch size")
done := make(chan any, 1)
go func() { done <- Batch(s, 0) }()
require.Eventually(t, func() bool { return len(done) > 0 }, time.Second, time.Millisecond, "Batch 0 must not hang")
for _, i := range []int{-1, 0, 50} {
b = Batch(s, i)
require.Lenf(t, b, 1, "A batch size of %v must produce a single batch", i)
assert.Lenf(t, b[0], len(s), "A batch size of %v should produce a single batch", i)
}
}
type A int
func (a A) String() string {
return strconv.Itoa(int(a))
}
func TestSortStrings(t *testing.T) {
assert.Equal(t, []A{1, 2, 5, 6}, SortStrings([]A{6, 2, 5, 1}))
}
func TestCounter(t *testing.T) {
t.Parallel()
c := Counter{}
c.n.Store(-5)
require.Equal(t, int64(1), c.IncrementAndGet(), "Adding to a negative Counter must reset to zero and then increment")
require.Equal(t, int64(2), c.IncrementAndGet())
}
// 683185328 1.787 ns/op 0 B/op 0 allocs/op
func BenchmarkCounter(b *testing.B) {
c := Counter{}
for b.Loop() {
c.IncrementAndGet()
}
}
func TestNilGuard(t *testing.T) {
t.Parallel()
err := NilGuard((*int)(nil))
assert.ErrorIs(t, err, ErrNilPointer)
assert.ErrorContains(t, err, "*int")
s := "normal input"
err = NilGuard(&s, 2, &[]int{4, 5, 6}, []int{1, 2, 3}, new(A))
assert.NoError(t, err)
err = NilGuard(&s, nil, (*int)(nil))
assert.ErrorIs(t, err, ErrNilPointer)
assert.ErrorContains(t, err, "*int")
var mErr *multiError
require.ErrorAs(t, err, &mErr, "err must be a multiError")
assert.Len(t, mErr.Unwrap(), 2, "Should get 2 errors back")
assert.ErrorIs(t, NilGuard(nil), ErrNilPointer, "Unusual input of an untyped nil should still error correctly")
err = NilGuard()
require.NoError(t, err, "NilGuard with no arguments must not error")
}