moved stores to package

This commit is contained in:
Annika Hannig 2021-10-20 20:26:37 +00:00
parent 76236cb311
commit d52e06272d
No known key found for this signature in database
GPG Key ID: 62E226E47DDCE58D
10 changed files with 149 additions and 171 deletions

View File

@ -8,7 +8,7 @@ import (
// Route is a prefix with BGP information.
type Route struct {
ID string `json:"id"`
NeighborID string `json:"neighbor_id"`
NeighborID string `json:"neighbour_id"`
Network string `json:"network"`
Interface string `json:"interface"`
@ -109,7 +109,7 @@ type Searchable struct {
// LookupRoute is a route with additional
// neighbor and state information
type LookupRoute struct {
Route
*Route
State string `json:"state"` // Filtered, Imported, ...

View File

@ -193,7 +193,7 @@ func (cfg *Config) SourceInstanceByID(id string) sources.Source {
}
// Get instance from config
return sourceConfig.getInstance()
return sourceConfig.GetInstance()
}
func isSourceBase(section *ini.Section) bool {
@ -844,7 +844,7 @@ func LoadConfig(file string) (*Config, error) {
}
// Get source instance from config
func (cfg *SourceConfig) getInstance() sources.Source {
func (cfg *SourceConfig) GetInstance() sources.Source {
if cfg.instance != nil {
return cfg.instance
}

View File

@ -22,3 +22,15 @@ func TestDurationTimeframe(t *testing.T) {
}
}
func TestTrimmedCSVStringList(t *testing.T) {
l := TrimmedCSVStringList("foo, bar , dreiundzwanzig,")
if len(l) != 3 {
t.Error("Expected length to be 3, got:", len(l))
}
if l[0] != "foo" || l[1] != "bar" || l[2] != "dreiundzwanzig" {
t.Error("Expected list of [foo, bar, dreiundzwanzig], got:", l)
}
}

View File

@ -314,21 +314,7 @@ func (b *GenericBirdwatcher) LookupPrefix(
// A less bruteforce approach would be highly appreciated
route := &api.LookupRoute{
RouteServer: rs,
Route: api.Route{
ID: src.ID,
NeighborID: src.NeighborID,
Network: src.Network,
Interface: src.Interface,
Gateway: src.Gateway,
Metric: src.Metric,
BGP: src.BGP,
Age: src.Age,
Type: src.Type,
Details: src.Details,
},
Route: src,
}
results = append(results, route)
}

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/alice-lg/alice-lg/pkg/api"
"github.com/alice-lg/alice-lg/pkg/config"
)
var REGEX_MATCH_ASLOOKUP = regexp.MustCompile(`(?i)^AS(\d+)`)
@ -16,7 +17,7 @@ type NeighborsIndex map[string]*api.Neighbor
type NeighborsStore struct {
neighborsMap map[string]NeighborsIndex
configMap map[string]*SourceConfig
cfgMap map[string]*config.SourceConfig
statusMap map[string]StoreStatus
refreshInterval time.Duration
refreshNeighborStatus bool
@ -26,16 +27,16 @@ type NeighborsStore struct {
}
// NewNeighborsStore creates a new store for neighbors
func NewNeighborsStore(config *Config) *NeighborsStore {
func NewNeighborsStore(cfg *config.Config) *NeighborsStore {
// Build source mapping
neighborsMap := make(map[string]NeighborsIndex)
configMap := make(map[string]*SourceConfig)
cfgMap := make(map[string]*config.SourceConfig)
statusMap := make(map[string]StoreStatus)
for _, source := range config.Sources {
for _, source := range cfg.Sources {
id := source.ID
configMap[id] = source
cfgMap[id] = source
statusMap[id] = StoreStatus{
State: STATE_INIT,
}
@ -46,17 +47,17 @@ func NewNeighborsStore(config *Config) *NeighborsStore {
// Set refresh interval, default to 5 minutes when
// interval is set to 0
refreshInterval := time.Duration(
config.Server.NeighborsStoreRefreshInterval) * time.Minute
cfg.Server.NeighborsStoreRefreshInterval) * time.Minute
if refreshInterval == 0 {
refreshInterval = time.Duration(5) * time.Minute
}
refreshNeighborStatus := config.Server.EnableNeighborsStatusRefresh
refreshNeighborStatus := cfg.Server.EnableNeighborsStatusRefresh
store := &NeighborsStore{
neighborsMap: neighborsMap,
statusMap: statusMap,
configMap: configMap,
cfgMap: cfgMap,
refreshInterval: refreshInterval,
refreshNeighborStatus: refreshNeighborStatus,
}
@ -84,17 +85,17 @@ func (self *NeighborsStore) init() {
}
}
func (self *NeighborsStore) SourceStatus(sourceId string) StoreStatus {
func (self *NeighborsStore) SourceStatus(sourceID string) StoreStatus {
self.RLock()
status := self.statusMap[sourceId]
status := self.statusMap[sourceID]
self.RUnlock()
return status
}
// Get state by source Id
func (self *NeighborsStore) SourceState(sourceId string) int {
status := self.SourceStatus(sourceId)
// Get state by source ID
func (self *NeighborsStore) SourceState(sourceID string) int {
status := self.SourceStatus(sourceID)
return status.State
}
@ -103,21 +104,21 @@ func (self *NeighborsStore) update() {
successCount := 0
errorCount := 0
t0 := time.Now()
for sourceId, _ := range self.neighborsMap {
for sourceID, _ := range self.neighborsMap {
// Get current state
if self.statusMap[sourceId].State == STATE_UPDATING {
if self.statusMap[sourceID].State == STATE_UPDATING {
continue // nothing to do here. really.
}
// Start updating
self.Lock()
self.statusMap[sourceId] = StoreStatus{
self.statusMap[sourceID] = StoreStatus{
State: STATE_UPDATING,
}
self.Unlock()
sourceConfig := self.configMap[sourceId]
source := sourceConfig.getInstance()
sourceConfig := self.cfgMap[sourceID]
source := sourceConfig.GetInstance()
neighborsRes, err := source.Neighbors()
if err != nil {
@ -129,7 +130,7 @@ func (self *NeighborsStore) update() {
)
// That's sad.
self.Lock()
self.statusMap[sourceId] = StoreStatus{
self.statusMap[sourceID] = StoreStatus{
State: STATE_ERROR,
LastError: err,
LastRefresh: time.Now(),
@ -146,13 +147,13 @@ func (self *NeighborsStore) update() {
// Make neighbors index
index := make(NeighborsIndex)
for _, neighbor := range neighbors {
index[neighbor.Id] = neighbor
index[neighbor.ID] = neighbor
}
self.Lock()
self.neighborsMap[sourceId] = index
self.neighborsMap[sourceID] = index
// Update state
self.statusMap[sourceId] = StoreStatus{
self.statusMap[sourceID] = StoreStatus{
LastRefresh: time.Now(),
State: STATE_READY,
}
@ -168,33 +169,33 @@ func (self *NeighborsStore) update() {
)
}
func (self *NeighborsStore) GetNeighborsAt(sourceId string) api.Neighbors {
func (self *NeighborsStore) GetNeighborsAt(sourceID string) api.Neighbors {
self.RLock()
neighborsIdx := self.neighborsMap[sourceId]
neighborsIDx := self.neighborsMap[sourceID]
self.RUnlock()
var neighborsStatus map[string]api.NeighborStatus
if self.refreshNeighborStatus {
sourceConfig := self.configMap[sourceId]
source := sourceConfig.getInstance()
sourceConfig := self.cfgMap[sourceID]
source := sourceConfig.GetInstance()
neighborsStatusData, err := source.NeighborsStatus()
if err == nil {
neighborsStatus = make(map[string]api.NeighborStatus, len(neighborsStatusData.Neighbors))
for _, neighbor := range neighborsStatusData.Neighbors {
neighborsStatus[neighbor.Id] = *neighbor
neighborsStatus[neighbor.ID] = *neighbor
}
}
}
neighbors := make(api.Neighbors, 0, len(neighborsIdx))
neighbors := make(api.Neighbors, 0, len(neighborsIDx))
for _, neighbor := range neighborsIdx {
for _, neighbor := range neighborsIDx {
if self.refreshNeighborStatus {
if _, ok := neighborsStatus[neighbor.Id]; ok {
if _, ok := neighborsStatus[neighbor.ID]; ok {
self.Lock()
neighbor.State = neighborsStatus[neighbor.Id].State
neighbor.State = neighborsStatus[neighbor.ID].State
self.Unlock()
}
}
@ -206,25 +207,25 @@ func (self *NeighborsStore) GetNeighborsAt(sourceId string) api.Neighbors {
}
func (self *NeighborsStore) GetNeighborAt(
sourceId string,
sourceID string,
id string,
) *api.Neighbor {
// Lookup neighbor on RS
self.RLock()
neighborsIdx := self.neighborsMap[sourceId]
neighborsIDx := self.neighborsMap[sourceID]
self.RUnlock()
return neighborsIdx[id]
return neighborsIDx[id]
}
func (self *NeighborsStore) LookupNeighborsAt(
sourceId string,
sourceID string,
query string,
) api.Neighbors {
results := api.Neighbors{}
self.RLock()
neighbors := self.neighborsMap[sourceId]
neighbors := self.neighborsMap[sourceID]
self.RUnlock()
asn := -1
@ -236,7 +237,7 @@ func (self *NeighborsStore) LookupNeighborsAt(
}
for _, neighbor := range neighbors {
if asn >= 0 && neighbor.Asn == asn { // only executed if valid AS query is detected
if asn >= 0 && neighbor.ASN == asn { // only executed if valid AS query is detected
results = append(results, neighbor)
} else if ContainsCi(neighbor.Description, query) {
results = append(results, neighbor)
@ -254,8 +255,8 @@ func (self *NeighborsStore) LookupNeighbors(
// Create empty result set
results := make(api.NeighborsLookupResults)
for sourceId, _ := range self.neighborsMap {
results[sourceId] = self.LookupNeighborsAt(sourceId, query)
for sourceID, _ := range self.neighborsMap {
results[sourceID] = self.LookupNeighborsAt(sourceID, query)
}
return results
@ -265,13 +266,13 @@ func (self *NeighborsStore) LookupNeighbors(
Filter neighbors from a single route server.
*/
func (self *NeighborsStore) FilterNeighborsAt(
sourceId string,
sourceID string,
filter *api.NeighborFilter,
) api.Neighbors {
results := []*api.Neighbor{}
self.RLock()
neighbors := self.neighborsMap[sourceId]
neighbors := self.neighborsMap[sourceID]
self.RUnlock()
// Apply filters
@ -293,8 +294,8 @@ func (self *NeighborsStore) FilterNeighbors(
results := []*api.Neighbor{}
// Get neighbors from all routeservers
for sourceId, _ := range self.neighborsMap {
rsResults := self.FilterNeighborsAt(sourceId, filter)
for sourceID, _ := range self.neighborsMap {
rsResults := self.FilterNeighborsAt(sourceID, filter)
results = append(results, rsResults...)
}
@ -307,11 +308,11 @@ func (self *NeighborsStore) Stats() NeighborsStoreStats {
rsStats := []RouteServerNeighborsStats{}
self.RLock()
for sourceId, neighbors := range self.neighborsMap {
status := self.statusMap[sourceId]
for sourceID, neighbors := range self.neighborsMap {
status := self.statusMap[sourceID]
totalNeighbors += len(neighbors)
serverStats := RouteServerNeighborsStats{
Name: self.configMap[sourceId].Name,
Name: self.cfgMap[sourceID].Name,
State: stateToString(status.State),
Neighbors: len(neighbors),
UpdatedAt: status.LastRefresh,

View File

@ -7,15 +7,6 @@ import (
"github.com/alice-lg/alice-lg/pkg/api"
)
/*
Start the global neighbors store,
because the route store in the tests have
this as a dependency.
*/
func startTestNeighborsStore() {
store := makeTestNeighborsStore()
AliceNeighborsStore = store
}
/*
Make a store and populate it with data
@ -25,37 +16,37 @@ func makeTestNeighborsStore() *NeighborsStore {
// Populate neighbors
rs1 := NeighborsIndex{
"ID2233_AS2342": &api.Neighbor{
Id: "ID2233_AS2342",
Asn: 2342,
ID: "ID2233_AS2342",
ASN: 2342,
Description: "PEER AS2342 192.9.23.42 Customer Peer 1",
RouteServerId: "rs1",
RouteServerID: "rs1",
},
"ID2233_AS2343": &api.Neighbor{
Id: "ID2233_AS2343",
Asn: 2343,
ID: "ID2233_AS2343",
ASN: 2343,
Description: "PEER AS2343 192.9.23.43 Different Peer 1",
RouteServerId: "rs1",
RouteServerID: "rs1",
},
"ID2233_AS2344": &api.Neighbor{
Id: "ID2233_AS2344",
Asn: 2344,
ID: "ID2233_AS2344",
ASN: 2344,
Description: "PEER AS2344 192.9.23.44 3rd Peer from the sun",
RouteServerId: "rs1",
RouteServerID: "rs1",
},
}
rs2 := NeighborsIndex{
"ID2233_AS2342": &api.Neighbor{
Id: "ID2233_AS2342",
Asn: 2342,
ID: "ID2233_AS2342",
ASN: 2342,
Description: "PEER AS2342 192.9.23.42 Customer Peer 1",
RouteServerId: "rs2",
RouteServerID: "rs2",
},
"ID2233_AS4223": &api.Neighbor{
Id: "ID2233_AS4223",
Asn: 4223,
ID: "ID2233_AS4223",
ASN: 4223,
Description: "PEER AS4223 192.9.42.23 Cloudfoo Inc.",
RouteServerId: "rs2",
RouteServerID: "rs2",
},
}
@ -94,7 +85,7 @@ func TestGetNeighborAt(t *testing.T) {
store := makeTestNeighborsStore()
neighbor := store.GetNeighborAt("rs1", "ID2233_AS2343")
if neighbor.Id != "ID2233_AS2343" {
if neighbor.ID != "ID2233_AS2343" {
t.Error("Expected another peer in GetNeighborAt")
}
}
@ -109,7 +100,7 @@ func TestGetNeighbors(t *testing.T) {
sort.Sort(neighbors)
if neighbors[0].Id != "ID2233_AS2342" {
if neighbors[0].ID != "ID2233_AS2342" {
t.Error("Expected neighbor: ID2233_AS2342, got:",
neighbors[0])
}
@ -134,7 +125,7 @@ func TestNeighborLookupAt(t *testing.T) {
// Make index
index := NeighborsIndex{}
for _, n := range neighbors {
index[n.Id] = n
index[n.ID] = n
}
for _, id := range expected {
@ -164,7 +155,7 @@ func TestNeighborLookup(t *testing.T) {
}
n := neighbors[0]
if n.Id != "ID2233_AS4223" {
if n.ID != "ID2233_AS4223" {
t.Error("Wrong peer in lookup response")
}
}

View File

@ -7,35 +7,41 @@ import (
"time"
"github.com/alice-lg/alice-lg/pkg/api"
"github.com/alice-lg/alice-lg/pkg/config"
)
// The RoutesStore holds a mapping of routes,
// status and configs and will be queried instead
// status and cfgs and will be queried instead
// of a backend by the API
type RoutesStore struct {
routesMap map[string]*api.RoutesResponse
statusMap map[string]StoreStatus
configMap map[string]*SourceConfig
cfgMap map[string]*config.SourceConfig
refreshInterval time.Duration
lastRefresh time.Time
neighborsStore *NeighborsStore
sync.RWMutex
}
// NewRoutesStore makes a new store instance
// with a config.
func NewRoutesStore(config *Config) *RoutesStore {
// with a cfg.
func NewRoutesStore(
neighborsStore *NeighborsStore,
cfg *config.Config,
) *RoutesStore {
// Build mapping based on source instances
routesMap := make(map[string]*api.RoutesResponse)
statusMap := make(map[string]StoreStatus)
configMap := make(map[string]*SourceConfig)
cfgMap := make(map[string]*config.SourceConfig)
for _, source := range config.Sources {
for _, source := range cfg.Sources {
id := source.ID
configMap[id] = source
cfgMap[id] = source
routesMap[id] = &api.RoutesResponse{}
statusMap[id] = StoreStatus{
State: STATE_INIT,
@ -45,7 +51,7 @@ func NewRoutesStore(config *Config) *RoutesStore {
// Set refresh interval as duration, fall back to
// five minutes if no interval is set.
refreshInterval := time.Duration(
config.Server.RoutesStoreRefreshInterval) * time.Minute
cfg.Server.RoutesStoreRefreshInterval) * time.Minute
if refreshInterval == 0 {
refreshInterval = time.Duration(5) * time.Minute
}
@ -53,8 +59,9 @@ func NewRoutesStore(config *Config) *RoutesStore {
store := &RoutesStore{
routesMap: routesMap,
statusMap: statusMap,
configMap: configMap,
cfgMap: cfgMap,
refreshInterval: refreshInterval,
neighborsStore: neighborsStore,
}
return store
}
@ -90,8 +97,8 @@ func (rs *RoutesStore) update() {
t0 := time.Now()
for sourceID := range rs.routesMap {
sourceConfig := rs.configMap[sourceID]
source := sourceConfig.getInstance()
sourceConfig := rs.cfgMap[sourceID]
source := sourceConfig.GetInstance()
// Get current update state
if rs.statusMap[sourceID].State == STATE_UPDATING {
@ -163,7 +170,7 @@ func (rs *RoutesStore) Stats() RoutesStoreStats {
totalFiltered += len(routes.Filtered)
serverStats := RouteServerRoutesStats{
Name: rs.configMap[sourceID].Name,
Name: rs.cfgMap[sourceID].Name,
Routes: RoutesStats{
Filtered: len(routes.Filtered),
@ -201,44 +208,29 @@ func (rs *RoutesStore) CacheTTL() time.Time {
// Lookup routes transform
func routeToLookupRoute(
source *SourceConfig,
nStore *NeighborsStore,
source *config.SourceConfig,
state string,
route *api.Route,
) *api.LookupRoute {
// Get neighbor
neighbor := AliceNeighborsStore.GetNeighborAt(source.ID, route.NeighborId)
// Make route
// Get neighbor and make route
neighbor := nStore.GetNeighborAt(source.ID, route.NeighborID)
lookup := &api.LookupRoute{
Id: route.Id,
NeighborId: route.NeighborId,
Route: route,
State: state,
Neighbor: neighbor,
Routeserver: api.Routeserver{
Id: source.ID,
RouteServer: &api.RouteServer{
ID: source.ID,
Name: source.Name,
},
State: state,
Network: route.Network,
Interface: route.Interface,
Gateway: route.Gateway,
Metric: route.Metric,
Bgp: route.Bgp,
Age: route.Age,
Type: route.Type,
Primary: route.Primary,
}
return lookup
}
// Routes filter
func filterRoutesByPrefix(
source *SourceConfig,
nStore *NeighborsStore,
source *config.SourceConfig,
routes api.Routes,
prefix string,
state string,
@ -247,25 +239,27 @@ func filterRoutesByPrefix(
for _, route := range routes {
// Naiive filtering:
if strings.HasPrefix(strings.ToLower(route.Network), prefix) {
lookup := routeToLookupRoute(source, state, route)
lookup := routeToLookupRoute(nStore, source, state, route)
results = append(results, lookup)
}
}
return results
}
func filterRoutesByNeighborIds(
source *SourceConfig,
func filterRoutesByNeighborIDs(
nStore *NeighborsStore,
source *config.SourceConfig,
routes api.Routes,
neighborIds []string,
neighborIDs []string,
state string,
) api.LookupRoutes {
results := api.LookupRoutes{}
for _, route := range routes {
// Filtering:
if MemberOf(neighborIds, route.NeighborId) == true {
lookup := routeToLookupRoute(source, state, route)
log.Println("r:", route.NeighborID)
if MemberOf(neighborIDs, route.NeighborID) == true {
lookup := routeToLookupRoute(nStore, source, state, route)
results = append(results, lookup)
}
}
@ -276,25 +270,27 @@ func filterRoutesByNeighborIds(
// routes lookup by neighbor id
func (rs *RoutesStore) LookupNeighborsPrefixesAt(
sourceID string,
neighborIds []string,
neighborIDs []string,
) chan api.LookupRoutes {
response := make(chan api.LookupRoutes)
go func() {
rs.RLock()
source := rs.configMap[sourceID]
source := rs.cfgMap[sourceID]
routes := rs.routesMap[sourceID]
rs.RUnlock()
filtered := filterRoutesByNeighborIds(
filtered := filterRoutesByNeighborIDs(
rs.neighborsStore,
source,
routes.Filtered,
neighborIds,
neighborIDs,
"filtered")
imported := filterRoutesByNeighborIds(
imported := filterRoutesByNeighborIDs(
rs.neighborsStore,
source,
routes.Imported,
neighborIds,
neighborIDs,
"imported")
var result api.LookupRoutes
@ -316,17 +312,19 @@ func (rs *RoutesStore) LookupPrefixAt(
go func() {
rs.RLock()
config := rs.configMap[sourceID]
cfg := rs.cfgMap[sourceID]
routes := rs.routesMap[sourceID]
rs.RUnlock()
filtered := filterRoutesByPrefix(
config,
rs.neighborsStore,
cfg,
routes.Filtered,
prefix,
"filtered")
imported := filterRoutesByPrefix(
config,
rs.neighborsStore,
cfg,
routes.Imported,
prefix,
"imported")
@ -377,12 +375,11 @@ func (rs *RoutesStore) LookupPrefixForNeighbors(
// Dispatch
for sourceID, locals := range neighbors {
lookupNeighborIds := []string{}
lookupNeighborIDs := []string{}
for _, n := range locals {
lookupNeighborIds = append(lookupNeighborIds, n.Id)
lookupNeighborIDs = append(lookupNeighborIDs, n.ID)
}
res := rs.LookupNeighborsPrefixesAt(sourceID, lookupNeighborIds)
res := rs.LookupNeighborsPrefixesAt(sourceID, lookupNeighborIDs)
responses = append(responses, res)
}

View File

@ -10,6 +10,7 @@ import (
"io/ioutil"
"github.com/alice-lg/alice-lg/pkg/api"
"github.com/alice-lg/alice-lg/pkg/config"
"github.com/alice-lg/alice-lg/pkg/sources/birdwatcher"
)
@ -68,6 +69,9 @@ func testCheckPrefixesPresence(prefixes, resultset []string, t *testing.T) {
//
func makeTestRoutesStore() *RoutesStore {
neighborsStore := makeTestNeighborsStore()
rs1RoutesResponse := loadTestRoutesResponse()
// Build mapping based on source instances:
@ -77,11 +81,11 @@ func makeTestRoutesStore() *RoutesStore {
"rs1": rs1RoutesResponse,
}
configMap := map[string]*SourceConfig{
"rs1": &SourceConfig{
configMap := map[string]*config.SourceConfig{
"rs1": &config.SourceConfig{
ID: "rs1",
Name: "rs1.test",
Type: SourceTypeBird,
Type: config.SourceTypeBird,
Birdwatcher: birdwatcher.Config{
API: "http://localhost:2342",
@ -96,7 +100,8 @@ func makeTestRoutesStore() *RoutesStore {
store := &RoutesStore{
routesMap: routesMap,
statusMap: statusMap,
configMap: configMap,
cfgMap: configMap,
neighborsStore: neighborsStore,
}
return store
@ -125,7 +130,6 @@ func TestRoutesStoreStats(t *testing.T) {
}
func TestLookupPrefixAt(t *testing.T) {
startTestNeighborsStore()
store := makeTestRoutesStore()
query := "193.200."
@ -146,7 +150,6 @@ func TestLookupPrefixAt(t *testing.T) {
}
func TestLookupPrefix(t *testing.T) {
startTestNeighborsStore()
store := makeTestRoutesStore()
query := "193.200."
@ -169,7 +172,6 @@ func TestLookupPrefix(t *testing.T) {
}
func TestLookupNeighborsPrefixesAt(t *testing.T) {
startTestNeighborsStore()
store := makeTestRoutesStore()
// Query
@ -195,16 +197,16 @@ func TestLookupPrefixForNeighbors(t *testing.T) {
neighbors := api.NeighborsLookupResults{
"rs1": api.Neighbors{
&api.Neighbor{
Id: "ID163_AS31078",
ID: "ID163_AS31078",
},
},
}
startTestNeighborsStore()
store := makeTestRoutesStore()
// Query
results := store.LookupPrefixForNeighbors(neighbors)
t.Log(results)
// We should have retrived 8 prefixes,
if len(results) != 8 {

View File

@ -1,4 +1,4 @@
package backend
package store
// Some helper functions
import (

View File

@ -1,4 +1,4 @@
package backend
package store
import (
"testing"
@ -36,14 +36,3 @@ func TestMaybePrefix(t *testing.T) {
}
}
func TestTrimmedStringList(t *testing.T) {
l := TrimmedStringList("foo, bar , dreiundzwanzig,")
if len(l) != 3 {
t.Error("Expected length to be 3, got:", len(l))
}
if l[0] != "foo" || l[1] != "bar" || l[2] != "dreiundzwanzig" {
t.Error("Expected list of [foo, bar, dreiundzwanzig], got:", l)
}
}