Merge pull request #8 from tribalwarshelp/feat/add-tests

feat: add tests - twdataloader
This commit is contained in:
Dawid Wysokiński 2021-07-17 07:51:02 +02:00 committed by GitHub
commit 2875c56885
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 923 additions and 72 deletions

5
go.mod
View File

@ -7,7 +7,6 @@ require (
github.com/Kichiyaki/gopgutil/v10 v10.0.0-20210521204542-cc672e361b3d
github.com/go-pg/pg/v10 v10.10.2
github.com/pkg/errors v0.9.1
github.com/vmihailenco/msgpack/v5 v5.3.1 // indirect
go.opentelemetry.io/otel v0.20.0 // indirect
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
github.com/stretchr/testify v1.7.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

17
go.sum
View File

@ -16,10 +16,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-pg/pg/v10 v10.9.1 h1:kU4t84zWGGaU0Qsu49FbNtToUVrlSTkNOngW8aQmwvk=
github.com/go-pg/pg/v10 v10.9.1/go.mod h1:rgmTPgHgl5EN2CNKKoMwC7QT62t8BqsdpEkUQuiZMQs=
github.com/go-pg/pg/v10 v10.10.1 h1:82lLX4KGs2wOFOvVVIICoU0Si1fLu6Aitniu73HaDuM=
github.com/go-pg/pg/v10 v10.10.1/go.mod h1:EmoJGYErc+stNN/1Jf+o4csXuprjxcRztBnn6cHe38E=
github.com/go-pg/pg/v10 v10.10.2 h1:8G2DdKrB3/0nRIlpur0HySEWBJnHYUByiC0ko4XzE8w=
github.com/go-pg/pg/v10 v10.10.2/go.mod h1:EmoJGYErc+stNN/1Jf+o4csXuprjxcRztBnn6cHe38E=
github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
@ -41,7 +38,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@ -84,17 +80,9 @@ github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgq
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
go.opentelemetry.io/otel v0.19.0/go.mod h1:j9bF567N9EfomkSidSfmMwIwIBuP37AMAIzVW85OxSg=
go.opentelemetry.io/otel v0.20.0 h1:eaP0Fqu7SXHwvjiqDq83zImeehOHX8doTvU9AwXON8g=
go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=
go.opentelemetry.io/otel/metric v0.19.0/go.mod h1:8f9fglJPRnXuskQmKpnad31lcLJ2VmNNqIsx/uIwBSc=
go.opentelemetry.io/otel/metric v0.20.0 h1:4kzhXFP+btKm4jwxpjIqjs41A7MakRFUS86bqLHTIw8=
go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=
go.opentelemetry.io/otel/oteltest v0.19.0/go.mod h1:tI4yxwh8U21v7JD6R3BcA/2+RBoTKFexE/PJ/nSO7IA=
go.opentelemetry.io/otel/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw=
go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=
go.opentelemetry.io/otel/trace v0.19.0/go.mod h1:4IXiNextNOpPnRlI4ryK69mn5iC84bjBWZQA5DXz/qg=
go.opentelemetry.io/otel/trace v0.20.0 h1:1DL6EXUdcg95gukhuRRvLDO/4X5THh/5dIV52lqtnbw=
go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -131,8 +119,6 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 h1:iGu644GcxtEcrInvDsQRCwJjtCIOlT2V7IRt6ah2Whw=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -175,8 +161,9 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w=

View File

@ -26,6 +26,7 @@ const (
EndpointKillAllTribeNotGzipped = "/map/kill_all_tribe.txt"
EndpointConquer = "/map/conquer.txt.gz"
EndpointConquerNotGzipped = "/map/conquer.txt"
EndpointGetConquer = "/interface.php?func=get_conquer&since=%d"
endpointInterface = "/interface.php"
EndpointGetConquer = endpointInterface + "?func=get_conquer&since=%d"
EndpointGetServers = "/backend/get_servers.php"
)

View File

@ -5,6 +5,7 @@ import (
"encoding/csv"
"io"
"net/http"
"net/http/httptest"
"time"
)
@ -22,3 +23,144 @@ func uncompressAndReadCsvLines(r io.Reader) ([][]string, error) {
defer uncompressedStream.Close()
return csv.NewReader(uncompressedStream).ReadAll()
}
type handlers struct {
getServers http.HandlerFunc
killAll http.HandlerFunc
killAtt http.HandlerFunc
killDef http.HandlerFunc
killSup http.HandlerFunc
killAllTribe http.HandlerFunc
killAttTribe http.HandlerFunc
killDefTribe http.HandlerFunc
player http.HandlerFunc
tribe http.HandlerFunc
village http.HandlerFunc
conquer http.HandlerFunc
getConquer http.HandlerFunc
}
func (h *handlers) init() {
noop := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
})
if h.getServers == nil {
h.getServers = noop
}
if h.killAll == nil {
h.killAll = noop
}
if h.killAtt == nil {
h.killAtt = noop
}
if h.killDef == nil {
h.killDef = noop
}
if h.killSup == nil {
h.killSup = noop
}
if h.killAllTribe == nil {
h.killAllTribe = noop
}
if h.killAttTribe == nil {
h.killAttTribe = noop
}
if h.killDefTribe == nil {
h.killDefTribe = noop
}
if h.player == nil {
h.player = noop
}
if h.tribe == nil {
h.tribe = noop
}
if h.village == nil {
h.village = noop
}
if h.conquer == nil {
h.conquer = noop
}
if h.getConquer == nil {
h.getConquer = noop
}
}
func prepareTestServer(h *handlers) *httptest.Server {
if h == nil {
h = &handlers{}
}
h.init()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.WriteHeader(http.StatusNotFound)
return
}
switch r.URL.Path {
case EndpointGetServers:
h.getServers(w, r)
return
case EndpointKillAll:
h.killAll(w, r)
return
case EndpointKillAtt:
h.killAtt(w, r)
return
case EndpointKillDef:
h.killDef(w, r)
return
case EndpointKillSup:
h.killSup(w, r)
return
case EndpointKillAllTribe:
h.killAllTribe(w, r)
return
case EndpointKillAttTribe:
h.killAttTribe(w, r)
return
case EndpointKillDefTribe:
h.killDefTribe(w, r)
return
case EndpointPlayer:
h.player(w, r)
return
case EndpointTribe:
h.tribe(w, r)
return
case EndpointVillage:
h.village(w, r)
return
case EndpointConquer:
h.conquer(w, r)
return
case endpointInterface:
h.getConquer(w, r)
return
default:
w.WriteHeader(http.StatusNotFound)
}
}))
}
func createWriteStringHandler(resp string) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte(resp))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
})
}
func createWriteCompressedStringHandler(resp string) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gzipWriter := gzip.NewWriter(w)
defer gzipWriter.Close()
_, err := gzipWriter.Write([]byte(resp))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
if err := gzipWriter.Flush(); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
})
}

