From fd18a63360f13677c9c0154cb18353c24a7eba36 Mon Sep 17 00:00:00 2001 From: Erik Brakkee Date: Thu, 15 Aug 2024 21:04:31 +0200 Subject: [PATCH] now using maps of Guid to Agent/Client in the state, working towards the definitive solution. Using LinkedMap that preserves insertion order for the implementation and also added unit tests for that. --- Makefile | 5 +- cmd/converge/notifier.go | 4 +- cmd/converge/prometheus.go | 43 +++--- cmd/templaterender/render.go | 8 +- go.mod | 5 + go.sum | 13 +- pkg/models/state.go | 17 ++- pkg/server/admin/admin.go | 10 +- pkg/server/matchmaker/matchmaker.go | 4 +- pkg/server/matchmaker/websessions.go | 6 +- pkg/server/templates/sessions.templ | 8 +- pkg/support/collections/linkedmap.go | 159 ++++++++++++++++++++++ pkg/support/collections/linkedmap_test.go | 130 ++++++++++++++++++ 13 files changed, 358 insertions(+), 54 deletions(-) create mode 100644 pkg/support/collections/linkedmap.go create mode 100644 pkg/support/collections/linkedmap_test.go diff --git a/Makefile b/Makefile index 52ebe86..4935e4f 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,9 @@ generate: vet: fmt go vet ./... +test: + go test -v ./... + build: generate vet mkdir -p bin go build -o bin ./cmd/... @@ -30,4 +33,4 @@ image: docker compose build push: image - docker compose push \ No newline at end of file + docker compose push diff --git a/cmd/converge/notifier.go b/cmd/converge/notifier.go index 87eb9bb..241b804 100644 --- a/cmd/converge/notifier.go +++ b/cmd/converge/notifier.go @@ -15,6 +15,6 @@ func NewStateNotifier() *StateNotifier { } func (notifier StateNotifier) Publish(state *models.State) { - notifier.webNotificationChannel <- state.Copy() - notifier.prometheusNotificationChannel <- state.Copy() + notifier.webNotificationChannel <- state + notifier.prometheusNotificationChannel <- state } diff --git a/cmd/converge/prometheus.go b/cmd/converge/prometheus.go index f669e31..335ab31 100644 --- a/cmd/converge/prometheus.go +++ b/cmd/converge/prometheus.go @@ -2,6 +2,7 @@ package main import ( "git.wamblee.org/converge/pkg/models" + "git.wamblee.org/converge/pkg/support/collections" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -14,20 +15,20 @@ const NAMESPACE = "converge" // more efficient state representation for state type PrometheusState struct { - agents map[models.AgentGuid]*models.Agent - clients map[models.ClientGuid]*models.Client + agents *collections.LinkedMap[models.AgentGuid, *models.Agent] + clients *collections.LinkedMap[models.ClientGuid, *models.Client] } func NewPrometheusState(state *models.State) *PrometheusState { res := PrometheusState{ - agents: make(map[models.AgentGuid]*models.Agent), - clients: make(map[models.ClientGuid]*models.Client), + agents: collections.NewLinkedMap[models.AgentGuid, *models.Agent](), + clients: collections.NewLinkedMap[models.ClientGuid, *models.Client](), } - for i, _ := range state.Agents { - res.agents[state.Agents[i].Guid] = &state.Agents[i] + for agent := range state.Agents.RangeValues() { + res.agents.Put(agent.Guid, agent) } - for i, _ := range state.Clients { - res.clients[state.Clients[i].Guid] = &state.Clients[i] + for client := range state.Clients.RangeValues() { + res.clients.Put(client.Guid, client) } return &res } @@ -35,7 +36,7 @@ func NewPrometheusState(state *models.State) *PrometheusState { var ( // remember previous values of agent guids and clients so that we can increment // the cumulative counters. - lastState *PrometheusState = NewPrometheusState(&models.State{}) + lastState *PrometheusState = NewPrometheusState(models.NewState()) cumulativeAgentCount = promauto.NewCounter(prometheus.CounterOpts{ Namespace: NAMESPACE, @@ -149,7 +150,7 @@ func clientLabels(client *models.Client) prometheus.Labels { } func agentActive(agent *models.Agent) { - prevAgent, ok := lastState.agents[agent.Guid] + prevAgent, ok := lastState.agents.Get(agent.Guid) if ok && *prevAgent != *agent { removeAgentInfoMetrics(prevAgent) } @@ -164,7 +165,7 @@ func agentActive(agent *models.Agent) { } func clientActive(client *models.Client) { - prevClient, ok := lastState.clients[client.Guid] + prevClient, ok := lastState.clients.Get(client.Guid) if ok && *prevClient != *client { removeClientInfoMetrics(prevClient) } @@ -222,12 +223,12 @@ func updateMetrics(state *models.State) { } func updateDurations() { - for _, agent := range lastState.agents { + for agent := range lastState.agents.RangeValues() { agentDuration. With(prometheus.Labels{"agent_guid": string(agent.Guid)}). Set(float64(time.Now().Sub(agent.StartTime).Seconds())) } - for _, client := range lastState.clients { + for client := range lastState.clients.RangeValues() { clientDuration. With(prometheus.Labels{"client_guid": string(client.Guid)}). Set(float64(time.Now().Sub(client.StartTime).Seconds())) @@ -239,13 +240,13 @@ func updateMetricsImpl(state *PrometheusState) { agentGuids := make(map[models.AgentGuid]*models.Agent) clientGuids := make(map[models.ClientGuid]*models.Client) - agentCount.Set(float64(len(state.agents))) + agentCount.Set(float64(state.agents.Len())) disconnectedAgents := make(map[models.AgentGuid]*models.Agent) - for _, agent := range lastState.agents { + for agent := range lastState.agents.RangeValues() { disconnectedAgents[agent.Guid] = agent } - for _, agent := range state.agents { - if lastState.agents[agent.Guid] == nil { + for agent := range state.agents.RangeValues() { + if lastState.agents.Contains(agent.Guid) { cumulativeAgentCount.Inc() } delete(disconnectedAgents, agent.Guid) @@ -256,15 +257,15 @@ func updateMetricsImpl(state *PrometheusState) { removeAgentMetrics(agent) } - clientCount.Set(float64(len(state.clients))) + clientCount.Set(float64(state.clients.Len())) // with this app disconnectedClients := make(map[models.ClientGuid]*models.Client) - for _, client := range lastState.clients { + for client := range lastState.clients.RangeValues() { disconnectedClients[client.Guid] = client } - for _, client := range state.clients { - if lastState.clients[client.Guid] == nil { + for client := range state.clients.RangeValues() { + if lastState.clients.Contains(client.Guid) { cumulativeClientCount.Inc() } delete(disconnectedClients, client.Guid) diff --git a/cmd/templaterender/render.go b/cmd/templaterender/render.go index 51770a1..42e70f7 100644 --- a/cmd/templaterender/render.go +++ b/cmd/templaterender/render.go @@ -79,7 +79,7 @@ func main() { panic(err) } - state := models.State{} + state := models.NewState() agent := models.Agent{ Guid: models.AgentGuid(strconv.Itoa(rand.Int())), RemoteAddr: "10.220.1.3:3333", @@ -94,7 +94,7 @@ func main() { }, ExpiryTime: time.Now().In(japan).Add(10 * time.Minute), } - state.Agents = append(state.Agents, agent) + state.Agents.Put(agent.Guid, &agent) client := models.Client{ Guid: models.ClientGuid(strconv.Itoa(rand.Int())), RemoteAddr: models.RemoteAddr("10.1.3.3"), @@ -104,7 +104,7 @@ func main() { StartTime: time.Now().In(japan), SessionType: models.SessionType("sftp"), } - state.Clients = append(state.Clients, client) - return templates2.SessionsTab(&state, netherlands) + state.Clients.Put(client.Guid, &client) + return templates2.SessionsTab(state, netherlands) }) } diff --git a/go.mod b/go.mod index 2da9462..a402fd1 100755 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/hashicorp/yamux v0.1.1 github.com/pkg/sftp v1.13.6 github.com/prometheus/client_golang v1.19.1 + github.com/stretchr/testify v1.9.0 golang.org/x/crypto v0.25.0 golang.org/x/term v0.22.0 ) @@ -21,10 +22,14 @@ require ( github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/fs v0.1.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect golang.org/x/sys v0.22.0 // indirect google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index cd8d6e5..f51b11b 100755 --- a/go.sum +++ b/go.sum @@ -10,6 +10,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -27,6 +28,10 @@ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -39,12 +44,14 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -83,6 +90,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/models/state.go b/pkg/models/state.go index 1c1dc23..880f533 100644 --- a/pkg/models/state.go +++ b/pkg/models/state.go @@ -2,6 +2,7 @@ package models import ( "git.wamblee.org/converge/pkg/comms" + "git.wamblee.org/converge/pkg/support/collections" "time" ) @@ -39,15 +40,13 @@ type Client struct { // Created by the server and used for updating the web client // and prometheus metrics. type State struct { - Agents []Agent - Clients []Client + Agents *collections.LinkedMap[AgentGuid, *Agent] + Clients *collections.LinkedMap[ClientGuid, *Client] } -func (state *State) Copy() *State { - c := State{} - c.Agents = make([]Agent, len(state.Agents)) - c.Clients = make([]Client, len(state.Clients)) - copy(c.Agents, state.Agents) - copy(c.Clients, state.Clients) - return &c +func NewState() *State { + return &State{ + Agents: collections.NewLinkedMap[AgentGuid, *Agent](), + Clients: collections.NewLinkedMap[ClientGuid, *Client](), + } } diff --git a/pkg/server/admin/admin.go b/pkg/server/admin/admin.go index 3b4c286..3c3bd79 100644 --- a/pkg/server/admin/admin.go +++ b/pkg/server/admin/admin.go @@ -81,16 +81,14 @@ func NewAdmin() *Admin { func (admin *Admin) CreateNotifification() *models.State { admin.mutex.Lock() defer admin.mutex.Unlock() - state := models.State{} - state.Agents = make([]models.Agent, 0, len(admin.agents)) - state.Clients = make([]models.Client, 0, len(admin.clients)) + state := models.NewState() for _, agent := range admin.agents { - state.Agents = append(state.Agents, agent.Agent) + state.Agents.Put(agent.Guid, &agent.Agent) } for _, client := range admin.clients { - state.Clients = append(state.Clients, client.Client) + state.Clients.Put(client.Guid, &client.Client) } - return &state + return state } func (admin *Admin) getFreeId(publicId models.RendezVousId) (models.RendezVousId, error) { diff --git a/pkg/server/matchmaker/matchmaker.go b/pkg/server/matchmaker/matchmaker.go index 94ed7a0..4e570b4 100644 --- a/pkg/server/matchmaker/matchmaker.go +++ b/pkg/server/matchmaker/matchmaker.go @@ -134,7 +134,7 @@ func logStatusImpl(admin *models.State, notifier Notifier) { lines = append(lines, fmt.Sprintf(format, "AGENT", "ACTIVE_SINCE", "EXPIRY_TIME", "USER", "HOST", "OS")) - for _, agent := range admin.Agents { + for agent := range admin.Agents.RangeValues() { lines = append(lines, fmt.Sprintf(format, agent.PublicId, agent.StartTime.Format(time.DateTime), agent.ExpiryTime.Format(time.DateTime), @@ -145,7 +145,7 @@ func logStatusImpl(admin *models.State, notifier Notifier) { lines = append(lines, "") format = "%-10s %-20s %-20s %-20s %-20s" lines = append(lines, fmt.Sprintf(format, "CLIENT", "AGENT", "ACTIVE_SINCE", "REMOTE_ADDRESS", "SESSION_TYPE")) - for _, client := range admin.Clients { + for client := range admin.Clients.RangeValues() { lines = append(lines, fmt.Sprintf(format, client.ClientId, client.PublicId, diff --git a/pkg/server/matchmaker/websessions.go b/pkg/server/matchmaker/websessions.go index 83c4a92..ba9a3b6 100644 --- a/pkg/server/matchmaker/websessions.go +++ b/pkg/server/matchmaker/websessions.go @@ -123,7 +123,7 @@ func (session *WebSession) WriteNotifications(location *time.Location, ctx conte log.Println("channel closed") return } - if session.writeNotificationToClient(location, notification) { + if !session.writeNotificationToClient(location, notification) { return } case <-timer.C: @@ -140,9 +140,9 @@ func (session *WebSession) writeNotificationToClient(location *time.Location, no err := templates.State(notification, location).Render(context.Background(), session.conn) if err != nil { log.Printf("WS connection closed: %v", err) - return true + return false } - return false + return true } func (sessions *WebSessions) SessionClosed(session *WebSession) { diff --git a/pkg/server/templates/sessions.templ b/pkg/server/templates/sessions.templ index 4a17096..777d0b8 100644 --- a/pkg/server/templates/sessions.templ +++ b/pkg/server/templates/sessions.templ @@ -31,7 +31,7 @@ templ State(state *models.State, location *time.Location) {

agents

- if len(state.Agents) == 0 { + if state.Agents.Len() == 0 {

-

} else { @@ -46,7 +46,7 @@ templ State(state *models.State, location *time.Location) { - for _, agent := range state.Agents { + for agent := range state.Agents.RangeValues() { @@ -64,7 +64,7 @@ templ State(state *models.State, location *time.Location) {

clients

- if len(state.Clients) == 0 { + if state.Clients.Len() == 0 {

-

} else {
shell
{string(agent.PublicId)} {agent.StartTime.In(location).Format(time.DateTime)}
@@ -80,7 +80,7 @@ templ State(state *models.State, location *time.Location) { - for _, client := range state.Clients { + for client := range state.Clients.RangeValues() { diff --git a/pkg/support/collections/linkedmap.go b/pkg/support/collections/linkedmap.go new file mode 100644 index 0000000..8648f88 --- /dev/null +++ b/pkg/support/collections/linkedmap.go @@ -0,0 +1,159 @@ +package collections + +import ( + "fmt" +) + +// similar to linkes hash map in Java, a map that preserves insertion order + +var checkStatus = false + +type Node[K comparable, V any] struct { + key K + value V + prev *Node[K, V] + next *Node[K, V] +} + +type LinkedMap[K comparable, V any] struct { + first *Node[K, V] + last *Node[K, V] + collection map[K]*Node[K, V] +} + +func NewLinkedMap[K comparable, V any]() *LinkedMap[K, V] { + res := LinkedMap[K, V]{ + first: nil, + last: nil, + collection: make(map[K]*Node[K, V]), + } + res.check() + return &res +} + +func (m *LinkedMap[K, V]) Len() int { + return len(m.collection) +} + +func (m *LinkedMap[K, V]) Put(key K, value V) { + defer m.check() + newNode := &Node[K, V]{ + key: key, + value: value, + prev: m.last, + next: nil, + } + if m.first == nil { + m.first = newNode + m.last = m.first + m.collection[key] = m.first + return + } + m.Delete(key) + m.last.next = newNode + m.last = newNode + m.collection[key] = newNode +} + +func (m *LinkedMap[K, V]) Delete(key K) bool { + defer m.check() + node, ok := m.collection[key] + if !ok { + return false + } + if node.prev != nil { + node.prev.next = node.next + } else { + m.first = node.next + } + if node.next != nil { + node.next.prev = node.prev + } else { + m.last = node.prev + } + delete(m.collection, key) + return true +} + +func (m *LinkedMap[K, V]) Get(key K) (V, bool) { + defer m.check() + v, ok := m.collection[key] + if !ok { + return *new(V), false + } + return v.value, true +} + +func (m *LinkedMap[K, V]) Contains(key K) bool { + _, ok := m.collection[key] + return ok +} + +type Entry[K comparable, V any] struct { + Key K + Value V +} + +func (m *LinkedMap[K, V]) RangeKeys() <-chan K { + defer m.check() + res := make(chan K, len(m.collection)) + for node := m.first; node != nil; node = node.next { + res <- node.key + } + close(res) + return res +} + +func (m *LinkedMap[K, V]) RangeValues() <-chan V { + defer m.check() + res := make(chan V, len(m.collection)) + for node := m.first; node != nil; node = node.next { + res <- node.value + } + close(res) + return res +} + +func (m *LinkedMap[K, V]) RangeEntries() <-chan Entry[K, V] { + defer m.check() + res := make(chan Entry[K, V], len(m.collection)) + for node := m.first; node != nil; node = node.next { + res <- Entry[K, V]{ + Key: node.key, + Value: node.value, + } + } + close(res) + return res +} + +func (m *LinkedMap[K, V]) check() { + if !checkStatus { + return + } + assert := func(c bool, text string) { + if !c { + panic(text) + } + } + if m.first == nil { + assert(m.last == nil, "Last should be nil") + } + if m.first != nil { + assert(m.last != nil, "Last must not be nil") + } + if m.first == nil { + assert(m.Len() == 0, "Len should be 0") + } + if m.first != nil { + assert(m.Len() > 0, "Len should be > 0") + count := 1 + for node := m.first; node.next != nil && count < 1000; node = node.next { + if node.prev != nil { + assert(node.prev.next == node, "Broken link between nodes") + } + count++ + } + assert(count == m.Len(), fmt.Sprintf("Len expected %d, got %d", count, m.Len())) + } +} diff --git a/pkg/support/collections/linkedmap_test.go b/pkg/support/collections/linkedmap_test.go new file mode 100644 index 0000000..2111973 --- /dev/null +++ b/pkg/support/collections/linkedmap_test.go @@ -0,0 +1,130 @@ +package collections + +import ( + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func TestMain(m *testing.M) { + checkStatus = true + exitCode := m.Run() + checkStatus = false + os.Exit(exitCode) +} + +func contentCheck(t *testing.T, m *LinkedMap[string, int], + keys []string, values []int) { + + assert.True(t, len(keys) == len(values), "input error expected keys and values differ in length") + + // keys + i := 0 + for key := range m.RangeKeys() { + assert.True(t, i < len(keys), "Too many elements in map") + assert.Equal(t, keys[i], key) + i++ + } + assert.Equal(t, len(keys), i) + + // values + i = 0 + for value := range m.RangeValues() { + assert.True(t, i < len(values), "Too many elements in map") + assert.Equal(t, values[i], value) + i++ + } + assert.Equal(t, len(values), i) + + // Entries + i = 0 + for entry := range m.RangeEntries() { + assert.True(t, i < len(values), "Too many elements in map") + assert.Equal(t, keys[i], entry.Key) + assert.Equal(t, values[i], entry.Value) + i++ + } + assert.Equal(t, len(values), i) + + // Get and Contains + for i, key := range keys { + v, ok := m.Get(key) + assert.True(t, ok) + assert.Equal(t, values[i], v) + assert.True(t, m.Contains(key)) + } +} + +func Test_emptymap(t *testing.T) { + m := NewLinkedMap[string, int]() + contentCheck(t, m, []string{}, []int{}) +} + +func Test_elementAddRemove(t *testing.T) { + m := NewLinkedMap[string, int]() + m.Put("a", 1) + contentCheck(t, m, []string{"a"}, []int{1}) + + assert.False(t, m.Delete("b")) + contentCheck(t, m, []string{"a"}, []int{1}) + + assert.True(t, m.Delete("a")) + contentCheck(t, m, []string{}, []int{}) +} + +func Test_GetContainsForElementsNotInMap(t *testing.T) { + m := createSimpleMap(t) + + assert.False(t, m.Contains("d")) + val, ok := m.Get("d") + assert.False(t, ok) + assert.Equal(t, 0, val) +} + +func Test_elementRemoveBeginning(t *testing.T) { + m := createSimpleMap(t) + + assert.True(t, m.Delete("a")) + contentCheck(t, m, []string{"b", "c"}, []int{2, 3}) +} + +func Test_elementRemoveMiddle(t *testing.T) { + m := createSimpleMap(t) + + assert.True(t, m.Delete("b")) + contentCheck(t, m, []string{"a", "c"}, []int{1, 3}) +} + +func Test_elementRemoveEnd(t *testing.T) { + m := createSimpleMap(t) + + assert.True(t, m.Delete("c")) + contentCheck(t, m, []string{"a", "b"}, []int{1, 2}) +} + +func Test_addSameElementAgain(t *testing.T) { + m := createSimpleMap(t) + + m.Put("b", 4) + contentCheck(t, m, []string{"a", "c", "b"}, []int{1, 3, 4}) +} + +func createSimpleMap(t *testing.T) *LinkedMap[string, int] { + m := NewLinkedMap[string, int]() + m.Put("a", 1) + m.Put("b", 2) + m.Put("c", 3) + contentCheck(t, m, []string{"a", "b", "c"}, []int{1, 2, 3}) + return m +} + +func Test_manyElements(t *testing.T) { + m := NewLinkedMap[string, int]() + chars := "0123456789" + for i := 0; i < 10000; i++ { + m.Put(chars[i%10:i%10+1], i) + } + contentCheck(t, m, + []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}, + []int{9990, 9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999}) +}
shell
{string(client.ClientId)} {client.StartTime.In(location).Format(time.DateTime)}