View File

@ -49,7 +49,7 @@ type parsedODLine struct {
Score int
}
func (d *ServerDataLoader) parseODLine(line []string) (*parsedODLine, error) {
func (dl *ServerDataLoader) parseODLine(line []string) (*parsedODLine, error) {
if len(line) != 3 {
return nil, errors.New("invalid line format (should be rank,id,score)")
}
@ -70,19 +70,19 @@ func (d *ServerDataLoader) parseODLine(line []string) (*parsedODLine, error) {
return p, nil
}
func (d *ServerDataLoader) LoadOD(tribe bool) (map[int]*twmodel.OpponentsDefeated, error) {
func (dl *ServerDataLoader) LoadOD(tribe bool) (map[int]*twmodel.OpponentsDefeated, error) {
m := make(map[int]*twmodel.OpponentsDefeated)
formattedURLs := []string{
fmt.Sprintf("%s%s", d.baseURL, EndpointKillAll),
fmt.Sprintf("%s%s", d.baseURL, EndpointKillAtt),
fmt.Sprintf("%s%s", d.baseURL, EndpointKillDef),
fmt.Sprintf("%s%s", d.baseURL, EndpointKillSup),
fmt.Sprintf("%s%s", dl.baseURL, EndpointKillAll),
fmt.Sprintf("%s%s", dl.baseURL, EndpointKillAtt),
fmt.Sprintf("%s%s", dl.baseURL, EndpointKillDef),
fmt.Sprintf("%s%s", dl.baseURL, EndpointKillSup),
}
if tribe {
formattedURLs = []string{
fmt.Sprintf("%s%s", d.baseURL, EndpointKillAllTribe),
fmt.Sprintf("%s%s", d.baseURL, EndpointKillAttTribe),
fmt.Sprintf("%s%s", d.baseURL, EndpointKillDefTribe),
fmt.Sprintf("%s%s", dl.baseURL, EndpointKillAllTribe),
fmt.Sprintf("%s%s", dl.baseURL, EndpointKillAttTribe),
fmt.Sprintf("%s%s", dl.baseURL, EndpointKillDefTribe),
"",
}
}
@ -90,12 +90,12 @@ func (d *ServerDataLoader) LoadOD(tribe bool) (map[int]*twmodel.OpponentsDefeate
if formattedURL == "" {
continue
}
lines, err := d.getCSVData(formattedURL, true)
lines, err := dl.getCSVData(formattedURL, true)
if err != nil {
return nil, errors.Wrapf(err, "couldn't load data, formattedURL %s", formattedURL)
}
for _, line := range lines {
parsed, err := d.parseODLine(line)
parsed, err := dl.parseODLine(line)
if err != nil {
return nil, errors.Wrapf(err, "couldn't parse the line, url %s, line %s", formattedURL, strings.Join(line, ","))
}
@ -121,7 +121,7 @@ func (d *ServerDataLoader) LoadOD(tribe bool) (map[int]*twmodel.OpponentsDefeate
return m, nil
}
func (d *ServerDataLoader) parsePlayerLine(line []string) (*twmodel.Player, error) {
func (dl *ServerDataLoader) parsePlayerLine(line []string) (*twmodel.Player, error) {
if len(line) != 6 {
return nil, errors.New("invalid line format (should be id,name,tribeid,villages,points,rank)")
}
@ -159,16 +159,16 @@ func (d *ServerDataLoader) parsePlayerLine(line []string) (*twmodel.Player, erro
return player, nil
}
func (d *ServerDataLoader) LoadPlayers() ([]*twmodel.Player, error) {
formattedURL := d.baseURL + EndpointPlayer
lines, err := d.getCSVData(formattedURL, true)
func (dl *ServerDataLoader) LoadPlayers() ([]*twmodel.Player, error) {
formattedURL := dl.baseURL + EndpointPlayer
lines, err := dl.getCSVData(formattedURL, true)
if err != nil {
return nil, errors.Wrapf(err, "couldn't load data, url %s", formattedURL)
}
var players []*twmodel.Player
for _, line := range lines {
player, err := d.parsePlayerLine(line)
player, err := dl.parsePlayerLine(line)
if err != nil {
return nil, errors.Wrapf(err, "couldn't parse the line, url %s, line %s", formattedURL, strings.Join(line, ","))
}
@ -178,7 +178,7 @@ func (d *ServerDataLoader) LoadPlayers() ([]*twmodel.Player, error) {
return players, nil
}
func (d *ServerDataLoader) parseTribeLine(line []string) (*twmodel.Tribe, error) {
func (dl *ServerDataLoader) parseTribeLine(line []string) (*twmodel.Tribe, error) {
if len(line) != 8 {
return nil, errors.New("invalid line format (should be id,name,tag,members,villages,points,allpoints,rank)")
}
@ -224,15 +224,15 @@ func (d *ServerDataLoader) parseTribeLine(line []string) (*twmodel.Tribe, error)
return tribe, nil
}
func (d *ServerDataLoader) LoadTribes() ([]*twmodel.Tribe, error) {
formattedURL := d.baseURL + EndpointTribe
lines, err := d.getCSVData(formattedURL, true)
func (dl *ServerDataLoader) LoadTribes() ([]*twmodel.Tribe, error) {
formattedURL := dl.baseURL + EndpointTribe
lines, err := dl.getCSVData(formattedURL, true)
if err != nil {
return nil, errors.Wrapf(err, "couldn't load data, url %s", formattedURL)
}
var tribes []*twmodel.Tribe
for _, line := range lines {
tribe, err := d.parseTribeLine(line)
tribe, err := dl.parseTribeLine(line)
if err != nil {
return nil, errors.Wrapf(err, "couldn't parse the line, url %s, line %s", formattedURL, strings.Join(line, ","))
}
@ -241,7 +241,7 @@ func (d *ServerDataLoader) LoadTribes() ([]*twmodel.Tribe, error) {
return tribes, nil
}
func (d *ServerDataLoader) parseVillageLine(line []string) (*twmodel.Village, error) {
func (dl *ServerDataLoader) parseVillageLine(line []string) (*twmodel.Village, error) {
if len(line) != 7 {
return nil, errors.New("invalid line format (should be id,name,x,y,playerID,points,bonus)")
}
@ -278,15 +278,15 @@ func (d *ServerDataLoader) parseVillageLine(line []string) (*twmodel.Village, er
return village, nil
}
func (d *ServerDataLoader) LoadVillages() ([]*twmodel.Village, error) {
formattedURL := d.baseURL + EndpointVillage
lines, err := d.getCSVData(formattedURL, true)
func (dl *ServerDataLoader) LoadVillages() ([]*twmodel.Village, error) {
formattedURL := dl.baseURL + EndpointVillage
lines, err := dl.getCSVData(formattedURL, true)
if err != nil {
return nil, errors.Wrapf(err, "couldn't load data, formattedURL %s", formattedURL)
}
var villages []*twmodel.Village
for _, line := range lines {
village, err := d.parseVillageLine(line)
village, err := dl.parseVillageLine(line)
if err != nil {
return nil, errors.Wrapf(err, "couldn't parse the line, formattedURL %s, line %s", formattedURL, strings.Join(line, ","))
}
@ -295,7 +295,7 @@ func (d *ServerDataLoader) LoadVillages() ([]*twmodel.Village, error) {
return villages, nil
}
func (d *ServerDataLoader) parseEnnoblementLine(line []string) (*twmodel.Ennoblement, error) {
func (dl *ServerDataLoader) parseEnnoblementLine(line []string) (*twmodel.Ennoblement, error) {
if len(line) != 4 {
return nil, errors.New("invalid line format (should be village_id,timestamp,new_owner_id,old_owner_id)")
}
@ -326,25 +326,25 @@ type LoadEnnoblementsConfig struct {
EnnobledAtGT time.Time
}
func (d *ServerDataLoader) LoadEnnoblements(cfg *LoadEnnoblementsConfig) ([]*twmodel.Ennoblement, error) {
func (dl *ServerDataLoader) LoadEnnoblements(cfg *LoadEnnoblementsConfig) ([]*twmodel.Ennoblement, error) {
if cfg == nil {
cfg = &LoadEnnoblementsConfig{}
}
yesterdaysDate := time.Now().Add(-23 * time.Hour)
formattedURL := d.baseURL + EndpointConquer
formattedURL := dl.baseURL + EndpointConquer
compressed := true
if cfg.EnnobledAtGT.After(yesterdaysDate) || cfg.EnnobledAtGT.Equal(yesterdaysDate) {
formattedURL = d.baseURL + fmt.Sprintf(EndpointGetConquer, cfg.EnnobledAtGT.Unix())
formattedURL = dl.baseURL + fmt.Sprintf(EndpointGetConquer, cfg.EnnobledAtGT.Unix())
compressed = false
}
lines, err := d.getCSVData(formattedURL, compressed)
lines, err := dl.getCSVData(formattedURL, compressed)
if err != nil {
return nil, errors.Wrapf(err, "couldn't load data, formattedURL %s", formattedURL)
}
var ennoblements []*twmodel.Ennoblement
for _, line := range lines {
ennoblement, err := d.parseEnnoblementLine(line)
ennoblement, err := dl.parseEnnoblementLine(line)
if err != nil {
return nil, errors.Wrapf(err, "couldn't parse the line, formattedURL %s, line %s", formattedURL, strings.Join(line, ","))
}
@ -355,38 +355,38 @@ func (d *ServerDataLoader) LoadEnnoblements(cfg *LoadEnnoblementsConfig) ([]*twm
return ennoblements, nil
}
func (d *ServerDataLoader) GetConfig() (*twmodel.ServerConfig, error) {
formattedURL := d.baseURL + EndpointConfig
func (dl *ServerDataLoader) GetConfig() (*twmodel.ServerConfig, error) {
formattedURL := dl.baseURL + EndpointConfig
cfg := &twmodel.ServerConfig{}
err := d.getXML(formattedURL, cfg)
err := dl.getXML(formattedURL, cfg)
if err != nil {
return nil, errors.Wrap(err, "getConfig")
}
return cfg, nil
}
func (d *ServerDataLoader) GetBuildingConfig() (*twmodel.BuildingConfig, error) {
formattedURL := d.baseURL + EndpointBuildingConfig
func (dl *ServerDataLoader) GetBuildingConfig() (*twmodel.BuildingConfig, error) {
formattedURL := dl.baseURL + EndpointBuildingConfig
cfg := &twmodel.BuildingConfig{}
err := d.getXML(formattedURL, cfg)
err := dl.getXML(formattedURL, cfg)
if err != nil {
return nil, errors.Wrap(err, "getBuildingConfig")
}
return cfg, nil
}
func (d *ServerDataLoader) GetUnitConfig() (*twmodel.UnitConfig, error) {
formattedURL := d.baseURL + EndpointUnitConfig
func (dl *ServerDataLoader) GetUnitConfig() (*twmodel.UnitConfig, error) {
formattedURL := dl.baseURL + EndpointUnitConfig
cfg := &twmodel.UnitConfig{}
err := d.getXML(formattedURL, cfg)
err := dl.getXML(formattedURL, cfg)
if err != nil {
return nil, errors.Wrap(err, "getUnitConfig")
}
return cfg, nil
}
func (d *ServerDataLoader) getCSVData(url string, compressed bool) ([][]string, error) {
resp, err := d.client.Get(url)
func (dl *ServerDataLoader) getCSVData(url string, compressed bool) ([][]string, error) {
resp, err := dl.client.Get(url)
if err != nil {
return nil, err
}
@ -397,8 +397,8 @@ func (d *ServerDataLoader) getCSVData(url string, compressed bool) ([][]string,
return uncompressAndReadCsvLines(resp.Body)
}
func (d *ServerDataLoader) getXML(url string, decode interface{}) error {
resp, err := d.client.Get(url)
func (dl *ServerDataLoader) getXML(url string, decode interface{}) error {
resp, err := dl.client.Get(url)
if err != nil {
return err
}

View File

@ -0,0 +1,626 @@
package twdataloader
import (
"fmt"
"github.com/stretchr/testify/assert"
"testing"
"time"
"github.com/tribalwarshelp/shared/tw/twmodel"
)
func TestLoadOD(t *testing.T) {
type scenario struct {
respKillAll string
respKillAtt string
respKillDef string
respKillSup string
respKillAllTribe string
respKillAttTribe string
respKillDefTribe string
tribe bool
expectedResult map[int]*twmodel.OpponentsDefeated
expectedErrMsg string
}
scenarios := []scenario{
{
respKillAll: "1,1",
expectedErrMsg: "invalid line format (should be rank,id,score)",
},
{
respKillAllTribe: "1,1",
expectedErrMsg: "invalid line format (should be rank,id,score)",
tribe: true,
},
{
respKillAll: "1,1,1",
respKillAtt: "1,1,1",
respKillDef: "1,1,1",
respKillSup: "1,1",
expectedErrMsg: "invalid line format (should be rank,id,score)",
},
{
respKillAllTribe: "1,1,1",
respKillAttTribe: "1,1,1",
respKillDefTribe: "1,1",
expectedErrMsg: "invalid line format (should be rank,id,score)",
tribe: true,
},
{
respKillAll: "1,1,asd",
expectedErrMsg: "parsedODLine.Score: strconv.Atoi: parsing \"asd\"",
},
{
respKillAll: "1,asd,1",
expectedErrMsg: "parsedODLine.ID: strconv.Atoi: parsing \"asd\":",
},
{
respKillAll: "asd,1,1",
expectedErrMsg: "parsedODLine.Rank: strconv.Atoi: parsing \"asd\":",
},
{
respKillAllTribe: "1,1,asd",
expectedErrMsg: "parsedODLine.Score: strconv.Atoi: parsing \"asd\"",
tribe: true,
},
{
respKillAllTribe: "1,asd,1",
expectedErrMsg: "parsedODLine.ID: strconv.Atoi: parsing \"asd\":",
tribe: true,
},
{
respKillAllTribe: "asd,1,1",
expectedErrMsg: "parsedODLine.Rank: strconv.Atoi: parsing \"asd\":",
tribe: true,
},
{
respKillAll: "1,1,1\n4,2,4\n2,3,2",
respKillAtt: "2,1,2\n3,2,3\n1,3,1",
respKillDef: "3,1,3\n2,2,2\n4,3,4",
respKillSup: "4,1,4\n1,2,1\n3,3,3",
expectedResult: map[int]*twmodel.OpponentsDefeated{
1: {
RankAtt: 2,
ScoreAtt: 2,
RankDef: 3,
ScoreDef: 3,
RankSup: 4,
ScoreSup: 4,
RankTotal: 1,
ScoreTotal: 1,
},
2: {
RankAtt: 3,
ScoreAtt: 3,
RankDef: 2,
ScoreDef: 2,
RankSup: 1,
ScoreSup: 1,
ScoreTotal: 4,
RankTotal: 4,
},
3: {
RankAtt: 1,
ScoreAtt: 1,
RankDef: 4,
ScoreDef: 4,
RankSup: 3,
ScoreSup: 3,
ScoreTotal: 2,
RankTotal: 2,
},
},
},
{
respKillAllTribe: "1,1,1\n2,2,2\n3,3,3",
respKillAttTribe: "1,1,1\n2,2,2\n3,3,3",
respKillDefTribe: "1,1,1\n2,2,2\n3,3,3",
expectedResult: map[int]*twmodel.OpponentsDefeated{
1: {
RankAtt: 1,
ScoreAtt: 1,
RankDef: 1,
ScoreDef: 1,
ScoreTotal: 1,
RankTotal: 1,
},
2: {
RankAtt: 2,
ScoreAtt: 2,
RankDef: 2,
ScoreDef: 2,
ScoreTotal: 2,
RankTotal: 2,
},
3: {
RankAtt: 3,
ScoreAtt: 3,
RankDef: 3,
ScoreDef: 3,
ScoreTotal: 3,
RankTotal: 3,
},
},
tribe: true,
},
}
for _, scenario := range scenarios {
ts := prepareTestServer(&handlers{
killAll: createWriteCompressedStringHandler(scenario.respKillAll),
killAtt: createWriteCompressedStringHandler(scenario.respKillAtt),
killDef: createWriteCompressedStringHandler(scenario.respKillDef),
killSup: createWriteCompressedStringHandler(scenario.respKillSup),
killAllTribe: createWriteCompressedStringHandler(scenario.respKillAllTribe),
killAttTribe: createWriteCompressedStringHandler(scenario.respKillAttTribe),
killDefTribe: createWriteCompressedStringHandler(scenario.respKillDefTribe),
})
dl := NewServerDataLoader(&ServerDataLoaderConfig{
BaseURL: ts.URL,
Client: ts.Client(),
})
res, err := dl.LoadOD(scenario.tribe)
if scenario.expectedErrMsg != "" {
assert.NotNil(t, err)
assert.Contains(t, err.Error(), scenario.expectedErrMsg)
} else {
assert.Nil(t, err)
}
if scenario.expectedResult != nil {
assert.Len(t, res, len(scenario.expectedResult))
for id, singleResult := range res {
expected, ok := scenario.expectedResult[id]
assert.True(t, ok)
assert.NotNil(t, expected)
assert.EqualValues(t, expected, singleResult)
}
}
ts.Close()
}
}
func TestLoadPlayers(t *testing.T) {
type scenario struct {
resp string
expectedResult []*twmodel.Player
expectedErrMsg string
}
exists := true
scenarios := []scenario{
{
resp: "1,1,1,1",
expectedErrMsg: "invalid line format (should be id,name,tribeid,villages,points,rank)",
},
{
resp: "1,name,1,500,500",
expectedErrMsg: "invalid line format (should be id,name,tribeid,villages,points,rank)",
},
{
resp: "asd,name,1,500,500,500",
expectedErrMsg: "player.ID: strconv.Atoi: parsing \"asd\"",
},
{
resp: "1,name,asd,500,500,500",
expectedErrMsg: "player.TribeID: strconv.Atoi: parsing \"asd\"",
},
{
resp: "1,name,123,asd,500,500",
expectedErrMsg: "player.TotalVillages: strconv.Atoi: parsing \"asd\"",
},
{
resp: "1,name,123,500,asd,500",
expectedErrMsg: "player.Points: strconv.Atoi: parsing \"asd\"",
},
{
resp: "1,name,123,500,123,asd",
expectedErrMsg: "player.Rank: strconv.Atoi: parsing \"asd\"",
},
{
resp: "1,name,123,500,500,asd",
expectedErrMsg: "player.Rank: strconv.Atoi: parsing \"asd\"",
},
{
resp: "1,name,123,124,125,126\n2,name2,256,257,258,259",
expectedResult: []*twmodel.Player{
{
ID: 1,
Name: "name",
TribeID: 123,
TotalVillages: 124,
Points: 125,
Rank: 126,
Exists: &exists,
},
{
ID: 2,
Name: "name2",
TribeID: 256,
TotalVillages: 257,
Points: 258,
Rank: 259,
Exists: &exists,
},
},
},
}
for _, scenario := range scenarios {
ts := prepareTestServer(&handlers{
player: createWriteCompressedStringHandler(scenario.resp),
})
dl := NewServerDataLoader(&ServerDataLoaderConfig{
BaseURL: ts.URL,
Client: ts.Client(),
})
res, err := dl.LoadPlayers()
if scenario.expectedErrMsg != "" {
assert.NotNil(t, err)
assert.Contains(t, err.Error(), scenario.expectedErrMsg)
} else {
assert.Nil(t, err)
}
if scenario.expectedResult != nil {
assert.Len(t, res, len(scenario.expectedResult))
for _, singleResult := range res {
found := false
var player *twmodel.Player
for _, expected := range scenario.expectedResult {
if expected.ID == singleResult.ID {
found = true
player = expected
break
}
}
assert.True(t, found)
assert.NotNil(t, player)
assert.EqualValues(t, player, singleResult)
}
}
ts.Close()
}
}
func TestLoadTribes(t *testing.T) {
type scenario struct {
resp string
expectedResult []*twmodel.Tribe
expectedErrMsg string
}
ex := true
scenarios := []scenario{
{
resp: "1,1,1,1",
expectedErrMsg: "invalid line format (should be id,name,tag,members,villages,points,allpoints,rank)",
},
{
resp: "1,name,1,500,500",
expectedErrMsg: "invalid line format (should be id,name,tag,members,villages,points,allpoints,rank)",
},
{
resp: "asd,name,tag,500,500,500,500,500",
expectedErrMsg: "tribe.ID: strconv.Atoi: parsing \"asd\"",
},
{
resp: "123,name,tag,asd,500,500,500,500",
expectedErrMsg: "tribe.TotalMembers: strconv.Atoi: parsing \"asd\"",
},
{
resp: "123,name,tag,500,asd,500,500,500",
expectedErrMsg: "tribe.TotalVillages: strconv.Atoi: parsing \"asd\"",
},
{
resp: "123,name,tag,500,500,asd,500,500",
expectedErrMsg: "tribe.Points: strconv.Atoi: parsing \"asd\"",
},
{
resp: "123,name,tag,500,500,500,asd,500",
expectedErrMsg: "tribe.AllPoints: strconv.Atoi: parsing \"asd\"",
},
{
resp: "123,name,tag,500,500,500,500,asd",
expectedErrMsg: "tribe.Rank: strconv.Atoi: parsing \"asd\"",
},
{
resp: "123,name,tag,500,501,502,503,504\n1234,name2,tag2,5000,5001,5002,5003,5004",
expectedResult: []*twmodel.Tribe{
{
ID: 123,
Name: "name",
Tag: "tag",
TotalMembers: 500,
TotalVillages: 501,
Points: 502,
AllPoints: 503,
Rank: 504,
Exists: &ex,
},
{
ID: 1234,
Name: "name2",
Tag: "tag2",
TotalMembers: 5000,
TotalVillages: 5001,
Points: 5002,
AllPoints: 5003,
Rank: 5004,
Exists: &ex,
},
},
},
}
for _, scenario := range scenarios {
ts := prepareTestServer(&handlers{
tribe: createWriteCompressedStringHandler(scenario.resp),
})
dl := NewServerDataLoader(&ServerDataLoaderConfig{
BaseURL: ts.URL,
Client: ts.Client(),
})
res, err := dl.LoadTribes()
if scenario.expectedErrMsg != "" {
assert.NotNil(t, err)
assert.Contains(t, err.Error(), scenario.expectedErrMsg)
} else {
assert.Nil(t, err)
}
if scenario.expectedResult != nil {
assert.Len(t, res, len(scenario.expectedResult))
for _, singleResult := range res {
found := false
var tribe *twmodel.Tribe
for _, expected := range scenario.expectedResult {
if expected.ID == singleResult.ID {
found = true
tribe = expected
break
}
}
assert.True(t, found)
assert.NotNil(t, tribe)
assert.EqualValues(t, tribe, singleResult)
}
}
ts.Close()
}
}
func TestLoadVillages(t *testing.T) {
type scenario struct {
resp string
expectedResult []*twmodel.Village
expectedErrMsg string
}
scenarios := []scenario{
{
resp: "1,1,1,1",
expectedErrMsg: "invalid line format (should be id,name,x,y,playerID,points,bonus)",
},
{
resp: "1,name,1,500,500",
expectedErrMsg: "invalid line format (should be id,name,x,y,playerID,points,bonus)",
},
{
resp: "asd,name,500,500,500,500,0",
expectedErrMsg: "village.ID: strconv.Atoi: parsing \"asd\"",
},
{
resp: "123,name,asd,500,500,500,0",
expectedErrMsg: "village.X: strconv.Atoi: parsing \"asd\"",
},
{
resp: "123,name,500,asd,500,500,0",
expectedErrMsg: "village.Y: strconv.Atoi: parsing \"asd\"",
},
{
resp: "123,name,500,500,asd,500,0",
expectedErrMsg: "village.PlayerID: strconv.Atoi: parsing \"asd\"",
},
{
resp: "123,name,500,500,500,asd,0",
expectedErrMsg: "village.Points: strconv.Atoi: parsing \"asd\"",
},
{
resp: "123,name,500,500,500,500,asd",
expectedErrMsg: "village.Bonus: strconv.Atoi: parsing \"asd\"",
},
{
resp: "123,village 1,500,501,502,503,504\n124,village 2,100,101,102,103,104",
expectedResult: []*twmodel.Village{
{
ID: 123,
Name: "village 1",
X: 500,
Y: 501,
PlayerID: 502,
Points: 503,
Bonus: 504,
},
{
ID: 124,
Name: "village 2",
X: 100,
Y: 101,
PlayerID: 102,
Points: 103,
Bonus: 104,
},
},
},
}
for _, scenario := range scenarios {
ts := prepareTestServer(&handlers{
village: createWriteCompressedStringHandler(scenario.resp),
})
dl := NewServerDataLoader(&ServerDataLoaderConfig{
BaseURL: ts.URL,
Client: ts.Client(),
})
res, err := dl.LoadVillages()
if scenario.expectedErrMsg != "" {
assert.NotNil(t, err)
assert.Contains(t, err.Error(), scenario.expectedErrMsg)
} else {
assert.Nil(t, err)
}
if scenario.expectedResult != nil {
assert.Len(t, res, len(scenario.expectedResult))
for _, singleResult := range res {
found := false
var village *twmodel.Village
for _, expected := range scenario.expectedResult {
if expected.ID == singleResult.ID {
found = true
village = expected
break
}
}
assert.True(t, found)
assert.NotNil(t, village)
assert.EqualValues(t, village, singleResult)
}
}
ts.Close()
}
}
func TestLoadEnnoblements(t *testing.T) {
type scenario struct {
respConquer string
respGetConquer string
cfg *LoadEnnoblementsConfig
expectedResult []*twmodel.Ennoblement
expectedErrMsg string
}
nowMinus5h := time.Now().Add(-5 * time.Hour).Unix()
nowMinus6h := time.Now().Add(-6 * time.Hour).Unix()
scenarios := []scenario{
{
respConquer: "1,1,1",
expectedErrMsg: "invalid line format (should be village_id,timestamp,new_owner_id,old_owner_id)",
},
{
respConquer: "1,name,1,500,500",
expectedErrMsg: "invalid line format (should be village_id,timestamp,new_owner_id,old_owner_id)",
},
{
respConquer: "asd,123,123,123",
expectedErrMsg: "ennoblement.VillageID: strconv.Atoi: parsing \"asd\"",
},
{
respConquer: "123,asd,123,123",
expectedErrMsg: "timestamp: strconv.Atoi: parsing \"asd\"",
},
{
respConquer: "123,123,asd,123",
expectedErrMsg: "ennoblement.NewOwnerID: strconv.Atoi: parsing \"asd\"",
},
{
respConquer: "123,123,123,asd",
expectedErrMsg: "ennoblement.OldOwnerID: strconv.Atoi: parsing \"asd\"",
},
{
respConquer: "123,124,125,126\n127,128,129,130",
expectedResult: []*twmodel.Ennoblement{
{
VillageID: 123,
EnnobledAt: time.Unix(124, 0),
NewOwnerID: 125,
OldOwnerID: 126,
},
{
VillageID: 127,
EnnobledAt: time.Unix(128, 0),
NewOwnerID: 129,
OldOwnerID: 130,
},
},
},
{
respGetConquer: "123,124,125,126\n127,128,129,130",
cfg: &LoadEnnoblementsConfig{
EnnobledAtGT: time.Now().Add(5 * time.Hour),
},
expectedResult: []*twmodel.Ennoblement{},
},
{
respConquer: fmt.Sprintf("123,%d,125,126\n127,%d,129,130", nowMinus5h, nowMinus6h),
expectedResult: []*twmodel.Ennoblement{
{
VillageID: 123,
EnnobledAt: time.Unix(nowMinus5h, 0),
NewOwnerID: 125,
OldOwnerID: 126,
},
{
VillageID: 127,
EnnobledAt: time.Unix(nowMinus6h, 0),
NewOwnerID: 129,
OldOwnerID: 130,
},
},
},
}
for _, scenario := range scenarios {
ts := prepareTestServer(&handlers{
conquer: createWriteCompressedStringHandler(scenario.respConquer),
getConquer: createWriteStringHandler(scenario.respGetConquer),
})
dl := NewServerDataLoader(&ServerDataLoaderConfig{
BaseURL: ts.URL,
Client: ts.Client(),
})
res, err := dl.LoadEnnoblements(scenario.cfg)
if scenario.expectedErrMsg != "" {
assert.NotNil(t, err)
assert.Contains(t, err.Error(), scenario.expectedErrMsg)
} else {
assert.Nil(t, err)
}
if scenario.expectedResult != nil {
assert.Len(t, res, len(scenario.expectedResult))
for _, singleResult := range res {
found := false
var ennoblement *twmodel.Ennoblement
for _, expected := range scenario.expectedResult {
if expected.VillageID == singleResult.VillageID {
found = true
ennoblement = expected
break
}
}
assert.True(t, found)
assert.NotNil(t, ennoblement)
assert.EqualValues(t, ennoblement, singleResult)
}
}
ts.Close()
}
}

View File

@ -1,7 +1,6 @@
package twdataloader
import (
"fmt"
phpserialize "github.com/Kichiyaki/go-php-serialize"
"github.com/pkg/errors"
"io/ioutil"
@ -40,8 +39,8 @@ func NewVersionDataLoader(cfg *VersionDataLoaderConfig) *VersionDataLoader {
}
}
func (d *VersionDataLoader) LoadServers() ([]*Server, error) {
resp, err := d.client.Get(fmt.Sprintf("https://%s%s", d.host, EndpointGetServers))
func (dl *VersionDataLoader) LoadServers() ([]*Server, error) {
resp, err := dl.client.Get(dl.host + EndpointGetServers)
if err != nil {
return nil, errors.Wrap(err, "couldn't load servers")
}
@ -52,14 +51,28 @@ func (d *VersionDataLoader) LoadServers() ([]*Server, error) {
return nil, errors.Wrap(err, "couldn't read the response body")
}
body, err := phpserialize.Decode(string(bodyBytes))
if err != nil {
return nil, errors.Wrap(err, "couldn't decode the response body into the go value")
if err != nil || body == nil {
fmtedErr := errors.New("couldn't decode the response body into a go value")
if err != nil {
fmtedErr = errors.Wrap(err, fmtedErr.Error())
}
return nil, fmtedErr
}
bodyMap, ok := body.(map[interface{}]interface{})
if !ok {
return nil, errors.Errorf("expected map, got %T", body)
}
var servers []*Server
for serverKey, url := range body.(map[interface{}]interface{}) {
serverKeyStr := serverKey.(string)
urlStr := url.(string)
for serverKey, url := range bodyMap {
serverKeyStr, ok := serverKey.(string)
if !ok {
return nil, errors.Errorf("expected string as the key of the map, got %T", serverKey)
}
urlStr, ok := url.(string)
if !ok {
return nil, errors.Errorf("expected string as the value of the map, got %T", url)
}
if serverKeyStr != "" && urlStr != "" {
servers = append(servers, &Server{
Key: serverKeyStr,

View File

@ -0,0 +1,83 @@
package twdataloader
import (
"github.com/stretchr/testify/assert"
"strconv"
"strings"
"testing"
)
func TestLoadServers(t *testing.T) {
t.Run("invalid response", func(t *testing.T) {
type scenario struct {
resp string
expectedErrMsg string
}
scenarios := []scenario{
{
resp: `:"https://pl165.plemiona.pl";s:5:"pl166";s:25:"https://pl166.plemiona.pl";s:5:"pl167";s:25:"https://pl167.plemiona.pl";}`,
expectedErrMsg: "couldn't decode the response body into a go value",
},
{
resp: `a:19:{s:5:"pl150"s:25"https://pl150.plemiona.pl";}`,
expectedErrMsg: "expected string as the value of the map, got <nil>",
},
{
resp: "a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}",
expectedErrMsg: "expected string as the key of the map, got int64",
},
{
resp: `O:8:"stdClass":0:{}`,
expectedErrMsg: "expected map, got *phpserialize.PhpObject",
},
{
resp: `a:2:{s:3:"asd";i:123;s:4:"asd2";i:123;}`,
expectedErrMsg: "expected string as the value of the map, got int64",
},
}
for _, scenario := range scenarios {
ts := prepareTestServer(&handlers{
getServers: createWriteStringHandler(scenario.resp),
})
dl := NewVersionDataLoader(&VersionDataLoaderConfig{
Host: ts.URL,
Client: ts.Client(),
})
_, err := dl.LoadServers()
assert.NotNil(t, err)
assert.Contains(t, err.Error(), scenario.expectedErrMsg)
ts.Close()
}
})
t.Run("success", func(t *testing.T) {
resp := `a:19:{s:5:"pl150";s:25:"https://pl150.plemiona.pl";s:5:"pl151";s:25:"https://pl151.plemiona.pl";s:5:"pl152";s:25:"https://pl152.plemiona.pl";s:5:"pl153";s:25:"https://pl153.plemiona.pl";s:5:"pl154";s:25:"https://pl154.plemiona.pl";s:5:"pl155";s:25:"https://pl155.plemiona.pl";s:5:"pl156";s:25:"https://pl156.plemiona.pl";s:5:"pl157";s:25:"https://pl157.plemiona.pl";s:5:"pl158";s:25:"https://pl158.plemiona.pl";s:5:"pl159";s:25:"https://pl159.plemiona.pl";s:5:"pl160";s:25:"https://pl160.plemiona.pl";s:5:"pl161";s:25:"https://pl161.plemiona.pl";s:5:"pl162";s:25:"https://pl162.plemiona.pl";s:5:"pl163";s:25:"https://pl163.plemiona.pl";s:5:"pl164";s:25:"https://pl164.plemiona.pl";s:4:"plp7";s:24:"https://plp7.plemiona.pl";s:5:"pl165";s:25:"https://pl165.plemiona.pl";s:5:"pl166";s:25:"https://pl166.plemiona.pl";s:5:"pl167";s:25:"https://pl167.plemiona.pl";}`
ts := prepareTestServer(&handlers{
getServers: createWriteStringHandler(resp),
})
defer ts.Close()
expectedLength, err := strconv.Atoi(strings.Split(resp, ":")[1])
assert.Nil(t, err)
dl := NewVersionDataLoader(&VersionDataLoaderConfig{
Host: ts.URL,
Client: ts.Client(),
})
servers, err := dl.LoadServers()
assert.Nil(t, err)
cnt := 0
for _, server := range servers {
if strings.Contains(resp, server.URL) && strings.Contains(resp, server.Key) {
cnt++
}
}
assert.Equal(t, expectedLength, cnt)
})
}